<commit_before>
import webfinger from '../lib/webfinger/lib/webfinger.js';

const dispatchToActor = function(actor, activity) {
    HTTP.post(actor.inbox, {data: activity, npmRequestOptions: {
        httpSignature: {
            authorizationHeaderName: "Signature",
            keyId: activity.actor + "#main-key",
            key: Keys.findOne({id: activity.actor + "#main-key"}, {fields: {privateKeyPem: 1}}).privateKeyPem
        }
    }}, function(err, result) {
        if (err) console.log("Error: ", err);
        if (result) console.log("Result: ", result);
    });
};

dispatchActivity = function(activity) {

    activity = cleanActivityPub(activity);

    const targetArrays = ['to', 'cc', 'bto', 'bcc', 'audience'];

    for (let i = 0; i < targetArrays.length; i++) {
        const arrayName = targetArrays[i];
        const targetArray = activity[arrayName];
        for (let j = 0; j < targetArray.length; j++) {
            const targetID = targetArray[j];
            if (targetID === "https://www.w3.org/ns/activitystreams#Public")
                continue;
            let actor = Actors.findOne({id: targetID});
            if (actor)
                dispatchToActor(actor, activity);
            else {
                const list = FollowerLists.findOne({id: targetID});
                if (list)
                    for (let actorID in list.orderedItems) {
                        actor = Actors.findOne({id: actorID});
                        if (actor)
                            dispatchToActor(actor, activity);
                    }
            }
        }
    }
};

const checkFederatedActivityPermitted = function(activity) {

    //TODO: Check to see if foreign instances are blocked or banned.

    const actor = Actors.findOne({id: activity.actor});

    if (!actor)
        throw new Meteor.Error('Actor not found!', 'No actor with the given ID could be found in the database: ' + activity.actor);

    switch(activity.type) {

        //Users can follow without being verified. Thus, return here, instead of further down after the verification check.
        case 'Follow':
        case 'Undo':
            return;

        case 'Update':
        case 'Delete':

            checkUpdateOrDeleteActivityPermitted(activity);

        //No break here, as update and delete activities should be subject to the same restrictions as create and announce.
        case 'Create':
        case 'Announce':
    }

    //TODO: Check to see if foreign actors are blocked or banned.
};

const processFederatedFollowActivity = function(activity) {
    const followee = Actors.findOne({id: activity.object});

    if (followee.local)
        FollowerLists.update({id: followee.followers}, {$inc: {totalItems: 1}, $push: {orderedItems: activity.actor}});
};

const processFederatedCreateActivity = function(activity) {
    const post = getObjectFromActivity(activity);

    if (Posts.findOne({id: post.id}))
        throw new Meteor.Error('Post id already exists', 'Cannot insert post, id already exists.');

    Posts.insert(post);

    return activity;
};

const processFederatedDeleteActivity = function(activity) {
    const postID = activity.object;

    deletePost(postID);

    return activity;
};

const processFederatedAcceptActivity = function(activity) {

    const follower = Actors.findOne({id: activity.object});

    if (!follower)
        throw new Meteor.Error('Actor not found!', 'No actor with the given ID could be found: ' + activity.object);

    const followee = Actors.findOne({id: activity.actor});

    let pendingFollow = PendingFollows.findOne({follower: follower.id, followee: followee.id});

    if (!pendingFollow)
        throw new Meteor.Error('No Follow Pending!', 'No pending follow between those actors was found: ' + activity.object + ", " + activity.actor);

    FollowingLists.update({id: follower.following}, {$inc: {totalItems: 1}, $push: {orderedItems: activity.actor}});

    if (followee.local)
        FollowerLists.update({id: followee.followers}, {$inc: {totalItems: 1}, $push: {orderedItems: activity.object}});

    PendingFollows.remove(pendingFollow);

    return activity;
};

const processFederatedUndoActivity = function(activity) {

    let targetActivity = Activities.findOne({id: activity.object});

    if (targetActivity.type === "Follow"){

        const follower = Actors.findOne({id: targetActivity.actor});

        if (!follower)
            throw new Meteor.Error('Actor not found!', 'No actor with the given ID could be found: ' + targetActivity.object);

        const followee = Actors.findOne({id: targetActivity.actor});

        FollowingLists.update({id: follower.following}, {$inc: {totalItems: -1}, $pull: {orderedItems: targetActivity.actor}});

        if (followee.local)
            FollowerLists.update({id: followee.followers}, {$inc: {totalItems: -1}, $pull: {orderedItems: targetActivity.object}});

        PendingFollows.remove({follower: follower.id, followee: followee.id});

        return activity;
    }
};

const processFederatedActivity = function(activity) {

    if (activity.id && Activities.findOne({id: activity.id}))
        return;

    checkFederatedActivityPermitted(activity);

    switch(activity.type) {
        case 'Create':
            activity = processFederatedCreateActivity(activity);
            break;
        case 'Delete':
            activity = processFederatedDeleteActivity(activity);
            break;
        case "Accept":
            activity = processFederatedAcceptActivity(activity);
            break;
    }

    const _id = Activities.insert(activity);

    return Activities.findOne({_id: _id});
};

const importActorFromActivityPubJSON = function(json) {
    if (!Actors.findOne({id: json.id})) { //Is actor already present? If not,
        json.local = false; //mark it as foreign,
        Actors.insert(json); //then add it.
    }
};

const importPostFromActivityPubJSON = function(json) {
    if (!Posts.findOne({id: json.id})) { //Is post already present? If not,
        json.local = false; //mark it as foreign,
        Posts.insert(json); //then add it.
    }
};

Meteor.methods({
    getActivityJSONFromUrl: function(url) {
        return getActivityFromUrl(url)
        .then((response) => {
            if (response) {
                return response.json();
            } else throw new Meteor.Error('No response from url');
        })
        .then((json) => {
            if (json) Meteor.setTimeout(function(){
                Meteor.call('importFromActivityPubJSON', json);
            }, 0);

            return json;
        });
    },
    importFromActivityPubJSON: function(json) {
        if (!json) throw new Meteor.Error('Empty JSON');

        if (!json.type) throw new Meteor.Error('Untyped ActivityPub JSON');

        if (activityPubActorTypes.includes(json.type))
            importActorFromActivityPubJSON(json);

        else if (activityPubContentTypes.includes(json.type))
            importPostFromActivityPubJSON(json);
    },
    postActivity: function(object) {
        const user = Meteor.users.findOne({_id: this.userId});

        let result = processClientActivity(user, object);

        return result;
    }
});

const successfulJSON = function(data) {
    response = {
        statusCode: 200
    };

    if (data) {
        response.body = cleanActivityPub(data);
    }

    return response;
};

const failedJSON = function(message) {
    return {
        statusCode: 400,
        body: {status: "fail", message: message}
    };
};

Api = new Restivus({
    apiPath: '/',
    prettyJson: true
});

Api.addRoute('.well-known/host-meta', {}, {
    get: {
        action: function () {
            let response = successfulJSON();
            response.headers = {
                'Access-Control-Allow-Origin': '*',
                'Content-Type': 'application/xml; charset=UTF-8'
            };

            response.body = '<XRD>' +
                            '  <Link rel="lrdd"' +
                            '    type="application/xrd+xml"' +
                            '    template="' +
                            process.env.ROOT_URL + '/webfinger/xrd/{uri}" />' +
                            '</XRD>'

            return response;
        }
    }
});

Api.addRoute('.well-known/host-meta.json', {}, {
    get: {
        action: function () {
            let response = successfulJSON();
            response.headers = {
                'Access-Control-Allow-Origin': '*',
                'Content-Type': 'application/json; charset=UTF-8'
            };

            response.body = {
                links: [
                    {
                        "rel": "lrdd",
                        "type": "application/json",
                        "template": process.env.ROOT_URL + "/.well-known/webfinger?resource={uri}"
                    }
                ]
            }

            return response;
        }
    }
});

Api.addRoute('.well-known/webfinger', {}, {
    get: {
        action: function () {

            let resource = this.queryParams.resource;
            var re = /(acct:@?)(.*)(@.*)/;
            let handle = resource.replace(re, "$2");
            let actor = Actors.findOne({preferredUsername: handle, local: true});

            actor = cleanActivityPub(actor);

            let response = successfulJSON();

            response.headers = {
                'Content-Type': 'application/json; charset=UTF-8'
            };

            response.body = {
                subject: resource,
                aliases: [actor.url, actor.id],
                links: [
                    {
                        "rel":"http://webfinger.net/rel/profile-page",
                        "type":"text/html",
                        "href": actor.url
                    },
                    {
                        "rel":"self",
                        "type":"application/activity+json",
                        "href": actor.id
                    }
                ]
            }

            return response;
        }
    }
});

Api.addRoute('post/:_id', {}, {
    get: {
        action: function () {
            let post = Posts.findOne({_id: this.urlParams._id});
            if (post) {
                let response = successfulJSON(post);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get post!");
        }
    }
});

Api.addRoute('activity/:_id', {}, {
    get: {
        action: function () {
            let activity = Activities.findOne({_id: this.urlParams._id});
            if (activity) {
                let response = successfulJSON(activity);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get post!");
        }
    }
});

Api.addRoute('actors/:handle', {}, {
    get: {
        action: function () {
            let actor = Actors.findOne({preferredUsername: this.urlParams.handle});
            if (actor) {
                let response = successfulJSON(actor);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get actor!");
        }
    }
});

Api.addRoute('actors/:handle/inbox', {}, {
    post: {
        action: function () {
            let object = this.request.body;
            processFederatedActivity(object);
            return successfulJSON();
        }
    }
});

Api.addRoute('actors/:handle/outbox', {}, {
    get: {
        action: function () {
            var outbox = Outboxes.findOne({id: process.env.ROOT_URL + "actors/" + this.urlParams.handle + "/outbox"});
            if (outbox) {
                let orderedItems = [];
                for (let i = 0; i < outbox.totalItems && i < 10; i++) {
                    orderedItems.push(Activities.findOne({id: outbox.orderedItems[i]}));
                }
                outbox.orderedItems = orderedItems;
                let response = successfulJSON(outbox);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get outbox!");
        }
    },
    post: {
        action: function () {
            let object = this.request.body;

            try {
                return successfulJSON(processClientActivity(object));
            } catch (exception) {
                return failedJSON(exception.reason);
            }
        }
    }
});

Api.addRoute('actors/:handle/following', {}, {
    get: {
        action: function () {
            let following = FollowingLists.findOne({id: process.env.ROOT_URL + "actors/" + this.urlParams.handle + "/following"});
            if (following) {
                let response = successfulJSON(following);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get following list!");
        }
    }
});

Api.addRoute('actors/:handle/followers', {}, {
    get: {
        action: function () {
            let actor = Actors.findOne({preferredUsername: this.urlParams.handle});
            if (actor) {
                let followers = FollowerLists.findOne({id: actor.followers});
                if (followers) {
                    return successfulJSON(followers);
                }
            }

            return failedJSON("Unable to get followers list!");
        }
    }
});
<commit_msg>only show data for local actors<commit_after>
import webfinger from '../lib/webfinger/lib/webfinger.js';

const dispatchToActor = function(actor, activity) {
    HTTP.post(actor.inbox, {data: activity, npmRequestOptions: {
        httpSignature: {
            authorizationHeaderName: "Signature",
            keyId: activity.actor + "#main-key",
            key: Keys.findOne({id: activity.actor + "#main-key"}, {fields: {privateKeyPem: 1}}).privateKeyPem
        }
    }}, function(err, result) {
        if (err) console.log("Error: ", err);
        if (result) console.log("Result: ", result);
    });
};

dispatchActivity = function(activity) {

    activity = cleanActivityPub(activity);

    const targetArrays = ['to', 'cc', 'bto', 'bcc', 'audience'];

    for (let i = 0; i < targetArrays.length; i++) {
        const arrayName = targetArrays[i];
        const targetArray = activity[arrayName];
        for (let j = 0; j < targetArray.length; j++) {
            const targetID = targetArray[j];
            if (targetID === "https://www.w3.org/ns/activitystreams#Public")
                continue;
            let actor = Actors.findOne({id: targetID});
            if (actor)
                dispatchToActor(actor, activity);
            else {
                const list = FollowerLists.findOne({id: targetID});
                if (list)
                    for (let actorID in list.orderedItems) {
                        actor = Actors.findOne({id: actorID});
                        if (actor)
                            dispatchToActor(actor, activity);
                    }
            }
        }
    }
};

const checkFederatedActivityPermitted = function(activity) {

    //TODO: Check to see if foreign instances are blocked or banned.

    const actor = Actors.findOne({id: activity.actor});

    if (!actor)
        throw new Meteor.Error('Actor not found!', 'No actor with the given ID could be found in the database: ' + activity.actor);

    switch(activity.type) {

        //Users can follow without being verified. Thus, return here, instead of further down after the verification check.
        case 'Follow':
        case 'Undo':
            return;

        case 'Update':
        case 'Delete':

            checkUpdateOrDeleteActivityPermitted(activity);

        //No break here, as update and delete activities should be subject to the same restrictions as create and announce.
        case 'Create':
        case 'Announce':
    }

    //TODO: Check to see if foreign actors are blocked or banned.
};

const processFederatedFollowActivity = function(activity) {
    const followee = Actors.findOne({id: activity.object});

    if (followee.local)
        FollowerLists.update({id: followee.followers}, {$inc: {totalItems: 1}, $push: {orderedItems: activity.actor}});
};

const processFederatedCreateActivity = function(activity) {
    const post = getObjectFromActivity(activity);

    if (Posts.findOne({id: post.id}))
        throw new Meteor.Error('Post id already exists', 'Cannot insert post, id already exists.');

    Posts.insert(post);

    return activity;
};

const processFederatedDeleteActivity = function(activity) {
    const postID = activity.object;

    deletePost(postID);

    return activity;
};

const processFederatedAcceptActivity = function(activity) {

    const follower = Actors.findOne({id: activity.object});

    if (!follower)
        throw new Meteor.Error('Actor not found!', 'No actor with the given ID could be found: ' + activity.object);

    const followee = Actors.findOne({id: activity.actor});

    let pendingFollow = PendingFollows.findOne({follower: follower.id, followee: followee.id});

    if (!pendingFollow)
        throw new Meteor.Error('No Follow Pending!', 'No pending follow between those actors was found: ' + activity.object + ", " + activity.actor);

    FollowingLists.update({id: follower.following}, {$inc: {totalItems: 1}, $push: {orderedItems: activity.actor}});

    if (followee.local)
        FollowerLists.update({id: followee.followers}, {$inc: {totalItems: 1}, $push: {orderedItems: activity.object}});

    PendingFollows.remove(pendingFollow);

    return activity;
};

const processFederatedUndoActivity = function(activity) {

    let targetActivity = Activities.findOne({id: activity.object});

    if (targetActivity.type === "Follow"){

        const follower = Actors.findOne({id: targetActivity.actor});

        if (!follower)
            throw new Meteor.Error('Actor not found!', 'No actor with the given ID could be found: ' + targetActivity.object);

        const followee = Actors.findOne({id: targetActivity.actor});

        FollowingLists.update({id: follower.following}, {$inc: {totalItems: -1}, $pull: {orderedItems: targetActivity.actor}});

        if (followee.local)
            FollowerLists.update({id: followee.followers}, {$inc: {totalItems: -1}, $pull: {orderedItems: targetActivity.object}});

        PendingFollows.remove({follower: follower.id, followee: followee.id});

        return activity;
    }
};

const processFederatedActivity = function(activity) {

    if (activity.id && Activities.findOne({id: activity.id}))
        return;

    checkFederatedActivityPermitted(activity);

    switch(activity.type) {
        case 'Create':
            activity = processFederatedCreateActivity(activity);
            break;
        case 'Delete':
            activity = processFederatedDeleteActivity(activity);
            break;
        case "Accept":
            activity = processFederatedAcceptActivity(activity);
            break;
    }

    const _id = Activities.insert(activity);

    return Activities.findOne({_id: _id});
};

const importActorFromActivityPubJSON = function(json) {
    if (!Actors.findOne({id: json.id})) { //Is actor already present? If not,
        json.local = false; //mark it as foreign,
        Actors.insert(json); //then add it.
    }
};

const importPostFromActivityPubJSON = function(json) {
    if (!Posts.findOne({id: json.id})) { //Is post already present? If not,
        json.local = false; //mark it as foreign,
        Posts.insert(json); //then add it.
    }
};

Meteor.methods({
    getActivityJSONFromUrl: function(url) {
        return getActivityFromUrl(url)
        .then((response) => {
            if (response) {
                return response.json();
            } else throw new Meteor.Error('No response from url');
        })
        .then((json) => {
            if (json) Meteor.setTimeout(function(){ //This is to get this bit of code to run independently.
                Meteor.call('importFromActivityPubJSON', json);
            }, 0);

            return json;
        });
    },
    importFromActivityPubJSON: function(json) {
        if (!json) throw new Meteor.Error('Empty JSON');

        if (!json.type) throw new Meteor.Error('Untyped ActivityPub JSON');

        if (activityPubActorTypes.includes(json.type))
            importActorFromActivityPubJSON(json);

        else if (activityPubContentTypes.includes(json.type))
            importPostFromActivityPubJSON(json);
    },
    postActivity: function(object) {
        const user = Meteor.users.findOne({_id: this.userId});

        let result = processClientActivity(user, object);

        return result;
    }
});

const successfulJSON = function(data) {
    response = {
        statusCode: 200
    };

    if (data) {
        response.body = cleanActivityPub(data);
    }

    return response;
};

const failedJSON = function(message) {
    return {
        statusCode: 400,
        body: {status: "fail", message: message}
    };
};

Api = new Restivus({
    apiPath: '/',
    prettyJson: true
});

Api.addRoute('.well-known/host-meta', {}, {
    get: {
        action: function () {
            let response = successfulJSON();
            response.headers = {
                'Access-Control-Allow-Origin': '*',
                'Content-Type': 'application/xml; charset=UTF-8'
            };

            response.body = '<XRD>' +
                            '  <Link rel="lrdd"' +
                            '    type="application/xrd+xml"' +
                            '    template="' +
                            process.env.ROOT_URL + '/webfinger/xrd/{uri}" />' +
                            '</XRD>'

            return response;
        }
    }
});

Api.addRoute('.well-known/host-meta.json', {}, {
    get: {
        action: function () {
            let response = successfulJSON();
            response.headers = {
                'Access-Control-Allow-Origin': '*',
                'Content-Type': 'application/json; charset=UTF-8'
            };

            response.body = {
                links: [
                    {
                        "rel": "lrdd",
                        "type": "application/json",
                        "template": process.env.ROOT_URL + "/.well-known/webfinger?resource={uri}"
                    }
                ]
            }

            return response;
        }
    }
});

Api.addRoute('.well-known/webfinger', {}, {
    get: {
        action: function () {

            let resource = this.queryParams.resource;
            var re = /(acct:@?)(.*)(@.*)/;
            let handle = resource.replace(re, "$2");
            let actor = Actors.findOne({preferredUsername: handle, local: true});

            actor = cleanActivityPub(actor);

            let response = successfulJSON();

            response.headers = {
                'Content-Type': 'application/json; charset=UTF-8'
            };

            response.body = {
                subject: resource,
                aliases: [actor.url, actor.id],
                links: [
                    {
                        "rel":"http://webfinger.net/rel/profile-page",
                        "type":"text/html",
                        "href": actor.url
                    },
                    {
                        "rel":"self",
                        "type":"application/activity+json",
                        "href": actor.id
                    }
                ]
            }

            return response;
        }
    }
});

Api.addRoute('post/:_id', {}, {
    get: {
        action: function () {
            let post = Posts.findOne({_id: this.urlParams._id});
            if (post) {
                let response = successfulJSON(post);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get post!");
        }
    }
});

Api.addRoute('activity/:_id', {}, {
    get: {
        action: function () {
            let activity = Activities.findOne({_id: this.urlParams._id});
            if (activity) {
                let response = successfulJSON(activity);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get post!");
        }
    }
});

Api.addRoute('actors/:handle', {}, {
    get: {
        action: function () {
            let actor = Actors.findOne({preferredUsername: this.urlParams.handle, local: true});
            if (actor) {
                let response = successfulJSON(actor);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get actor!");
        }
    }
});

Api.addRoute('actors/:handle/inbox', {}, {
    post: {
        action: function () {
            let object = this.request.body;
            processFederatedActivity(object);
            return successfulJSON();
        }
    }
});

Api.addRoute('actors/:handle/outbox', {}, {
    get: {
        action: function () {
            var outbox = Outboxes.findOne({id: process.env.ROOT_URL + "actors/" + this.urlParams.handle + "/outbox"});
            if (outbox) {
                let orderedItems = [];
                for (let i = 0; i < outbox.totalItems && i < 10; i++) {
                    orderedItems.push(Activities.findOne({id: outbox.orderedItems[i]}));
                }
                outbox.orderedItems = orderedItems;
                let response = successfulJSON(outbox);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get outbox!");
        }
    },
    post: {
        action: function () {
            let object = this.request.body;

            try {
                return successfulJSON(processClientActivity(object));
            } catch (exception) {
                return failedJSON(exception.reason);
            }
        }
    }
});

Api.addRoute('actors/:handle/following', {}, {
    get: {
        action: function () {
            let following = FollowingLists.findOne({id: process.env.ROOT_URL + "actors/" + this.urlParams.handle + "/following"});
            if (following) {
                let response = successfulJSON(following);
                response.headers = {
                    'Content-Type': 'activity/json; charset=UTF-8'
                };
                return response;
            } else return failedJSON("Unable to get following list!");
        }
    }
});

Api.addRoute('actors/:handle/followers', {}, {
    get: {
        action: function () {
            let actor = Actors.findOne({preferredUsername: this.urlParams.handle});
            if (actor) {
                let followers = FollowerLists.findOne({id: actor.followers});
                if (followers) {
                    return successfulJSON(followers);
                }
            }

            return failedJSON("Unable to get followers list!");
        }
    }
});
<|endoftext|>


<commit_before>/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE.  AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////

'use strict'; // http://www.w3schools.com/js/js_strict.asp

// token handling in session
var Credentials = require('./../credentials');
// forge config information, such as client ID and secret
var config = require('./../config');

// web framework
var express = require('express');
var router = express.Router();
// forge oAuth package
var forgeSDK = require('forge-apis');

router.get('/api/forge/tree', function (req, res) {
  var token = new Credentials(req.session);
  var forge3legged = new forgeSDK.AuthClientThreeLegged(
    config.forge.credentials.client_id,
    config.forge.credentials.client_secret,
    config.forge.callbackURL,
    config.forge.scope,
    true);

  var href = decodeURIComponent(req.query.id);
  if (href === '') {
    res.status(500).end();
    return;
  }
  if (href === '#') {
    getHubs(forge3legged, token.getForgeCredentials(), res);
  }
  else {
    var params = href.split('/');
    var resourceName = params[params.length - 2];
    var resourceId = params[params.length - 1];
    switch (resourceName) {
      case 'hubs':
        getProjects(resourceId, forge3legged, token.getForgeCredentials(), res);
        break;
      case 'projects':
        // for a project, first we need the top/root folder
        var hubId = params[params.length - 3];
        getFolders(hubId, resourceId/*project_id*/, forge3legged, token.getForgeCredentials(), res)
        break;
      case 'folders':
        var projectId = params[params.length - 3];
        getFolderContents(projectId, resourceId/*folder_id*/, forge3legged, token.getForgeCredentials(), res);
        break;
      case 'items':
        var projectId = params[params.length - 3];
        getVersions(projectId, resourceId/*item_id*/, forge3legged, token.getForgeCredentials(), res);
        break;
    }
  }
});

function getHubs(oauthClient, credentials, res) {
  var hubs = new forgeSDK.HubsApi();
  hubs.getHubs({}, oauthClient, credentials)
    .then(function (data) {
      if (process.env.CONSOLELOG)
        if (data.body.meta.warnings)
          for (var key in data.body.meta.warnings) {
            var warning = data.body.meta.warnings[key];
            console.log(warning.HttpStatusCode + "/" + warning.ErrorCode + ":" + warning.Detail + ' > ' + warning.Title)
          }

      var hubsForTree = [];
      data.body.data.forEach(function (hub) {
        var hubType;

        switch (hub.attributes.extension.type) {
          case "hubs:autodesk.core:Hub":
            hubType = "hubs";
            break;
          case "hubs:autodesk.a360:PersonalHub":
            hubType = "personalHub";
            break;
          case "hubs:autodesk.bim360:Account":
            hubType = "bim360Hubs";
            break;
        }

        hubsForTree.push(prepareItemForTree(
          hub.links.self.href,
          hub.attributes.name,
          hubType,
          true
        ));
      });
      res.json(hubsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getProjects(hubId, oauthClient, credentials, res) {
  var projects = new forgeSDK.ProjectsApi();

  projects.getHubProjects(hubId, {}, oauthClient, credentials)
    .then(function (projects) {
      var projectsForTree = [];
      projects.body.data.forEach(function (project) {
        var projectType = 'projects';
        switch (project.attributes.extension.type) {
          case 'projects:autodesk.core:Project':
            projectType = 'a360projects';
            break;
          case 'projects:autodesk.bim360:Project':
            projectType = 'bim360projects';
            break;
        }

        projectsForTree.push(prepareItemForTree(
          project.links.self.href,
          project.attributes.name,
          projectType,
          true
        ));
      });
      res.json(projectsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getFolders(hubId, projectId, oauthClient, credentials, res) {
  var projects = new forgeSDK.ProjectsApi();
  projects.getProjectTopFolders(hubId, projectId, oauthClient, credentials)
    .then(function (topFolders) {
      var folderItemsForTree = [];
      topFolders.body.data.forEach(function (item) {
        folderItemsForTree.push(prepareItemForTree(
          item.links.self.href,
          item.attributes.displayName == null ? item.attributes.name : item.attributes.displayName,
          item.type,
          true
        ))
      });
      res.json(folderItemsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getFolderContents(projectId, folderId, oauthClient, credentials, res) {
  var folders = new forgeSDK.FoldersApi();
  folders.getFolderContents(projectId, folderId, {}, oauthClient, credentials)
    .then(function (folderContents) {
      var folderItemsForTree = [];
      folderContents.body.data.forEach(function (item) {

        var displayName = item.attributes.displayName == null ? item.attributes.name : item.attributes.displayName;
        if (displayName !== '') { // BIM 360 Items with no displayName also don't have storage, so not file to transfer
          folderItemsForTree.push(prepareItemForTree(
            item.links.self.href,
            displayName,
            item.type,
            true
          ));
        }
      });
      res.json(folderItemsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getVersions(projectId, itemId, oauthClient, credentials, res) {
  var items = new forgeSDK.ItemsApi();
  items.getItemVersions(projectId, itemId, {}, oauthClient, credentials)
    .then(function (versions) {
      var versionsForTree = [];
      var moment = require('moment');
      versions.body.data.forEach(function (version) {
        var lastModifiedTime = moment(version.attributes.lastModifiedTime);
        var days = moment().diff(lastModifiedTime, 'days')
        var dateFormated = (versions.body.data.length > 1 || days > 7 ? lastModifiedTime.format('MMM D, YYYY, h:mm a') : lastModifiedTime.fromNow());
        versionsForTree.push(prepareItemForTree(
          version.links.self.href,
          dateFormated + ' by ' + version.attributes.lastModifiedUserName,
          'versions',
          false
        ));
      });
      res.json(versionsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    })
}

function prepareItemForTree(_id, _text, _type, _children) {
  return {id: _id, text: _text, type: _type, children: _children};
}

module.exports = router;<commit_msg>decodeURI on versions<commit_after>/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Forge Partner Development
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE.  AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////

'use strict'; // http://www.w3schools.com/js/js_strict.asp

// token handling in session
var Credentials = require('./../credentials');
// forge config information, such as client ID and secret
var config = require('./../config');

// web framework
var express = require('express');
var router = express.Router();
// forge oAuth package
var forgeSDK = require('forge-apis');

router.get('/api/forge/tree', function (req, res) {
  var token = new Credentials(req.session);
  var forge3legged = new forgeSDK.AuthClientThreeLegged(
    config.forge.credentials.client_id,
    config.forge.credentials.client_secret,
    config.forge.callbackURL,
    config.forge.scope,
    true);

  var href = decodeURIComponent(req.query.id);
  if (href === '') {
    res.status(500).end();
    return;
  }
  if (href === '#') {
    getHubs(forge3legged, token.getForgeCredentials(), res);
  }
  else {
    var params = href.split('/');
    var resourceName = params[params.length - 2];
    var resourceId = params[params.length - 1];
    switch (resourceName) {
      case 'hubs':
        getProjects(resourceId, forge3legged, token.getForgeCredentials(), res);
        break;
      case 'projects':
        // for a project, first we need the top/root folder
        var hubId = params[params.length - 3];
        getFolders(hubId, resourceId/*project_id*/, forge3legged, token.getForgeCredentials(), res)
        break;
      case 'folders':
        var projectId = params[params.length - 3];
        getFolderContents(projectId, resourceId/*folder_id*/, forge3legged, token.getForgeCredentials(), res);
        break;
      case 'items':
        var projectId = params[params.length - 3];
        getVersions(projectId, resourceId/*item_id*/, forge3legged, token.getForgeCredentials(), res);
        break;
    }
  }
});

function getHubs(oauthClient, credentials, res) {
  var hubs = new forgeSDK.HubsApi();
  hubs.getHubs({}, oauthClient, credentials)
    .then(function (data) {
      if (process.env.CONSOLELOG)
        if (data.body.meta.warnings)
          for (var key in data.body.meta.warnings) {
            var warning = data.body.meta.warnings[key];
            console.log(warning.HttpStatusCode + "/" + warning.ErrorCode + ":" + warning.Detail + ' > ' + warning.Title)
          }

      var hubsForTree = [];
      data.body.data.forEach(function (hub) {
        var hubType;

        switch (hub.attributes.extension.type) {
          case "hubs:autodesk.core:Hub":
            hubType = "hubs";
            break;
          case "hubs:autodesk.a360:PersonalHub":
            hubType = "personalHub";
            break;
          case "hubs:autodesk.bim360:Account":
            hubType = "bim360Hubs";
            break;
        }

        hubsForTree.push(prepareItemForTree(
          hub.links.self.href,
          hub.attributes.name,
          hubType,
          true
        ));
      });
      res.json(hubsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getProjects(hubId, oauthClient, credentials, res) {
  var projects = new forgeSDK.ProjectsApi();

  projects.getHubProjects(hubId, {}, oauthClient, credentials)
    .then(function (projects) {
      var projectsForTree = [];
      projects.body.data.forEach(function (project) {
        var projectType = 'projects';
        switch (project.attributes.extension.type) {
          case 'projects:autodesk.core:Project':
            projectType = 'a360projects';
            break;
          case 'projects:autodesk.bim360:Project':
            projectType = 'bim360projects';
            break;
        }

        projectsForTree.push(prepareItemForTree(
          project.links.self.href,
          project.attributes.name,
          projectType,
          true
        ));
      });
      res.json(projectsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getFolders(hubId, projectId, oauthClient, credentials, res) {
  var projects = new forgeSDK.ProjectsApi();
  projects.getProjectTopFolders(hubId, projectId, oauthClient, credentials)
    .then(function (topFolders) {
      var folderItemsForTree = [];
      topFolders.body.data.forEach(function (item) {
        folderItemsForTree.push(prepareItemForTree(
          item.links.self.href,
          item.attributes.displayName == null ? item.attributes.name : item.attributes.displayName,
          item.type,
          true
        ))
      });
      res.json(folderItemsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getFolderContents(projectId, folderId, oauthClient, credentials, res) {
  var folders = new forgeSDK.FoldersApi();
  folders.getFolderContents(projectId, folderId, {}, oauthClient, credentials)
    .then(function (folderContents) {
      var folderItemsForTree = [];
      folderContents.body.data.forEach(function (item) {

        var displayName = item.attributes.displayName == null ? item.attributes.name : item.attributes.displayName;
        if (displayName !== '') { // BIM 360 Items with no displayName also don't have storage, so not file to transfer
          folderItemsForTree.push(prepareItemForTree(
            item.links.self.href,
            displayName,
            item.type,
            true
          ));
        }
      });
      res.json(folderItemsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    });
}

function getVersions(projectId, itemId, oauthClient, credentials, res) {
  var items = new forgeSDK.ItemsApi();
  items.getItemVersions(projectId, itemId, {}, oauthClient, credentials)
    .then(function (versions) {
      var versionsForTree = [];
      var moment = require('moment');
      versions.body.data.forEach(function (version) {
        var lastModifiedTime = moment(version.attributes.lastModifiedTime);
        var days = moment().diff(lastModifiedTime, 'days')
        var dateFormated = (versions.body.data.length > 1 || days > 7 ? lastModifiedTime.format('MMM D, YYYY, h:mm a') : lastModifiedTime.fromNow());
        var versionst =version.id.match (/^(.*)\?version=(\d+)$/) [2] ;
        versionsForTree.push(prepareItemForTree(
          version.links.self.href,
          decodeURI ('v' + versionst + ': ' + dateFormated + ' by ' + version.attributes.lastModifiedUserName),
          'versions',
          false
        ));
      });
      res.json(versionsForTree);
    })
    .catch(function (error) {
      console.log(error);
      res.status(500).end();
    })
}

function prepareItemForTree(_id, _text, _type, _children) {
  return {id: _id, text: _text, type: _type, children: _children};
}

module.exports = router;<|endoftext|>


<commit_before>const zmq = require('zmq');
const router = zmq.socket('router');
const logger = require('./log.js');
const config = require('./config.js');
const _ = require('underscore');
const EventEmitter = require('events');
const GameRepository = require('./repositories/gameRepository.js');

class GameRouter extends EventEmitter {
    constructor() {
        super();

        this.workers = {};
        this.gameRepository = new GameRepository();

        router.bind(config.mqUrl, err => {
            if(err) {
                logger.info(err);
            }
        });

        router.on('message', this.onMessage.bind(this));

        setInterval(this.checkTimeouts.bind(this), 100);
    }

    // External methods
    startGame(game) {
        var node = this.getNextAvailableGameNode();

        if(!node) {
            logger.error('Could not find new node for game');
            return;
        }

        this.gameRepository.create(game.getSaveState()).catch(err => {
            logger.error(err);
        });

        node.numGames++;

        this.sendCommand(node.identity, 'STARTGAME', game);
        return node;
    }

    addSpectator(game, username) {
        this.sendCommand(game.node.identity, 'SPECTATOR', { game: game, username: username });
    }

    getNextAvailableGameNode() {
        if(_.isEmpty(this.workers)) {
            return undefined;
        }

        var returnedWorker = undefined;

        _.each(this.workers, worker => {
            if(worker.numGames >= worker.maxGames || worker.disabled) {
                return;
            }

            if(!returnedWorker || returnedWorker.numGames > worker.numGames) {
                returnedWorker = worker;
            }
        });

        return returnedWorker;
    }

    getNodeStatus() {
        return _.map(this.workers, worker => {
            return { name: worker.identity, numGames: worker.numGames, status: worker.disabled ? 'disabled' : 'active' };
        });
    }

    disableNode(nodeName) {
        var worker = this.workers[nodeName];
        if(!worker) {
            return false;
        }

        worker.disabled = true;

        return true;
    }

    enableNode(nodeName) {
        var worker = this.workers[nodeName];
        if(!worker) {
            return false;
        }

        worker.disabled = false;

        return true;
    }

    // Events
    onMessage(identity, msg) {
        var identityStr = identity.toString();

        var worker = this.workers[identityStr];

        var message = JSON.parse(msg.toString());
        switch(message.command) {
            case 'HELLO':
                this.emit('onWorkerStarted', identityStr);
                this.workers[identityStr] = {
                    identity: identityStr,
                    maxGames: message.arg.maxGames,
                    numGames: 0,
                    address: message.arg.address,
                    port: message.arg.port,
                    protocol: message.arg.protocol
                };
                worker = this.workers[identityStr];
                break;
            case 'PONG':
                worker.pingSent = undefined;
                break;
            case 'GAMEWIN':
                this.gameRepository.update(message.arg);
                break;
            case 'GAMECLOSED':
                logger.info('game closed', message.arg.game);
                worker.numGames--;
                this.emit('onGameClosed', message.arg.game);

                break;
            case 'PLAYERLEFT':
                logger.info('player left', message.arg.gameId);

                if(!message.arg.spectator) {
                    this.gameRepository.update(message.arg.game);
                }

                this.emit('onPlayerLeft', message.arg.gameId, message.arg.player);

                break;
        }

        worker.lastMessage = Date.now();
    }

    // Internal methods
    sendCommand(identity, command, arg) {
        router.send([identity, '', JSON.stringify({ command: command, arg: arg })]);
    }

    checkTimeouts() {
        var currentTime = Date.now();

        _.each(this.workers, worker => {
            if(worker.pingSent && currentTime - worker.pingSent > 5 * 60 * 1000) {
                logger.info('worker', worker.identity + ' timed out');
                delete this.workers[worker.identity];
                this.emit('onWorkerTimedOut', worker.identity);
            } else if(!worker.pingSent) {
                if(currentTime - worker.lastMessage > 5 * 60 * 1000) {
                    worker.pingSent = currentTime;
                    this.sendCommand(worker.identity, 'PING');
                }
            }
        });
    }
}

module.exports = GameRouter;
<commit_msg>add error handling to repository calls<commit_after>const zmq = require('zmq');
const router = zmq.socket('router');
const logger = require('./log.js');
const config = require('./config.js');
const _ = require('underscore');
const EventEmitter = require('events');
const GameRepository = require('./repositories/gameRepository.js');

class GameRouter extends EventEmitter {
    constructor() {
        super();

        this.workers = {};
        this.gameRepository = new GameRepository();

        router.bind(config.mqUrl, err => {
            if(err) {
                logger.info(err);
            }
        });

        router.on('message', this.onMessage.bind(this));

        setInterval(this.checkTimeouts.bind(this), 100);
    }

    // External methods
    startGame(game) {
        var node = this.getNextAvailableGameNode();

        if(!node) {
            logger.error('Could not find new node for game');
            return;
        }

        this.gameRepository.create(game.getSaveState()).catch(err => {
            logger.error(err);
        });

        node.numGames++;

        this.sendCommand(node.identity, 'STARTGAME', game);
        return node;
    }

    addSpectator(game, username) {
        this.sendCommand(game.node.identity, 'SPECTATOR', { game: game, username: username });
    }

    getNextAvailableGameNode() {
        if(_.isEmpty(this.workers)) {
            return undefined;
        }

        var returnedWorker = undefined;

        _.each(this.workers, worker => {
            if(worker.numGames >= worker.maxGames || worker.disabled) {
                return;
            }

            if(!returnedWorker || returnedWorker.numGames > worker.numGames) {
                returnedWorker = worker;
            }
        });

        return returnedWorker;
    }

    getNodeStatus() {
        return _.map(this.workers, worker => {
            return { name: worker.identity, numGames: worker.numGames, status: worker.disabled ? 'disabled' : 'active' };
        });
    }

    disableNode(nodeName) {
        var worker = this.workers[nodeName];
        if(!worker) {
            return false;
        }

        worker.disabled = true;

        return true;
    }

    enableNode(nodeName) {
        var worker = this.workers[nodeName];
        if(!worker) {
            return false;
        }

        worker.disabled = false;

        return true;
    }

    // Events
    onMessage(identity, msg) {
        var identityStr = identity.toString();

        var worker = this.workers[identityStr];

        var message = JSON.parse(msg.toString());
        switch(message.command) {
            case 'HELLO':
                this.emit('onWorkerStarted', identityStr);
                this.workers[identityStr] = {
                    identity: identityStr,
                    maxGames: message.arg.maxGames,
                    numGames: 0,
                    address: message.arg.address,
                    port: message.arg.port,
                    protocol: message.arg.protocol
                };
                worker = this.workers[identityStr];
                break;
            case 'PONG':
                worker.pingSent = undefined;
                break;
            case 'GAMEWIN':
                this.gameRepository.update(message.arg).catch(err => {
                    logger.error(err);
                });
                break;
            case 'GAMECLOSED':
                logger.info('game closed', message.arg.game);
                worker.numGames--;
                this.emit('onGameClosed', message.arg.game);

                break;
            case 'PLAYERLEFT':
                logger.info('player left', message.arg.gameId);

                if(!message.arg.spectator) {
                    this.gameRepository.update(message.arg.game).catch(err => {
                        logger.error(err);
                    }
                }

                this.emit('onPlayerLeft', message.arg.gameId, message.arg.player);

                break;
        }

        worker.lastMessage = Date.now();
    }

    // Internal methods
    sendCommand(identity, command, arg) {
        router.send([identity, '', JSON.stringify({ command: command, arg: arg })]);
    }

    checkTimeouts() {
        var currentTime = Date.now();

        _.each(this.workers, worker => {
            if(worker.pingSent && currentTime - worker.pingSent > 5 * 60 * 1000) {
                logger.info('worker', worker.identity + ' timed out');
                delete this.workers[worker.identity];
                this.emit('onWorkerTimedOut', worker.identity);
            } else if(!worker.pingSent) {
                if(currentTime - worker.lastMessage > 5 * 60 * 1000) {
                    worker.pingSent = currentTime;
                    this.sendCommand(worker.identity, 'PING');
                }
            }
        });
    }
}

module.exports = GameRouter;
<|endoftext|>


<commit_before>/**
 * Functions related to search
 */

import config from 'config';
import { sortBy, get } from 'lodash';

import { CollectiveTypesList } from '../constants/collectives';
import { RateLimitExceeded } from '../graphql/errors';
import models, { Op, sequelize } from '../models';
import algolia from './algolia';
import cache from './cache';

// Returned when there's no result for a search
const EMPTY_SEARCH_RESULT = [[], 0];

/**
 * Search users by email address. `user` must be set because this endpoint is rate
 * limited to prevent abuse.
 *
 * @param {String} email - a valid email address
 * @param {Object} user - the user triggering the search
 */
export const searchCollectivesByEmail = async (email, user, offset = 0, limit = 10) => {
  if (!email || !user) {
    return EMPTY_SEARCH_RESULT;
  }

  // Put some rate limiting to users can't use this endpoint to bruteforce emails
  const maxChangePerHour = config.limits.searchEmailPerHour;
  const countCacheKey = `user_email_search_${user.id}`;
  const existingCount = (await cache.get(countCacheKey)) || 0;
  if (existingCount > maxChangePerHour) {
    throw new RateLimitExceeded();
  } else {
    // Update the cache
    const oneHourInSeconds = 60 * 60;
    cache.set(countCacheKey, existingCount + 1, oneHourInSeconds);
  }

  // Emails are uniques, thus there should never be more than one result - this is
  // why it's safe to use `collectives.length` in the return.
  const collectives = await sequelize.query(
    `
    SELECT  c.*, COUNT(*) OVER() AS __total__
    FROM "Collectives" c
    INNER JOIN "Users" u ON u."CollectiveId" = c.id
    WHERE c."isIncognito" = FALSE AND c.type = 'USER' AND u.email = :email
    OFFSET :offset
    LIMIT :limit
    `,
    {
      model: models.Collective,
      mapToModel: true,
      replacements: { offset, limit, email },
    },
  );

  return [collectives, get(collectives[0], 'dataValues.__total__', 0)];
};

/**
 * Turn a search string into a TS vector using 'OR' operator.
 *
 * Ex: "open potatoes" => "open|potatoes"
 */
const searchTermToTsVector = term => {
  return term
    .trim()
    .split(/\s+/g)
    .join('|');
};

/**
 * Search collectives directly in the DB, using a full-text query.
 */
export const searchCollectivesInDB = async (term, offset = 0, limit = 100, types, hostCollectiveIds) => {
  // TSVector to search for collectives names/description/slug
  const tsVector = `
    to_tsvector('simple', c.name)
    || to_tsvector('simple', c.slug)
    || to_tsvector('simple', COALESCE(c.description, ''))
  `;

  // Build dynamic conditions based on arguments
  let dynamicConditions = '';

  if (hostCollectiveIds && hostCollectiveIds.length > 0) {
    dynamicConditions += 'AND "HostCollectiveId" IN (:hostCollectiveIds) ';
  }

  if (term && term.length > 0) {
    term = term.replace(/(_|%|\\)/g, ' ');
    dynamicConditions += `AND (${tsVector} @@ to_tsquery('simple', :term) OR name ILIKE '%' || :term || '%') `;
  }

  // Build the query
  const result = await sequelize.query(
    `
    SELECT
      c.*,
      COUNT(*) OVER() AS __total__,
      ts_rank(${tsVector}, to_tsquery('simple', :term)) AS __rank__
    FROM "Collectives" c
    WHERE "deletedAt" IS NULL
    AND "deactivatedAt" IS NULL
    AND "isActive" = true
    AND type IN (:types) ${dynamicConditions}
    ORDER BY __rank__ DESC
    OFFSET :offset
    LIMIT :limit
    `,
    {
      model: models.Collective,
      mapToModel: true,
      replacements: {
        types: types || CollectiveTypesList,
        term: searchTermToTsVector(term),
        offset,
        limit,
        hostCollectiveIds,
      },
    },
  );

  return [result, get(result[0], 'dataValues.__total__', 0)];
};

/**
 * Search for collectives using Algolia.
 *
 * @returns a tuple like [collectives, total]
 */
export const searchCollectivesOnAlgolia = async (term, offset, limit, types) => {
  const index = algolia.getIndex();
  if (!index) {
    return EMPTY_SEARCH_RESULT;
  }

  const { hits, nbHits: total } = await index.search({
    query: term,
    length: limit,
    offset,
  });

  const collectiveIds = hits.map(({ id }) => id);

  if (collectiveIds.length === 0) {
    return EMPTY_SEARCH_RESULT;
  }

  // Build and run SQL query
  const where = { id: { [Op.in]: collectiveIds } };
  if (types !== undefined) {
    where.type = { [Op.in]: types };
  }

  const collectives = await models.Collective.findAll({ where });
  const sortedCollectives = sortBy(collectives, collective => collectiveIds.indexOf(collective.id));
  return [sortedCollectives, total];
};
<commit_msg>chore(search): Add partial search for slug<commit_after>/**
 * Functions related to search
 */

import config from 'config';
import { sortBy, get } from 'lodash';

import { CollectiveTypesList } from '../constants/collectives';
import { RateLimitExceeded } from '../graphql/errors';
import models, { Op, sequelize } from '../models';
import algolia from './algolia';
import cache from './cache';

// Returned when there's no result for a search
const EMPTY_SEARCH_RESULT = [[], 0];

/**
 * Search users by email address. `user` must be set because this endpoint is rate
 * limited to prevent abuse.
 *
 * @param {String} email - a valid email address
 * @param {Object} user - the user triggering the search
 */
export const searchCollectivesByEmail = async (email, user, offset = 0, limit = 10) => {
  if (!email || !user) {
    return EMPTY_SEARCH_RESULT;
  }

  // Put some rate limiting to users can't use this endpoint to bruteforce emails
  const maxChangePerHour = config.limits.searchEmailPerHour;
  const countCacheKey = `user_email_search_${user.id}`;
  const existingCount = (await cache.get(countCacheKey)) || 0;
  if (existingCount > maxChangePerHour) {
    throw new RateLimitExceeded();
  } else {
    // Update the cache
    const oneHourInSeconds = 60 * 60;
    cache.set(countCacheKey, existingCount + 1, oneHourInSeconds);
  }

  // Emails are uniques, thus there should never be more than one result - this is
  // why it's safe to use `collectives.length` in the return.
  const collectives = await sequelize.query(
    `
    SELECT  c.*, COUNT(*) OVER() AS __total__
    FROM "Collectives" c
    INNER JOIN "Users" u ON u."CollectiveId" = c.id
    WHERE c."isIncognito" = FALSE AND c.type = 'USER' AND u.email = :email
    OFFSET :offset
    LIMIT :limit
    `,
    {
      model: models.Collective,
      mapToModel: true,
      replacements: { offset, limit, email },
    },
  );

  return [collectives, get(collectives[0], 'dataValues.__total__', 0)];
};

/**
 * Turn a search string into a TS vector using 'OR' operator.
 *
 * Ex: "open potatoes" => "open|potatoes"
 */
const searchTermToTsVector = term => {
  return term
    .trim()
    .split(/\s+/g)
    .join('|');
};

/**
 * Search collectives directly in the DB, using a full-text query.
 */
export const searchCollectivesInDB = async (term, offset = 0, limit = 100, types, hostCollectiveIds) => {
  // TSVector to search for collectives names/description/slug
  const tsVector = `
    to_tsvector('simple', c.name)
    || to_tsvector('simple', c.slug)
    || to_tsvector('simple', COALESCE(c.description, ''))
  `;

  // Build dynamic conditions based on arguments
  let dynamicConditions = '';

  if (hostCollectiveIds && hostCollectiveIds.length > 0) {
    dynamicConditions += 'AND "HostCollectiveId" IN (:hostCollectiveIds) ';
  }

  if (term && term.length > 0) {
    term = term.replace(/(_|%|\\)/g, ' ');
    dynamicConditions += `AND (${tsVector} @@ to_tsquery('simple', :term) OR name ILIKE '%' || :term || '%') OR slug ILIKE '%' || :term || '%') `;
  }

  // Build the query
  const result = await sequelize.query(
    `
    SELECT
      c.*,
      COUNT(*) OVER() AS __total__,
      ts_rank(${tsVector}, to_tsquery('simple', :term)) AS __rank__
    FROM "Collectives" c
    WHERE "deletedAt" IS NULL
    AND "deactivatedAt" IS NULL
    AND "isActive" = true
    AND type IN (:types) ${dynamicConditions}
    ORDER BY __rank__ DESC
    OFFSET :offset
    LIMIT :limit
    `,
    {
      model: models.Collective,
      mapToModel: true,
      replacements: {
        types: types || CollectiveTypesList,
        term: searchTermToTsVector(term),
        offset,
        limit,
        hostCollectiveIds,
      },
    },
  );

  return [result, get(result[0], 'dataValues.__total__', 0)];
};

/**
 * Search for collectives using Algolia.
 *
 * @returns a tuple like [collectives, total]
 */
export const searchCollectivesOnAlgolia = async (term, offset, limit, types) => {
  const index = algolia.getIndex();
  if (!index) {
    return EMPTY_SEARCH_RESULT;
  }

  const { hits, nbHits: total } = await index.search({
    query: term,
    length: limit,
    offset,
  });

  const collectiveIds = hits.map(({ id }) => id);

  if (collectiveIds.length === 0) {
    return EMPTY_SEARCH_RESULT;
  }

  // Build and run SQL query
  const where = { id: { [Op.in]: collectiveIds } };
  if (types !== undefined) {
    where.type = { [Op.in]: types };
  }

  const collectives = await models.Collective.findAll({ where });
  const sortedCollectives = sortBy(collectives, collective => collectiveIds.indexOf(collective.id));
  return [sortedCollectives, total];
};
<|endoftext|>


<commit_before>/*
 * One Controller per layout view
 */

const _ = require('lodash');
const express = require('express');
const router = express.Router();
const authHelper = require('../helpers/authentication');
const api = require('../api');
const moment = require('moment');
moment.locale('de');
const recurringEventsHelper = require('../helpers/recurringEvents');

// secure routes
router.use(authHelper.authChecker);

router.get('/', function (req, res, next) {
    // we display time from 8 a.m. to 8 p.m.
    const timeStart = 8;
    const timeEnd = 18;
    const numHours = timeEnd - timeStart;
    const numMinutes = numHours * 60;
    const hours = [];

    for(let j = 0; j < numHours; j++) {
        hours.push(j + timeStart);
    }
    const start = new Date();
    start.setHours(timeStart,0,0,0);
    const end = new Date();
    end.setHours(timeEnd,0,0,0);

    const currentTime = new Date();
    let currentTimePercentage = 100 * (((currentTime.getHours() - timeStart) * 60) + currentTime.getMinutes()) / numMinutes;
    if(currentTimePercentage < 0) currentTimePercentage = 0;
    else if(currentTimePercentage > 100) currentTimePercentage = 100;

    const eventsPromise = api(req).get('/calendar/', {
        qs: {
            all: true,
            until: end.toLocalISOString()
        }
    }).then(events => {
        // because the calender service is *§$" and is not
        // returning recurring events for a given time period
        // now we have to load all events from the beginning of time
        // until end of the current day, map recurring events and
        // display only the correct ones.
        // I'm not happy with the solution but don't see any other less
        // crappy way for this without changing the
        // calendar service in it's core.

        return Promise.all(events.map(event => recurringEventsHelper.mapEventProps(event, req))).then(events => {
            events = [].concat.apply([], events.map(recurringEventsHelper.mapRecurringEvent)).filter(event => {
                const eventStart = new Date(event.start);
                const eventEnd = new Date(event.end);

                return eventStart < end && eventEnd > start;
            });

            return (events || []).map(event => {
                const eventStart = new Date(event.start);
                let eventEnd = new Date(event.end);

                // cur events that are too long
                if(eventEnd > end) {
                    eventEnd = end;
                    event.end = eventEnd.toLocalISOString();
                }

                // subtract timeStart so we can use these values for left alignment
                const eventStartRelativeMinutes = ((eventStart.getUTCHours() - timeStart) * 60) + eventStart.getMinutes();
                const eventEndRelativeMinutes = ((eventEnd.getUTCHours() - timeStart) * 60) + eventEnd.getMinutes();
                const eventDuration = eventEndRelativeMinutes - eventStartRelativeMinutes;

                event.comment = moment.utc(eventStart).format('kk:mm') + ' - ' + moment.utc(eventEnd).format('kk:mm');
                event.style = {
                    left: 100 * (eventStartRelativeMinutes / numMinutes),  // percent
                    width: 100 * (eventDuration / numMinutes)  // percent
                };

                return event;
            });
        });
    }).catch(_ => []);

    const homeworksPromise = api(req).get('/homework/', {
        qs: {
            $populate: ['courseId'],
            $sort: 'dueDate',
            archived : {$ne: res.locals.currentUser._id },
            'dueDate': {
                $gte: new Date().getTime(),
                $lte: new Date(new Date().setFullYear(new Date().getFullYear() + 1))
            }
        }
    }).then(data => data.data.map(homeworks => {
        homeworks.menuInfo = moment(homeworks.dueDate).fromNow();
        if (homeworks.courseId != null) {
            homeworks.title = '['+homeworks.courseId.name+'] ' + homeworks.name;
            homeworks.background = homeworks.courseId.color;
        } else {
            homeworks.title = homeworks.name;
            homeworks.private = true;
        }
        homeworks.url = '/homework/' + homeworks._id;
        homeworks.content = homeworks.description;
        return homeworks;
    }));

    function sortFunction(a, b) {
        if (a.displayAt === b.displayAt) {
            return 0;
        }
        else {
            return (a.displayAt < b.displayAt) ? 1 : -1;
        }
    }
    //Somehow $lte doesn't work in normal query so I manually put it into a request
    const colors = ["CDDC39","3F51B5","FF9800","00BCD4","FF5722","03A9F4","2196F3","F44336","FFC107","009688","E91E63","4CAF50"];
    const newsPromise = api(req).get('/news/',{
        qs: {
            schoolId : res.locals.currentSchool,
            'displayAt': {
                $lte: new Date().getTime()
            }
        }
    }).then(news => news.data.map(news => {
            news.url = '/news/' + news._id;
            news.menuInfo = moment(news.displayAt).fromNow();
            // ToDo: insert real Header Image from News
            news.background = '#'+colors[(news.title||"").length % colors.length];
            return news;
    }).sort(sortFunction).slice(0,3));

    Promise.all([
        eventsPromise,
        homeworksPromise,
        newsPromise
    ]).then(([events, homeworks, news]) => {

        homeworks.sort((a,b) => {
            if(a.dueDate > b.dueDate) {
                return 1;
            } else {
                return -1;
            }
        });

        res.render('dashboard/dashboard', {
            title: 'Übersicht',
            events,
            eventsDate: moment().format('dddd, DD. MMMM YYYY'),
            homeworks: homeworks.filter(function(task){return !task.private;}).slice(0, 4),
            myhomeworks: homeworks.filter(function(task){return task.private;}).slice(0, 4),
            news,
            hours,
            currentTimePercentage,
            currentTime: moment(currentTime).format('kk:mm')
        });
    });
});

module.exports = router;
<commit_msg>dezentere Datumsanzeige im Dashboard<commit_after>/*
 * One Controller per layout view
 */

const _ = require('lodash');
const express = require('express');
const router = express.Router();
const authHelper = require('../helpers/authentication');
const api = require('../api');
const moment = require('moment');
moment.locale('de');
const recurringEventsHelper = require('../helpers/recurringEvents');

// secure routes
router.use(authHelper.authChecker);

router.get('/', function (req, res, next) {
    // we display time from 8 a.m. to 8 p.m.
    const timeStart = 8;
    const timeEnd = 18;
    const numHours = timeEnd - timeStart;
    const numMinutes = numHours * 60;
    const hours = [];

    for(let j = 0; j < numHours; j++) {
        hours.push(j + timeStart);
    }
    const start = new Date();
    start.setHours(timeStart,0,0,0);
    const end = new Date();
    end.setHours(timeEnd,0,0,0);

    const currentTime = new Date();
    let currentTimePercentage = 100 * (((currentTime.getHours() - timeStart) * 60) + currentTime.getMinutes()) / numMinutes;
    if(currentTimePercentage < 0) currentTimePercentage = 0;
    else if(currentTimePercentage > 100) currentTimePercentage = 100;

    const eventsPromise = api(req).get('/calendar/', {
        qs: {
            all: true,
            until: end.toLocalISOString()
        }
    }).then(events => {
        // because the calender service is *§$" and is not
        // returning recurring events for a given time period
        // now we have to load all events from the beginning of time
        // until end of the current day, map recurring events and
        // display only the correct ones.
        // I'm not happy with the solution but don't see any other less
        // crappy way for this without changing the
        // calendar service in it's core.

        return Promise.all(events.map(event => recurringEventsHelper.mapEventProps(event, req))).then(events => {
            events = [].concat.apply([], events.map(recurringEventsHelper.mapRecurringEvent)).filter(event => {
                const eventStart = new Date(event.start);
                const eventEnd = new Date(event.end);

                return eventStart < end && eventEnd > start;
            });

            return (events || []).map(event => {
                const eventStart = new Date(event.start);
                let eventEnd = new Date(event.end);

                // cur events that are too long
                if(eventEnd > end) {
                    eventEnd = end;
                    event.end = eventEnd.toLocalISOString();
                }

                // subtract timeStart so we can use these values for left alignment
                const eventStartRelativeMinutes = ((eventStart.getUTCHours() - timeStart) * 60) + eventStart.getMinutes();
                const eventEndRelativeMinutes = ((eventEnd.getUTCHours() - timeStart) * 60) + eventEnd.getMinutes();
                const eventDuration = eventEndRelativeMinutes - eventStartRelativeMinutes;

                event.comment = moment.utc(eventStart).format('kk:mm') + ' - ' + moment.utc(eventEnd).format('kk:mm');
                event.style = {
                    left: 100 * (eventStartRelativeMinutes / numMinutes),  // percent
                    width: 100 * (eventDuration / numMinutes)  // percent
                };

                return event;
            });
        });
    }).catch(_ => []);

    const homeworksPromise = api(req).get('/homework/', {
        qs: {
            $populate: ['courseId'],
            $sort: 'dueDate',
            archived : {$ne: res.locals.currentUser._id },
            'dueDate': {
                $gte: new Date().getTime(),
                $lte: new Date(new Date().setFullYear(new Date().getFullYear() + 1))
            }
        }
    }).then(data => data.data.map(homeworks => {
        homeworks.secondaryTitle = moment(homeworks.dueDate).fromNow();
        if (homeworks.courseId != null) {
            homeworks.title = '['+homeworks.courseId.name+'] ' + homeworks.name;
            homeworks.background = homeworks.courseId.color;
        } else {
            homeworks.title = homeworks.name;
            homeworks.private = true;
        }
        homeworks.url = '/homework/' + homeworks._id;
        homeworks.content = homeworks.description;
        return homeworks;
    }));

    function sortFunction(a, b) {
        if (a.displayAt === b.displayAt) {
            return 0;
        }
        else {
            return (a.displayAt < b.displayAt) ? 1 : -1;
        }
    }
    //Somehow $lte doesn't work in normal query so I manually put it into a request
    const colors = ["CDDC39","3F51B5","FF9800","00BCD4","FF5722","03A9F4","2196F3","F44336","FFC107","009688","E91E63","4CAF50"];
    const newsPromise = api(req).get('/news/',{
        qs: {
            schoolId : res.locals.currentSchool,
            'displayAt': {
                $lte: new Date().getTime()
            }
        }
    }).then(news => news.data.map(news => {
            news.url = '/news/' + news._id;
            news.secondaryTitle = moment(news.displayAt).fromNow();
            // ToDo: insert real Header Image from News
            news.background = '#'+colors[(news.title||"").length % colors.length];
            return news;
    }).sort(sortFunction).slice(0,3));

    Promise.all([
        eventsPromise,
        homeworksPromise,
        newsPromise
    ]).then(([events, homeworks, news]) => {

        homeworks.sort((a,b) => {
            if(a.dueDate > b.dueDate) {
                return 1;
            } else {
                return -1;
            }
        });

        res.render('dashboard/dashboard', {
            title: 'Übersicht',
            events,
            eventsDate: moment().format('dddd, DD. MMMM YYYY'),
            homeworks: homeworks.filter(function(task){return !task.private;}).slice(0, 4),
            myhomeworks: homeworks.filter(function(task){return task.private;}).slice(0, 4),
            news,
            hours,
            currentTimePercentage,
            currentTime: moment(currentTime).format('kk:mm')
        });
    });
});

module.exports = router;
<|endoftext|>


<commit_before>
// Playbill Express Server

var express = require('express'),
    marked = require('marked'),
    _ = require('underscore'),
    fs = require('graceful-fs'),
    path = require('path'),
    when = require('when'),
    apply = require('when/apply'),
    nodefn = require('when/node/function'),
    yaml = require('js-yaml'),
    arrayQuery = require('array-query'),
    truncate = require('html-truncate'),
    Post = require('./post'),
    PostIndex = require('./post-index');

function Playbill(options) {
    this.options = options = options || {};
    this.filePath = options.filePath || path.join(process.cwd(), 'posts');
    this.ext = options.ext || '.md';
    this.extRegex = new RegExp('\\'+this.ext+'$', 'i');

    this.perPage = options.perPage || 10;

    this.defaultPostView = options.defaultPostView || 'post';
    this.defaultWrapperView = options.defaultWrapperView || 'wrapper';
    this.defaultListView = options.defaultListView || 'list';

    this.markedOptions = _.extend({}, {
        breaks: true,
        smartLists: true
    }, options.markedOptions);

    this.client = options.client && typeof options.client === 'string' ?
        options.client :
        true;

    this.index = new PostIndex(_.extend({}, options, { playbill: this }));

    this.app = options.app || express();

    if (options.views) this.app.set('views', options.views);
    if (options.viewEngine) this.app.set('view engine', options.viewEngine);

    this.app.locals({
        truncate: truncate,

        // listView is false unless overridden later.
        listView: false,
        // prevent errors by defining these,
        // they will be overridden later.
        post: {},
        posts: [],
        total: 0,
        pages: 0,
        currentPage: 0
    });

    this.init(this.app);

    return this;
}

Playbill.prototype.init = function init(app) {
    var playbill = this;

    app.disable('x-powered-by');
    app.use(express.bodyParser());
    app.use(app.router);

    var getPostsRoute = _getPosts.call(playbill, app);
    app.get('/', getPostsRoute);
    app.get('/.:type(json)', getPostsRoute);
    app.get('/p:page([1-9][0-9]{0,})/', getPostsRoute);
    app.get('/p:page([1-9][0-9]{0,})/.:type(json)', getPostsRoute);

    var getPostRoute = _getPost.call(playbill, app);
    app.get('/:slug([\\w-]+)', getPostRoute);
    app.get('/:slug([\\w-]+).:type(json)', getPostRoute);

    if (playbill.client) {
        var clientPath = typeof playbill.client === 'string' ? playbill.client : '/playbill.js';
        app.get(clientPath, function (req, res) {
            res.sendfile('client.js', { root: path.join(__dirname, '../../client/lib') });
        });
    }
};

Playbill.prototype.listPosts = function listPosts(options) {
    var playbill = this,
        limit,
        offset,
        total;

    options = options || {};

    return when(options)
    .then(function (options) {
        options.page = options.page || 1;
        options.perPage = options.perPage || playbill.perPage;

        limit = options.perPage;
        offset = options.perPage * (options.page - 1);
    })
    .then(function () {
        return playbill.index.init();
    })
    .then(function (index) {
        return index.posts;
    })
    .then(function (posts) {
        total = posts.length;

        // Apply query options.
        posts = arrayQuery('published').is(true)
        .sort('created').date().desc()
        .sort('title')
        .on(posts);

        // Replace broken array-query .offset() implementation.
        posts = posts.slice(offset, limit ? offset+limit : undefined);

        return posts;
    })
    .then(function (unsafePosts) {
        // Sanitize parsed posts.
        return when.all(_.map(unsafePosts, function (unsafePost) {
            return playbill._sanitizePost(unsafePost);
        }));
    })
    .then(function (posts) {
        return {
            total: total,
            posts: posts
        };
    });
};

function _getPosts(app) {
    var playbill = this;

    return function getPosts(req, res, next) {
        var type = req.param('type') || 'html',
            page = parseInt(req.param('page'), 10) || 1;

        playbill.listPosts({
            page: page
        })
        .then(function (data) {
            var posts = data.posts;

            if (!posts.length) {
                var err = new Error('No posts found.');
                err.status = 404;
                return next(err);
            }

            res.locals({
                listView: true,
                total: data.total,
                pages: Math.round(data.total/playbill.perPage),
                currentPage: page
            });

            switch (type) {
                case "json":
                    var _render = nodefn.lift(res.render.bind(res));

                    when.map(posts, function (post) {
                        return _render(post.meta.view || playbill.defaultPostView, { post: post })
                        .then(function (html) {
                            post.rendered = html;

                            return post;
                        });
                    })
                    .then(function (posts) {
                        return res.send(200, { posts: posts, total: res.locals.total, pages: res.locals.pages, currentPage: page });
                    })
                    .otherwise(next);
                    break;
                default:
                    res.render(playbill.defaultWrapperView, { posts: posts });
                    break;
            }
        })
        .otherwise(function (err) {
            next(err);
        });
    };
}

Playbill.prototype._path = function _path(slug) {
    var playbill = this;
    return path.join(playbill.filePath, slug + playbill.ext);
};

var promiseReadFile = nodefn.lift(fs.readFile);

Playbill.prototype._loadPost = function _loadPost(slug) {
    var playbill = this,
        path = playbill._path(slug);
    return promiseReadFile(path, 'utf8')
    .then(function (string) {
        return {
            slug: slug,
            path: path,
            raw: string
        };
    });
};

var promiseMarked = nodefn.lift(marked);

Playbill.prototype._parsePost = function _parsePost(rawPost) {
    var playbill = this;

    return when(rawPost)
    .then(function (rawPost) {
        return playbill._parseMeta(rawPost);
    })
    .then(function (post) {
        return promiseMarked(post.raw, playbill.markedOptions)
        .then(function (postHTML) {
            post.content = postHTML;
            return post;
        });
    });
};

Playbill.metaRegex = /^(---[\s\S]+)---/;

var promiseStat = nodefn.lift(fs.stat);

Playbill.prototype._parseMeta = function _parseMeta(post) {
    var playbill = this;

    return when(post)
    .then(function (post) {
        post.meta = {};

        var metaMatch = post.raw.match(Playbill.metaRegex);
        if (metaMatch && metaMatch.length > 1) {
            post.meta = metaMatch[1];

            try{
                post.meta = yaml.safeLoad(post.meta);
            } catch(e) {
                console.error(e);
                post.meta = {};
            }

            post.raw = post.raw.replace(Playbill.metaRegex, '');
        }

        return post;
    })
    .then(function (post) {
        if (post.meta.created) {
            post.meta.created = new Date(post.meta.created);
        }
        return post;
    })
    .then(function (post) {
        if (!post.meta.created && post.path) {
            return promiseStat(post.path)
            .then(function (stat) {
                post.meta.created = stat.ctime;
                return post;
            });
        }
        return post;
    })
    .then(function (post) {
        _.each(Post.liftMeta, function (key) {
            post[key] = post.meta[key];
        });
        return post;
    });
};

Playbill.prototype._sanitizePost = function _sanitizePost(unsafePost) {
    return (unsafePost.toJSON ? unsafePost : new Post(unsafePost)).toJSON();
};

Playbill.prototype.fetchPost = function fetchPost(slug) {
    var playbill = this;

    return playbill._loadPost(slug)
    .then(function (rawPost) {
        return playbill._parsePost(rawPost);
    })
    .then(function (unsafePost) {
        return playbill._sanitizePost(unsafePost);
    });
};

function _getPost(app) {
    var playbill = this;

    return function getPost(req, res, next) {
        var slug = req.param('slug'),
            type = req.param('type');

        playbill.index.init()
        .then(function (index) {
            return index.get(slug);
        })
        .then(function (post) {
            if (!post) {
                var err = new Error('Post not found.');
                err.status = 404;
                throw err;
            }
            return post;
        })
        .then(function (post) {
            switch (type) {
                case "json":
                    nodefn.call(res.render.bind(res), post.meta.view || playbill.defaultPostView, { post: post })
                    .then(function (html) {
                        post.rendered = html;

                        return res.send(200, post);
                    })
                    .otherwise(next);
                    break;
                default:
                    res.render(post.meta.wrapper ? post.meta.wrapper : playbill.defaultWrapperView, { post: post });
                    break;
            }
        })
        .otherwise(function (err) {
            if (err.code === 'ENOENT') {
                err.status = 404;
            }
            return next(err);
        });
    };
}

module.exports = Playbill;
<commit_msg>Fix bug in post rendering<commit_after>
// Playbill Express Server

var express = require('express'),
    marked = require('marked'),
    _ = require('underscore'),
    fs = require('graceful-fs'),
    path = require('path'),
    when = require('when'),
    apply = require('when/apply'),
    nodefn = require('when/node/function'),
    yaml = require('js-yaml'),
    arrayQuery = require('array-query'),
    truncate = require('html-truncate'),
    Post = require('./post'),
    PostIndex = require('./post-index');

function Playbill(options) {
    this.options = options = options || {};
    this.filePath = options.filePath || path.join(process.cwd(), 'posts');
    this.ext = options.ext || '.md';
    this.extRegex = new RegExp('\\'+this.ext+'$', 'i');

    this.perPage = options.perPage || 10;

    this.defaultPostView = options.defaultPostView || 'post';
    this.defaultWrapperView = options.defaultWrapperView || 'wrapper';
    this.defaultListView = options.defaultListView || 'list';

    this.markedOptions = _.extend({}, {
        breaks: true,
        smartLists: true
    }, options.markedOptions);

    this.client = options.client && typeof options.client === 'string' ?
        options.client :
        true;

    this.index = new PostIndex(_.extend({}, options, { playbill: this }));

    this.app = options.app || express();

    if (options.views) this.app.set('views', options.views);
    if (options.viewEngine) this.app.set('view engine', options.viewEngine);

    this.app.locals({
        truncate: truncate,

        // listView is false unless overridden later.
        listView: false,
        // prevent errors by defining these,
        // they will be overridden later.
        post: {},
        posts: [],
        total: 0,
        pages: 0,
        currentPage: 0
    });

    this.init(this.app);

    return this;
}

Playbill.prototype.init = function init(app) {
    var playbill = this;

    app.disable('x-powered-by');
    app.use(express.bodyParser());
    app.use(app.router);

    var getPostsRoute = _getPosts.call(playbill, app);
    app.get('/', getPostsRoute);
    app.get('/.:type(json)', getPostsRoute);
    app.get('/p:page([1-9][0-9]{0,})/', getPostsRoute);
    app.get('/p:page([1-9][0-9]{0,})/.:type(json)', getPostsRoute);

    var getPostRoute = _getPost.call(playbill, app);
    app.get('/:slug([\\w-]+)', getPostRoute);
    app.get('/:slug([\\w-]+).:type(json)', getPostRoute);

    if (playbill.client) {
        var clientPath = typeof playbill.client === 'string' ? playbill.client : '/playbill.js';
        app.get(clientPath, function (req, res) {
            res.sendfile('client.js', { root: path.join(__dirname, '../../client/lib') });
        });
    }
};

Playbill.prototype.listPosts = function listPosts(options) {
    var playbill = this,
        limit,
        offset,
        total;

    options = options || {};

    return when(options)
    .then(function (options) {
        options.page = options.page || 1;
        options.perPage = options.perPage || playbill.perPage;

        limit = options.perPage;
        offset = options.perPage * (options.page - 1);
    })
    .then(function () {
        return playbill.index.init();
    })
    .then(function (index) {
        return index.posts;
    })
    .then(function (posts) {
        total = posts.length;

        // Apply query options.
        posts = arrayQuery('published').is(true)
        .sort('created').date().desc()
        .sort('title')
        .on(posts);

        // Replace broken array-query .offset() implementation.
        posts = posts.slice(offset, limit ? offset+limit : undefined);

        return posts;
    })
    .then(function (unsafePosts) {
        // Sanitize parsed posts.
        return when.all(_.map(unsafePosts, function (unsafePost) {
            return playbill._sanitizePost(unsafePost);
        }));
    })
    .then(function (posts) {
        return {
            total: total,
            posts: posts
        };
    });
};

function _getPosts(app) {
    var playbill = this;

    return function getPosts(req, res, next) {
        var type = req.param('type') || 'html',
            page = parseInt(req.param('page'), 10) || 1;

        playbill.listPosts({
            page: page
        })
        .then(function (data) {
            var posts = data.posts;

            if (!posts.length) {
                var err = new Error('No posts found.');
                err.status = 404;
                return next(err);
            }

            res.locals({
                listView: true,
                total: data.total,
                pages: Math.round(data.total/playbill.perPage),
                currentPage: page
            });

            switch (type) {
                case "json":
                    var _render = nodefn.lift(res.render.bind(res));

                    when.map(posts, function (post) {
                        return _render(post.meta.view || playbill.defaultPostView, { post: post })
                        .then(function (html) {
                            post.html = html;

                            return post;
                        });
                    })
                    .then(function (posts) {
                        return res.send(200, { posts: posts, total: res.locals.total, pages: res.locals.pages, currentPage: page });
                    })
                    .otherwise(next);
                    break;
                default:
                    res.render(playbill.defaultWrapperView, { posts: posts });
                    break;
            }
        })
        .otherwise(function (err) {
            next(err);
        });
    };
}

Playbill.prototype._path = function _path(slug) {
    var playbill = this;
    return path.join(playbill.filePath, slug + playbill.ext);
};

var promiseReadFile = nodefn.lift(fs.readFile);

Playbill.prototype._loadPost = function _loadPost(slug) {
    var playbill = this,
        path = playbill._path(slug);
    return promiseReadFile(path, 'utf8')
    .then(function (string) {
        return {
            slug: slug,
            path: path,
            raw: string
        };
    });
};

var promiseMarked = nodefn.lift(marked);

Playbill.prototype._parsePost = function _parsePost(rawPost) {
    var playbill = this;

    return when(rawPost)
    .then(function (rawPost) {
        return playbill._parseMeta(rawPost);
    })
    .then(function (post) {
        return promiseMarked(post.raw, playbill.markedOptions)
        .then(function (postHTML) {
            post.content = postHTML;
            return post;
        });
    });
};

Playbill.metaRegex = /^(---[\s\S]+)---/;

var promiseStat = nodefn.lift(fs.stat);

Playbill.prototype._parseMeta = function _parseMeta(post) {
    var playbill = this;

    return when(post)
    .then(function (post) {
        post.meta = {};

        var metaMatch = post.raw.match(Playbill.metaRegex);
        if (metaMatch && metaMatch.length > 1) {
            post.meta = metaMatch[1];

            try{
                post.meta = yaml.safeLoad(post.meta);
            } catch(e) {
                console.error(e);
                post.meta = {};
            }

            post.raw = post.raw.replace(Playbill.metaRegex, '');
        }

        return post;
    })
    .then(function (post) {
        if (post.meta.created) {
            post.meta.created = new Date(post.meta.created);
        }
        return post;
    })
    .then(function (post) {
        if (!post.meta.created && post.path) {
            return promiseStat(post.path)
            .then(function (stat) {
                post.meta.created = stat.ctime;
                return post;
            });
        }
        return post;
    })
    .then(function (post) {
        _.each(Post.liftMeta, function (key) {
            post[key] = post.meta[key];
        });
        return post;
    });
};

Playbill.prototype._sanitizePost = function _sanitizePost(unsafePost) {
    return (unsafePost.toJSON ? unsafePost : new Post(unsafePost)).toJSON();
};

Playbill.prototype.fetchPost = function fetchPost(slug) {
    var playbill = this;

    return playbill._loadPost(slug)
    .then(function (rawPost) {
        return playbill._parsePost(rawPost);
    })
    .then(function (unsafePost) {
        return playbill._sanitizePost(unsafePost);
    });
};

function _getPost(app) {
    var playbill = this;

    return function getPost(req, res, next) {
        var slug = req.param('slug'),
            type = req.param('type');

        playbill.index.init()
        .then(function (index) {
            return index.get(slug);
        })
        .then(function (post) {
            if (!post) {
                var err = new Error('Post not found.');
                err.status = 404;
                throw err;
            }
            return post;
        })
        .then(function (post) {
            switch (type) {
                case "json":
                    nodefn.call(res.render.bind(res), post.meta.view || playbill.defaultPostView, { post: post })
                    .then(function (html) {
                        post.html = html;

                        return res.send(200, post);
                    })
                    .otherwise(next);
                    break;
                default:
                    res.render(post.meta.wrapper ? post.meta.wrapper : playbill.defaultWrapperView, { post: post });
                    break;
            }
        })
        .otherwise(function (err) {
            if (err.code === 'ENOENT') {
                err.status = 404;
            }
            return next(err);
        });
    };
}

module.exports = Playbill;
<|endoftext|>