<commit_before>'use strict'

module.exports = (app) => {
  let slapp = app.slapp
  let kv = app.kv

    slapp.message('remove work item (.*)', ['direct_mention', 'direct_message'], (msg, text, workItemTitle) => {
        slapp.client.users.info({ token: msg.meta.bot_token, user: msg.meta.user_id }, (err, data) => {

            kv.get("workItems", (err, dbworkItemList) => {

                let workItemList = [];

                if(dbworkItemList != null)
                    workItemList = dbworkItemList;

                var workItem = workItemList.find(x => x.title === workItemTitle)

                if(workItem == null){
                    msg.say("Cannot find *" + workItemTitle + "*!")
                    return
                }

                let index = workItemList.indexOf(workItem)
                if(index !== -1)
                    workItemList.splice(index, 1);

                kv.set("workItems", workItemList, (err) => {
                    if (err) return handleError(err, msg)

                    kv.get("workItems", (err, updatedWorkItemList) => {
                        if (err) return handleError(err, msg)

                    if(updatedWorkItemList == null)
                        msg.say("Nothing Found!")
                    else
                        msg.say("Removed " + workItemTitle + " from current work item list.").say("Current list: ").say(`\`\`\`${JSON.stringify(workItemList)}\`\`\``)
                    })
                })
            })
        })
    })

    slapp.message('view work items', ['direct_mention', 'direct_message'], (msg) => {
        kv.get("workItems", (err, workItemList) => {
            if (err) return handleError(err, msg)

            if(workItemList == null)
                msg.say("Nothing Found!")
            else
                msg.say(`Current Work Items: \`\`\`${JSON.stringify(workItemList)}\`\`\``)
        })
    })

    slapp.message('new work item', ['direct_mention', 'direct_message'], (msg, text, role) => {
        slapp.client.users.info({ token: msg.meta.bot_token, user: msg.meta.user_id }, (err, data) => {
            kv.get("owners", (err, roleList) => {
                if (err) return handleError(err, msg)

                if(roleList.indexOf(data.user.name) !== -1)
                    msg.say(`Title?`).route('new-item-title', { id: new Date().getTime() })
                else
                    msg.say("Must be owner!")
            })
        })
    })
    .route('new-item-title', (msg, state) => {

        var text = (msg.body.event && msg.body.event.text) || ''

        if (!text) {
        return msg
            .say("Whoops, I'm still waiting to hear our new item.")
            .say('Title?')
            .route('new-item-title', state)
        }

        state.title = text

        msg.say(`Sounds good! Little description?`).route('new-item-desc', state)
    })
    .route('new-item-desc', (msg, state) => {
    var text = (msg.body.event && msg.body.event.text) || ''

    if (!text) {
      return msg
        .say("I'm eagerly awaiting to hear the description.")
        .route('new-item-desc', state)
    }

    state.desc = text

    kv.get("workItems", (err, workItemList) => {
        if (err) return handleError(err, msg)

        let list = [];

        if(workItemList != null){
            list = workItemList
        }

        list.push(state)

        kv.set("workItems", list, (err) => {
            if (err) return handleError(err, msg)
        })
    })

    kv.get("qa reviewers", (err, roleList) => {
        if (err) return handleError(err, msg)

        msg.say('Thanks!').say(`Here's what you've told me so far: \`\`\`${JSON.stringify(state)}\`\`\``)

        roleList.forEach(function(element){
            msg.say('@'+element + ' *' + state.title + '* changes do not require QA or have automated test coverage?')
            msg.say('@'+element + ' Please reply "qa review ' + state.title + ' yes/no"')
        })

        kv.get("dev reviewers", (err, roleList) => {
            if (err) return handleError(err, msg)

            roleList.forEach(function(element){
                msg.say('@'+element + ' *' + state.title + '* changes constainted to one system?')
                msg.say('@'+element + ' Please reply "dev review ' + state.title + ' yes/no"')
            })
        })
    })
  })

    slapp.message('(qa|dev) review (.*) (yes|no)', 'mention', (msg, text, role, item, answer) => {
        if(role === 'dev') {
            if(answer === 'no'){
                msg.say("Can you please give a quick explanation for the channel?").route('handleDevNo', item)
            }
            else{
                kv.get("workItems", (err, dbworkItemList) => {
                    var workItem = dbworkItemList.find(x => x.title === item)

                    workItem.devApproved = true;

                    let answerText = "Thanks!";

                    if(workItem.qaApproved) {
                        workItem.accepted = true;
                        answerText = "Thanks! *" + item + "* is fully approved!"
                    }
                    else{
                        answerText = "Thanks! *" + item + "* is waiting on QA Review."
                    }

                    kv.set("workItems", dbworkItemList, (err) => {
                        if (err) return handleError(err, msg)
                        msg.say(answerText)
                    })
                })
            }
        }
        else {
            if(answer === 'no'){
                msg.say("Can you please give a quick explanation for the channel?").route('handleQANo', item)
            }
            else{
                kv.get("workItems", (err, dbworkItemList) => {
                    var workItem = dbworkItemList.find(x => x.title === item)

                    workItem.qaApproved = true;

                    let answerText = "Thanks!";

                    if(workItem.qaApproved) {
                        workItem.accepted = true;
                        answerText = "Thanks! *" + item + "* is fully approved!"
                    }
                    else{
                        answerText = "Thanks! *" + item + "* is waiting on Dev Review."
                    }

                    kv.set("workItems", dbworkItemList, (err) => {
                        if (err) return handleError(err, msg)
                        msg.say(answerText)
                    })
                })
            }
        }
    })

    slapp.route('handleDevNo', (msg, item) => {
            var text = msg.body.event.text

            kv.get("workItems", (err, dbworkItemList) => {
                var workItem = dbworkItemList.find(x => x.title === item)

                workItem.devApproved = false;
                workItem.rejected = true;
                workItem.devReason = text;

                kv.set("workItems", dbworkItemList, (err) => {
                    if (err) return handleError(err, msg)
                    msg.say("Thanks!")
                })
            })
    })

        slapp.route('handleQANo', (msg, item) => {
            var text = (msg.body.event && msg.body.event.text) || ''

            if (!text) {
                return msg.say("Whoops, I'm still waiting to hear from you.").route('handleQANo', item)
            }

            kv.get("workItems", (err, dbworkItemList) => {
                var workItem = dbworkItemList.find(x => x.title === item)

                workItem.qaApproved = false;
                workItem.rejected = true;
                workItem.qaReason = text;

                kv.set("workItems", dbworkItemList, (err) => {
                    if (err) return handleError(err, msg)
                    msg.say("Thanks!")
                })
            })
    })

    function handleError (err, msg) {
    console.error(err)

    // Only show errors when we can respond with an ephemeral message
    // So this includes any button actions or slash commands
    if (!msg.body.response_url) return

    msg.respond({
        text: `:scream: Uh Oh: ${err.message || err}`,
        response_type: 'ephemeral',
        replace_original: false
    }, (err) => {
        if (err) console.error('Error handling error:', err)
    })
    }

}<commit_msg>adding auth to review and remove work item commands<commit_after>'use strict'

module.exports = (app) => {
  let slapp = app.slapp
  let kv = app.kv

    slapp.message('remove work item (.*)', ['direct_mention', 'direct_message'], (msg, text, workItemTitle) => {
        slapp.client.users.info({ token: msg.meta.bot_token, user: msg.meta.user_id }, (err, data) => {
            kv.get("owners", (err, roleList) => {
                if (err) return handleError(err, msg)

                if(roleList.indexOf(data.user.name) !== -1){
                    kv.get("workItems", (err, dbworkItemList) => {
                        let workItemList = [];

                        if(dbworkItemList != null)
                            workItemList = dbworkItemList;

                        var workItem = workItemList.find(x => x.title === workItemTitle)

                        if(workItem == null){
                            msg.say("Cannot find *" + workItemTitle + "*!")
                            return
                        }

                        let index = workItemList.indexOf(workItem)
                        if(index !== -1)
                            workItemList.splice(index, 1);

                        kv.set("workItems", workItemList, (err) => {
                            if (err) return handleError(err, msg)

                            kv.get("workItems", (err, updatedWorkItemList) => {
                                if (err) return handleError(err, msg)

                            if(updatedWorkItemList == null)
                                msg.say("Nothing Found!")
                            else
                                msg.say("Removed " + workItemTitle + " from current work item list.").say("Current list: ").say(`\`\`\`${JSON.stringify(workItemList)}\`\`\``)
                            })
                        })
                    })
                }
                else
                    msg.say("Must be owner!")
            })
        })
    })

    slapp.message('view work items', ['direct_mention', 'direct_message'], (msg) => {
        kv.get("workItems", (err, workItemList) => {
            if (err) return handleError(err, msg)

            if(workItemList == null)
                msg.say("Nothing Found!")
            else
                msg.say(`Current Work Items: \`\`\`${JSON.stringify(workItemList)}\`\`\``)
        })
    })

    slapp.message('new work item', ['direct_mention', 'direct_message'], (msg, text, role) => {
        slapp.client.users.info({ token: msg.meta.bot_token, user: msg.meta.user_id }, (err, data) => {
            kv.get("owners", (err, roleList) => {
                if (err) return handleError(err, msg)

                if(roleList.indexOf(data.user.name) !== -1)
                    msg.say(`Title?`).route('new-item-title', { id: new Date().getTime() })
                else
                    msg.say("Must be owner!")
            })
        })
    })
    .route('new-item-title', (msg, state) => {

        var text = (msg.body.event && msg.body.event.text) || ''

        if (!text) {
        return msg
            .say("Whoops, I'm still waiting to hear our new item.")
            .say('Title?')
            .route('new-item-title', state)
        }

        state.title = text

        msg.say(`Sounds good! Little description?`).route('new-item-desc', state)
    })
    .route('new-item-desc', (msg, state) => {
        var text = (msg.body.event && msg.body.event.text) || ''

        if (!text) {
        return msg
            .say("I'm eagerly awaiting to hear the description.")
            .route('new-item-desc', state)
        }

        state.desc = text

        kv.get("workItems", (err, workItemList) => {
            if (err) return handleError(err, msg)

            let list = [];

            if(workItemList != null){
                list = workItemList
            }

            list.push(state)

            kv.set("workItems", list, (err) => {
                if (err) return handleError(err, msg)
            })
        })

        kv.get("qa reviewers", (err, roleList) => {
            if (err) return handleError(err, msg)

            msg.say('Thanks!').say(`Here's what you've told me so far: \`\`\`${JSON.stringify(state)}\`\`\``)

            roleList.forEach(function(element){
                msg.say('@'+element + ' *' + state.title + '* changes do not require QA or have automated test coverage?')
                msg.say('@'+element + ' Please reply "qa review ' + state.title + ' yes/no"')
            })

            kv.get("dev reviewers", (err, roleList) => {
                if (err) return handleError(err, msg)

                roleList.forEach(function(element){
                    msg.say('@'+element + ' *' + state.title + '* changes constainted to one system?')
                    msg.say('@'+element + ' Please reply "dev review ' + state.title + ' yes/no"')
                })
            })
        })
    })

    slapp.message('(qa|dev) review (.*) (yes|no)', 'mention', (msg, text, role, item, answer) => {
        if(role === 'dev') {
            slapp.client.users.info({ token: msg.meta.bot_token, user: msg.meta.user_id }, (err, data) => {
                kv.get("dev reviewers", (err, roleList) => {
                    if (err) return handleError(err, msg)

                    if(roleList.indexOf(data.user.name) !== -1){
                        if(answer === 'no'){
                            msg.say("Can you please give a quick explanation for the channel?").route('handleDevNo', item)
                        }
                        else{
                            kv.get("workItems", (err, dbworkItemList) => {
                                var workItem = dbworkItemList.find(x => x.title === item)

                                workItem.devApproved = true;

                                let answerText = "Thanks!";

                                if(workItem.qaApproved) {
                                    workItem.accepted = true;
                                    answerText = "Thanks! *" + item + "* is fully approved!"
                                }
                                else{
                                    answerText = "Thanks! *" + item + "* is waiting on QA Review."
                                }

                                kv.set("workItems", dbworkItemList, (err) => {
                                    if (err) return handleError(err, msg)
                                    msg.say(answerText)
                                })
                            })
                        }
                    }
                    else
                        msg.say("Must be Dev Reviewer!")
                })
            })
        }
        else {
            slapp.client.users.info({ token: msg.meta.bot_token, user: msg.meta.user_id }, (err, data) => {
                kv.get("owners", (err, roleList) => {
                    if (err) return handleError(err, msg)

                    if(roleList.indexOf(data.user.name) !== -1){

                        if(answer === 'no'){
                            msg.say("Can you please give a quick explanation for the channel?").route('handleQANo', item)
                        }
                        else{
                            kv.get("workItems", (err, dbworkItemList) => {
                                var workItem = dbworkItemList.find(x => x.title === item)

                                workItem.qaApproved = true;

                                let answerText = "Thanks!";

                                if(workItem.qaApproved) {
                                    workItem.accepted = true;
                                    answerText = "Thanks! *" + item + "* is fully approved!"
                                }
                                else{
                                    answerText = "Thanks! *" + item + "* is waiting on Dev Review."
                                }

                                kv.set("workItems", dbworkItemList, (err) => {
                                    if (err) return handleError(err, msg)
                                    msg.say(answerText)
                                })
                            })
                        }
                    }
                    else
                        msg.say("Must be QA Reviewer!")
                })
            })
        }
    })

    slapp.route('handleDevNo', (msg, item) => {
            var text = msg.body.event.text

            kv.get("workItems", (err, dbworkItemList) => {
                var workItem = dbworkItemList.find(x => x.title === item)

                workItem.devApproved = false;
                workItem.rejected = true;
                workItem.devReason = text;

                kv.set("workItems", dbworkItemList, (err) => {
                    if (err) return handleError(err, msg)
                    msg.say("Thanks!")
                })
            })
    })

    slapp.route('handleQANo', (msg, item) => {
        var text = (msg.body.event && msg.body.event.text) || ''

        if (!text) {
            return msg.say("Whoops, I'm still waiting to hear from you.").route('handleQANo', item)
        }

        kv.get("workItems", (err, dbworkItemList) => {
            var workItem = dbworkItemList.find(x => x.title === item)

            workItem.qaApproved = false;
            workItem.rejected = true;
            workItem.qaReason = text;

            kv.set("workItems", dbworkItemList, (err) => {
                if (err) return handleError(err, msg)
                msg.say("Thanks!")
            })
        })
    })

    function handleError (err, msg) {
    console.error(err)

    // Only show errors when we can respond with an ephemeral message
    // So this includes any button actions or slash commands
    if (!msg.body.response_url) return

    msg.respond({
        text: `:scream: Uh Oh: ${err.message || err}`,
        response_type: 'ephemeral',
        replace_original: false
    }, (err) => {
        if (err) console.error('Error handling error:', err)
    })
    }

}<|endoftext|>


<commit_before>
import _ from 'lodash';
import express from 'express';
import WebSocket from 'ws';
import bodyParser from 'body-parser';
import superagent from 'superagent';
import mongodb from 'mongodb';
import core from './forest-core';
import auth from './auth';

let serverHost=null;
let serverPort=0;

// --------------------------------

const logRequest = (req, res, next) => {
  console.log('---------------------------->');
  if(req.method==='POST') console.log(req.method, req.originalUrl, req.body && req.body.UID, req.headers.authorization||'');
  else                    console.log(req.method, req.originalUrl,                           req.headers.authorization||'');
  next();
};

const logResponse = (req, res, next) => {
  console.log(res.statusCode);
  console.log('<----------------------------');
  next();
};

const CORS = (req, res, next) => {
  res.header('Access-Control-Allow-Origin','*');
  res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, Authorization'); // Accept, X-Requested-By, Origin, Cache-Control
  next();
};

// --------------------------------

function prefixUIDs(o){
  const s = JSON.stringify(_.omit(o, core.localProps), null, 2);
  return s.replace(/"(uid-[^"]*"[^:])/g, `"http://${serverHost}:${serverPort}/$1`);
}

function checkPKAndReturnObject(Peer, uid, res, r){
  if(!r.pk || r.pk==='FAIL'){
    res.status(404).send('Not found');
  }
  else if(r.pk==='OK' || auth.checkSig(r.pk)){
    // TODO: delete r
    return core.getObject(uid).then(o => {
      res.json(JSON.parse(prefixUIDs(o)));
      if(Peer) core.setNotify(o,Peer);
    });
  }
  else{
    res.status(404).send('Not found');
  }
  // TODO: delete r
}

function checkPKAndSaveObject(User, Peer, body, setnotify, res, r){
  if(!r.pk || r.pk==='FAIL'){
    res.status(403).send('Forbidden');
  }
  else if(r.pk==='OK' || auth.checkSig(r.pk)){
    core.incomingObject(Object.assign({ User }, Peer && { Peer }, body), setnotify);
    res.json({ });
  }
  else{
    res.status(403).send('Forbidden');
  }
  // TODO: delete r
}

// ---- HTTP ----------------------

const app = express();

app.use(bodyParser.json());

app.options("/*",
  logRequest,
  CORS,
  (req, res, next) => {
    res.sendStatus(200);
  },
  logResponse,
);

app.get('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const { Peer, User } = auth.getPeerUser(req);
    const uid = req.originalUrl.substring(1);
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'GET',
      url: uid,
    });
    core.runEvaluator(rc).then(r => {
      if(r.pk) return checkPKAndReturnObject(Peer, uid, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndReturnObject(Peer, uid, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(404).send('Not found');
      next();
    });
  },
  logResponse,
);

app.post('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const body=req.body;
    if(!body || !body.UID) next();
    const { Peer, User } = auth.getPeerUser(req);
    const path = req.originalUrl.substring(1);
    const setnotify=(path!=='notify' && path);
    const n=(setnotify && [setnotify] || []).concat(body.Notify||[]);
    const notifying=(n.length===1 && n[0]) || n;
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'POST',
      url: body.UID,
      notifying,
      body,
    });
    core.runEvaluator(rc).then(r => {
      if(r.pk) return checkPKAndSaveObject(User, Peer, body, setnotify, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndSaveObject(User, Peer, body, setnotify, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(403).send('Forbidden');
      next();
    });
  },
  logResponse,
);

// --------------------------------

function doGet(url){
  return superagent.get(url)
    .timeout({ response: 9000, deadline: 10000 })
    .set(auth.makeHTTPAuth())
    .then(x => x.body);
}

// ---- WebSockets ----------------

const pendingWSpackets = {};

function doPost(o, u){
  if(core.isURL(u)) return Promise.resolve(false);
  const Peer = core.isNotify(u)? u: o.Peer;
  if(!pendingWSpackets[Peer]) pendingWSpackets[Peer] = [];
  pendingWSpackets[Peer].push(prefixUIDs(o));
  wsFlush(Peer);
  return Promise.resolve(true);
}

const peer2ws = {};

function wsInit(config){
  const wss = new WebSocket.Server(config);
  wss.on('connection', (ws) => {
    ws.on('message', (data) => {
      const body = JSON.parse(data);
      if(body.Peer){
        console.log('ws init:', body);
        peer2ws[body.Peer]=ws;
        ws.send(auth.makeWSAuth());
        wsFlush(body.Peer);
      }
      else{
        console.log('ws incoming:', body);
      }
    });
  });
}

function wsFlush(Peer){
  const ws = peer2ws[Peer];
  if(!ws){
    console.log('websocket not found', Peer);
    return;
  }
  let packet;
  while((packet=(pendingWSpackets[Peer]||[]).shift())){
    try{
      console.log('<<----------ws---------------', Peer);
      if(ws.readyState === ws.OPEN) ws.send(packet);
      else console.log('WebSocket closed sending\n', packet, '\nto', Peer);
    }
    catch(e){
      console.error('error sending\n', packet, '\nto', Peer, '\n', e);
    }
  }
}

// --------------------------------

core.setNetwork({ doGet, doPost });

// ---- Mongo ---------------------

let forestdb;

function persistenceInit(mongoHostPort){
  return mongodb.MongoClient.connect(mongoHostPort)
    .then((client) => {
      forestdb = client.db('forest');
    });
}

function persist(o){
  if(!o.is) return Promise.resolve();
  const collectionName = (o.is.constructor===String)? o.is:
                        ((o.is.constructor===Array)? o.is.join('-'): null);
  if(!collectionName) return Promise.resolve();
  return forestdb.collection(collectionName)
    .update({ UID: o.UID }, o, { upsert: true })
    .then(result => result.result)
    .catch(err => { console.log(err, `Failed to insert ${o.UID} in ${collectionName}`); return err });
}

function fetch(uid){
  return forestdb.collections().then(colls=>findOneFirst(colls, uid));
}

function findOneFirst(colls, uid){
  if(!(colls && colls.length)) return Promise.resolve(null);
  return colls[0].findOne({ UID: uid }, { projection: { _id: 0 }}).then(o => o || findOneFirst(colls.slice(1), uid));
}

function recache(){
  return forestdb.collections()
    .then(colls=>Promise.all(colls.map(coll=>coll.find({ Cache: 'keep-active' }, { projection: { _id: 0 }}).toArray()))
                  .then(actives => [].concat(...actives)));
}

function query(is, scope, query){
  return forestdb.collection(is.join('-'))
    .find(toMongo(scope, query.match), { projection: { _id: 0 }})
    .toArray()
    .then(r => r.map(o => query.inline? getInlineVals(o, query.inline): o.UID))
    .catch(e => console.error(e));
}

function toMongo(scope, match){
  return Object.assign({}, ...Object.keys(match).map(k => ({[k]: toMongoProp(k,match[k])})));
}

function toMongoProp(key, val){
  if(val.length===3 && val[1]==='..'){
    return { $gt: val[0], $lt: val[2] };
  }
  return val;
}

function getInlineVals(o, inline){
  return Object.assign({}, ...inline.map(k => o[k] && { [k]: o[k] }), { More: o.UID });
}

function dropAll(actually){
  return forestdb.collections()
            .then(colls=>Promise.all(colls.map(coll=>coll.stats()
              .then(s=>{
                  if(!/system.indexes/.test(s.ns)){
                    console.log(actually? '*************** dropping': '(not dropping)', s.ns, s.count);
                    return actually && coll.drop();
                  }
              })
            )));
}

core.setPersistence({ persist, fetch, recache, query });

// --------------------------------

function init({httpHost, httpPort, wsPort, mongoHostPort}){
  serverHost=httpHost; serverPort=httpPort;
  return new Promise((resolve, reject) => {
    persistenceInit(mongoHostPort)
      .then(() => {
        app.listen(httpPort, ()=>{
          console.log(`Server started on port ${httpPort}`);
          wsInit({ port: wsPort });
          resolve();
        }).on('error', (err) => reject(err));
      })
      .catch((err) => reject(err));
  });
}

export default {
  init,
  dropAll,
  cacheObjects:        core.cacheObjects,
  reCacheObjects:      core.reCacheObjects,
  setEvaluator:        core.setEvaluator,
  runEvaluator:        core.runEvaluator,
  getObject:           core.getObject,
  spawnObject:         core.spawnObject,
  makeUID:             core.makeUID,
  setLogging:          core.setLogging,
  setPeerIdentityUser: auth.setPeerIdentityUser,
}

<commit_msg>set me up some mqtt<commit_after>
import _ from 'lodash';
import express from 'express';
import WebSocket from 'ws';
import bodyParser from 'body-parser';
import superagent from 'superagent';
import mongodb from 'mongodb';
import core from './forest-core';
import auth from './auth';
import mosca from 'mosca';

let serverHost=null;
let serverPort=0;

// --------------------------------

const logRequest = (req, res, next) => {
  console.log('---------------------------->');
  if(req.method==='POST') console.log(req.method, req.originalUrl, req.body && req.body.UID, req.headers.authorization||'');
  else                    console.log(req.method, req.originalUrl,                           req.headers.authorization||'');
  next();
};

const logResponse = (req, res, next) => {
  console.log(res.statusCode);
  console.log('<----------------------------');
  next();
};

const CORS = (req, res, next) => {
  res.header('Access-Control-Allow-Origin','*');
  res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, Authorization'); // Accept, X-Requested-By, Origin, Cache-Control
  next();
};

// --------------------------------

function prefixUIDs(o){
  const s = JSON.stringify(_.omit(o, core.localProps), null, 2);
  return s.replace(/"(uid-[^"]*"[^:])/g, `"http://${serverHost}:${serverPort}/$1`);
}

function checkPKAndReturnObject(Peer, uid, res, r){
  if(!r.pk || r.pk==='FAIL'){
    res.status(404).send('Not found');
  }
  else if(r.pk==='OK' || auth.checkSig(r.pk)){
    // TODO: delete r
    return core.getObject(uid).then(o => {
      res.json(JSON.parse(prefixUIDs(o)));
      if(Peer) core.setNotify(o,Peer);
    });
  }
  else{
    res.status(404).send('Not found');
  }
  // TODO: delete r
}

function checkPKAndSaveObject(User, Peer, body, setnotify, res, r){
  if(!r.pk || r.pk==='FAIL'){
    res.status(403).send('Forbidden');
  }
  else if(r.pk==='OK' || auth.checkSig(r.pk)){
    core.incomingObject(Object.assign({ User }, Peer && { Peer }, body), setnotify);
    res.json({ });
  }
  else{
    res.status(403).send('Forbidden');
  }
  // TODO: delete r
}

// ---- HTTP ----------------------

const app = express();

app.use(bodyParser.json());

app.options("/*",
  logRequest,
  CORS,
  (req, res, next) => {
    res.sendStatus(200);
  },
  logResponse,
);

app.get('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const { Peer, User } = auth.getPeerUser(req);
    const uid = req.originalUrl.substring(1);
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'GET',
      url: uid,
    });
    core.runEvaluator(rc).then(r => {
      if(r.pk) return checkPKAndReturnObject(Peer, uid, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndReturnObject(Peer, uid, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(404).send('Not found');
      next();
    });
  },
  logResponse,
);

app.post('/*',
  logRequest,
  CORS,
  (req, res, next) => {
    const body=req.body;
    if(!body || !body.UID) next();
    const { Peer, User } = auth.getPeerUser(req);
    const path = req.originalUrl.substring(1);
    const setnotify=(path!=='notify' && path);
    const n=(setnotify && [setnotify] || []).concat(body.Notify||[]);
    const notifying=(n.length===1 && n[0]) || n;
    const rc=core.spawnTemporaryObject({
      Evaluator: 'evalRequestChecker',
      is: ['request', 'checker'],
      user: User,
      peer: Peer,
      method: 'POST',
      url: body.UID,
      notifying,
      body,
    });
    core.runEvaluator(rc).then(r => {
      if(r.pk) return checkPKAndSaveObject(User, Peer, body, setnotify, res, r);
      return new Promise(resolve => setTimeout(()=>core.getObject(rc).then(r=>resolve(checkPKAndSaveObject(User, Peer, body, setnotify, res, r))), 500));
    })
    .then(()=>next())
    .catch(e => {
      console.error(e);
      res.status(403).send('Forbidden');
      next();
    });
  },
  logResponse,
);

// --------------------------------

function doGet(url){
  return superagent.get(url)
    .timeout({ response: 9000, deadline: 10000 })
    .set(auth.makeHTTPAuth())
    .then(x => x.body);
}

// ---- WebSockets ----------------

const pendingWSpackets = {};

function doPost(o, u){
  if(core.isURL(u)) return Promise.resolve(false);
  const Peer = core.isNotify(u)? u: o.Peer;
  if(!pendingWSpackets[Peer]) pendingWSpackets[Peer] = [];
  pendingWSpackets[Peer].push(prefixUIDs(o));
  wsFlush(Peer);
  return Promise.resolve(true);
}

const peer2ws = {};

function wsInit(config){
  const wss = new WebSocket.Server(config);
  wss.on('connection', (ws) => {
    ws.on('message', (data) => {
      const body = JSON.parse(data);
      if(body.Peer){
        console.log('ws init:', body);
        peer2ws[body.Peer]=ws;
        ws.send(auth.makeWSAuth());
        wsFlush(body.Peer);
      }
      else{
        console.log('ws incoming:', body);
      }
    });
  });
}

function wsFlush(Peer){
  const ws = peer2ws[Peer];
  if(!ws){
    console.log('websocket not found', Peer);
    return;
  }
  let packet;
  while((packet=(pendingWSpackets[Peer]||[]).shift())){
    try{
      console.log('<<----------ws---------------', Peer);
      if(ws.readyState === ws.OPEN) ws.send(packet);
      else console.log('WebSocket closed sending\n', packet, '\nto', Peer);
    }
    catch(e){
      console.error('error sending\n', packet, '\nto', Peer, '\n', e);
    }
  }
}

function mqttInit(config){

  const mqtts = new mosca.Server(config);

  mqtts.on('ready', () => {
  });

  mqtts.on('clientConnected', (client) => {
  });

  mqtts.on('published', (packet, client) => {
    if(packet.topic.startsWith('$')) return;
  });
}

// --------------------------------

core.setNetwork({ doGet, doPost });

// ---- Mongo ---------------------

let forestdb;

function persistenceInit(mongoHostPort){
  return mongodb.MongoClient.connect(mongoHostPort)
    .then((client) => {
      forestdb = client.db('forest');
    });
}

function persist(o){
  if(!o.is) return Promise.resolve();
  const collectionName = (o.is.constructor===String)? o.is:
                        ((o.is.constructor===Array)? o.is.join('-'): null);
  if(!collectionName) return Promise.resolve();
  return forestdb.collection(collectionName)
    .update({ UID: o.UID }, o, { upsert: true })
    .then(result => result.result)
    .catch(err => { console.log(err, `Failed to insert ${o.UID} in ${collectionName}`); return err });
}

function fetch(uid){
  return forestdb.collections().then(colls=>findOneFirst(colls, uid));
}

function findOneFirst(colls, uid){
  if(!(colls && colls.length)) return Promise.resolve(null);
  return colls[0].findOne({ UID: uid }, { projection: { _id: 0 }}).then(o => o || findOneFirst(colls.slice(1), uid));
}

function recache(){
  return forestdb.collections()
    .then(colls=>Promise.all(colls.map(coll=>coll.find({ Cache: 'keep-active' }, { projection: { _id: 0 }}).toArray()))
                  .then(actives => [].concat(...actives)));
}

function query(is, scope, query){
  return forestdb.collection(is.join('-'))
    .find(toMongo(scope, query.match), { projection: { _id: 0 }})
    .toArray()
    .then(r => r.map(o => query.inline? getInlineVals(o, query.inline): o.UID))
    .catch(e => console.error(e));
}

function toMongo(scope, match){
  return Object.assign({}, ...Object.keys(match).map(k => ({[k]: toMongoProp(k,match[k])})));
}

function toMongoProp(key, val){
  if(val.length===3 && val[1]==='..'){
    return { $gt: val[0], $lt: val[2] };
  }
  return val;
}

function getInlineVals(o, inline){
  return Object.assign({}, ...inline.map(k => o[k] && { [k]: o[k] }), { More: o.UID });
}

function dropAll(actually){
  return forestdb.collections()
            .then(colls=>Promise.all(colls.map(coll=>coll.stats()
              .then(s=>{
                  if(!/system.indexes/.test(s.ns)){
                    console.log(actually? '*************** dropping': '(not dropping)', s.ns, s.count);
                    return actually && coll.drop();
                  }
              })
            )));
}

core.setPersistence({ persist, fetch, recache, query });

// --------------------------------

function init({httpHost, httpPort, wsPort, mongoHostPort, mqttConfig}){
  serverHost=httpHost; serverPort=httpPort;
  return new Promise((resolve, reject) => {
    persistenceInit(mongoHostPort)
      .then(() => {
        app.listen(httpPort, ()=>{
          console.log(`Server started on port ${httpPort}`);
          wsInit({ port: wsPort });
          mqttInit(mqttConfig);
          resolve();
        }).on('error', (err) => reject(err));
      })
      .catch((err) => reject(err));
  });
}

export default {
  init,
  dropAll,
  cacheObjects:        core.cacheObjects,
  reCacheObjects:      core.reCacheObjects,
  setEvaluator:        core.setEvaluator,
  runEvaluator:        core.runEvaluator,
  getObject:           core.getObject,
  spawnObject:         core.spawnObject,
  makeUID:             core.makeUID,
  setLogging:          core.setLogging,
  setPeerIdentityUser: auth.setPeerIdentityUser,
}

<|endoftext|>


<commit_before>define([
    'jquery',
    'var/blankImg',
    'var/slice',
    'var/css_class',
    'var/emojioneSupportMode',
    'function/trigger',
    'function/attach',
    'function/shortnameTo',
    'function/pasteHtmlAtCaret',
    'function/getOptions',
    'function/saveSelection',
    'function/restoreSelection',
    'function/htmlFromText',
    'function/textFromHtml',
    'function/isObject',
    'function/calcButtonPosition',
    'function/lazyLoading',
    'function/selector',
    'function/div'
],
function($, blankImg, slice, css_class, emojioneSupportMode, trigger, attach, shortnameTo, pasteHtmlAtCaret,
         getOptions, saveSelection, restoreSelection, htmlFromText, textFromHtml, isObject,
         calcButtonPosition, lazyLoading, selector, div)
{
    return function(self, source, options) {
        options = getOptions(options);
        self.sprite     = options.sprite && emojioneSupportMode < 3;
        self.shortnames = options.shortnames;
        self.pickerPosition = options.pickerPosition;

        var sourceValFunc = source.is("TEXTAREA") || source.is("INPUT") ? "val" : "text",
            editor, button, picker, tones, filters, filtersBtns, emojisList, categories, scrollArea,
            app = div({"class" : css_class + " " + (source.attr("class") || ""), role: "application"},
                editor = self.editor = div('editor').attr({
                    contenteditable: true,
                    placeholder: options["placeholder"] || source.data("placeholder") || source.attr("placeholder") || "",
                    tabindex: 0
                }),
                button = self.button = div('button',
                    div('button-open'),
                    div('button-close')
                ).attr('title', options.buttonTitle),
                picker = self.picker = div('picker',
                    div('wrapper',
                        filters = div('filters'),
                        scrollArea = div('scroll-area',
                            emojisList = div('emojis-list'),
                            tones = div('tones',
                                function() {
                                    var btns = [];
                                    if (options.tones) {
                                        this.addClass(selector('tones-' + options.tonesStyle, true));
                                        for (var i = 0; i <= 5; i++) {
                                            btns.push($("<button/>", {
                                                "class": "btn-tone btn-tone-" + i + (!i ? " active" : ""),
                                                "data-skin": i
                                            }));
                                        }
                                    }
                                    return btns;
                                }
                            )
                        )
                    )
                ).addClass(selector('picker-position-' + options.pickerPosition, true))
                 .addClass(selector('filters-position-' + options.filtersPosition, true))
                 .addClass('hidden')
            );

        if (options.inline) {
            app.addClass(selector('inline', true));
        }

        $.each(options.filters, function(filter, params) {
            var skin = 0;
            if (filter !== 'tones') {
                $("", {
                    "class": selector("filter", true) + " " + selector("filter-" + filter, true),
                    "data-filter": filter,
                    title: params.title
                })
                .wrapInner(shortnameTo(params.icon, self.sprite ? '' : ''))
                .appendTo(filters);
            } else if (options.tones) {
                skin = 5;
            } else {
                return;
            }
            do {
                var category = div('category').attr({name: filter, "data-tone": skin}).appendTo(emojisList),
                    items = params.emoji.replace(/[\s,;]+/g, '|');
                if (skin > 0) {
                    category.hide();
                    items = items.split('|').join('_tone' + skin + '|') + '_tone' + skin;
                }
                items = shortnameTo(items,
                    '<{0} class="emojione{1}"{2}>',
                    self.sprite, [["i", "img"], ["-{uni}", "emoji lazy-emoji"],
                        ["> 0);
        });

        options.filters = null;
        if (!self.sprite) {
            self.lasyEmoji = emojisList.find(".lazy-emoji");
        }

        filtersBtns = filters.find(selector("filter"));
        filtersBtns.eq(0).addClass("active");
        categories = emojisList.find(selector("category"));

        if (options.container) {
            $(options.container).wrapInner(app);
        } else {
            app.insertAfter(source);
        }

        if (options.hideSource) {
            source.hide();
        }

        self.setText(source[sourceValFunc]());
        calcButtonPosition.apply(self);

        // attach() must be called before any .on() methods !!!
        // 1) attach() stores events into possibleEvents{},
        // 2) .on() calls bindEvent() and stores handlers into eventStorage{},
        // 3) bindEvent() finds events in possibleEvents{} and bind founded via jQuery.on()
        // 4) attached events via jQuery.on() calls trigger()
        // 5) trigger() calls handlers stored into eventStorage{}

        attach(self, emojisList.find(".emojibtn"), {click: "emojibtn.click"});
        attach(self, window, {resize: "!resize"});
        attach(self, tones.children(), {click: "tone.click"});
        attach(self, [picker, button], {mousedown: "!mousedown"}, editor);
        attach(self, button, {click: "button.click"});
        attach(self, editor, {paste :"!paste"}, editor);
        attach(self, editor, ["focus", "blur"], function() { return self.stayFocused ? false : editor; });
        attach(self, picker, {mousedown: "picker.mousedown", mouseup: "picker.mouseup", click: "picker.click",
            keyup: "picker.keyup", keydown: "picker.keydown", keypress: "picker.keypress"});
        attach(self, editor, ["mousedown", "mouseup", "click", "keyup", "keydown", "keypress"]);
        attach(self, picker.find(".emojionearea-filter"), {click: "filter.click"});

        if (isObject(options.events) && !$.isEmptyObject(options.events)) {
            $.each(options.events, function(event, handler) {
                self.on(event.replace(/_/g, '.'), handler);
            });
        }

        var noListenScroll = false;
        scrollArea.on('scroll', function () {
            if (!noListenScroll) {
                lazyLoading.call(self);
                if (scrollArea.is(":not(.skinnable")) {
                    var item = categories.eq(0), scrollTop = scrollArea.offset().top;
                    categories.each(function (i, e) {
                        if ($(e).offset().top - scrollTop >= 3) {
                            return false;
                        }
                        item = $(e);
                    });
                    var filter = filtersBtns.filter('[data-filter="' + item.attr("name") + '"]');
                    if (filter[0] && !filter.is(".active")) {
                        filtersBtns.removeClass("active");
                        filter.addClass("active");
                    }
                }
            }
        });

        editor.on("input", function(){ console.log('input');});

        self.on("@filter.click", function(filter) {
            var isActive = filter.is(".active");
            if (scrollArea.is(".skinnable")) {
                if (isActive) return;
                tones.children().eq(0).click();
            }
            noListenScroll = true;
            if (!isActive) {
                filtersBtns.filter(".active").removeClass("active");
                filter.addClass("active");
            }
            var headerOffset = categories.filter('[name="' + filter.data('filter') + '"]').offset().top,
                scroll = scrollArea.scrollTop(),
                offsetTop = scrollArea.offset().top;
            scrollArea.stop().animate({
                scrollTop: headerOffset + scroll - offsetTop - 2
            }, 200, 'swing', function () {
                lazyLoading.call(self);
                noListenScroll = false;
            });
        })

        .on("@picker.show", lazyLoading)

        .on("@tone.click", function(tone) {
            tones.children().removeClass("active");
            var skin = tone.addClass("active").data("skin");
            if (skin) {
                scrollArea.addClass("skinnable");
                categories.hide().filter("[data-tone=" + skin + "]").show();
            } else {
                scrollArea.removeClass("skinnable");
                categories.hide().filter("[data-tone=0]").show();
            }
            lazyLoading.call(self);
        })

        .on("@button.click", function(button) {
            if (button.is(".active")) {
                self.hidePicker();
            } else {
                self.showPicker();
            }
        })

        .on("@!paste", function(editor) {
            self.stayFocused = true;
            // insert invisible character for fix caret position
            pasteHtmlAtCaret('');

            var sel = saveSelection(editor[0]),
                editorScrollTop = editor.scrollTop(),
                clipboard = $("
", {contenteditable: true})
                    .css({position: "fixed", left: "-999px", width: "1px", height: "1px", top: "20px", overflow: "hidden"})
                    .appendTo($("BODY"))
                    .focus();

            window.setTimeout(function() {
                var caretID = "caret-" + (new Date()).getTime();
                editor.focus();
                restoreSelection(editor[0], sel);
                var text = textFromHtml(clipboard.html().replace(/\r\n|\n|\r/g, '
'), self),
                    html = htmlFromText(text, self);
                pasteHtmlAtCaret(html);
                clipboard.remove();
                pasteHtmlAtCaret('');
                editor.scrollTop(editorScrollTop);
                var caret = $("#" + caretID),
                    top = caret.offset().top - editor.offset().top,
                    height = editor.height();
                if (editorScrollTop + top >= height || editorScrollTop > top) {
                    editor.scrollTop(editorScrollTop + top - 2 * height/3);
                }
                caret.remove();
                self.stayFocused = false;
                calcButtonPosition.apply(self);
                trigger(self, 'paste', [editor, text, html]);
            }, 200);
        })

        .on("@emojibtn.click", function(emojibtn) {
            if (!app.is(".focused")) {
                editor.focus();
            }
            saveSelection(editor[0]);
            pasteHtmlAtCaret(shortnameTo(emojibtn.data("name"),
                '', self.sprite, [['-{uni}','emoji'], [blankImg,'{img}']]));
        })

        .on("@!resize @keyup @emojibtn.click", calcButtonPosition)

        .on("@!mousedown", function(editor, event) {
            if (!app.is(".focused")) {
                editor.focus();
            }
            event.preventDefault();
            return false;
        })

        .on("@change", function(editor) {
            var html = editor.html().replace(/<\/?(?:div|span|p)[^>]*>/ig, '');
            // clear input: chrome adds
 when contenteditable is empty
            if (!html.length || /^
]*>$/i.test(html)) {
                self.setText('', false);
            }
            source[sourceValFunc](self.getText());
        })

        .on("@focus", function() {
            app.addClass("focused");
        })

        .on("@blur", function(editor) {
            app.removeClass("focused");

            if (options.hidePickerOnBlur) {
                self.hidePicker();
            }

            var content = editor.html();
            if (self.content !== content) {
                self.content = content;
                trigger(self, 'change', [editor]);
                source.blur().trigger("change");
            } else {
                source.blur();
            }
        });

    };
});<commit_msg>Update<commit_after>define([
    'jquery',
    'var/blankImg',
    'var/slice',
    'var/css_class',
    'var/emojioneSupportMode',
    'function/trigger',
    'function/attach',
    'function/shortnameTo',
    'function/pasteHtmlAtCaret',
    'function/getOptions',
    'function/saveSelection',
    'function/restoreSelection',
    'function/htmlFromText',
    'function/textFromHtml',
    'function/isObject',
    'function/calcButtonPosition',
    'function/lazyLoading',
    'function/selector',
    'function/div',
    'function/calcElapsedTime', // debug only
],
function($, blankImg, slice, css_class, emojioneSupportMode, trigger, attach, shortnameTo, pasteHtmlAtCaret,
         getOptions, saveSelection, restoreSelection, htmlFromText, textFromHtml, isObject,
         calcButtonPosition, lazyLoading, selector, div)
{
    return function(self, source, options) {
        calcElapsedTime('init', function() {
        options = getOptions(options);
        self.sprite     = options.sprite && emojioneSupportMode < 3;
        self.shortnames = options.shortnames;
        self.pickerPosition = options.pickerPosition;
        self.saveEmojisAs = options.saveEmojisAs;
        self.emojiTemplate = '' : 'emoji" src="{img}"/>');

        var sourceValFunc = source.is("TEXTAREA") || source.is("INPUT") ? "val" : "text",
            editor, button, picker, tones, filters, filtersBtns, emojisList, categories, scrollArea,
            app = div({"class" : css_class + " " + (source.attr("class") || ""), role: "application"},
                editor = self.editor = div('editor').attr({
                    contenteditable: true,
                    placeholder: options["placeholder"] || source.data("placeholder") || source.attr("placeholder") || "",
                    tabindex: 0
                }),
                button = self.button = div('button',
                    div('button-open'),
                    div('button-close')
                ).attr('title', options.buttonTitle),
                picker = self.picker = div('picker',
                    div('wrapper',
                        filters = div('filters'),
                        scrollArea = div('scroll-area',
                            emojisList = div('emojis-list'),
                            tones = div('tones',
                                function() {
                                    var btns = [];
                                    if (options.tones) {
                                        this.addClass(selector('tones-' + options.tonesStyle, true));
                                        for (var i = 0; i <= 5; i++) {
                                            btns.push($("<button/>", {
                                                "class": "btn-tone btn-tone-" + i + (!i ? " active" : ""),
                                                "data-skin": i
                                            }));
                                        }
                                    }
                                    return btns;
                                }
                            )
                        )
                    )
                ).addClass(selector('picker-position-' + options.pickerPosition, true))
                 .addClass(selector('filters-position-' + options.filtersPosition, true))
                 .addClass('hidden')
            );

        if (options.inline) {
            app.addClass(selector('inline', true));
        }

        $.each(options.filters, function(filter, params) {
            var skin = 0;
            if (filter !== 'tones') {
                $("", {
                    "class": selector("filter", true) + " " + selector("filter-" + filter, true),
                    "data-filter": filter,
                    title: params.title
                })
                .wrapInner(shortnameTo(params.icon, self.sprite ? '' : ''))
                .appendTo(filters);
            } else if (options.tones) {
                skin = 5;
            } else {
                return;
            }
            do {
                var category = div('category').attr({name: filter, "data-tone": skin}).appendTo(emojisList),
                    items = params.emoji.replace(/[\s,;]+/g, '|');
                if (skin > 0) {
                    category.hide();
                    items = items.split('|').join('_tone' + skin + '|') + '_tone' + skin;
                }
                items = shortnameTo(items,
                    self.sprite ?
                    '' :
                    '',
                    true).split('|').join('');
                category.html(items);
                $('
').text(params.title).prependTo(category);
            } while (--skin > 0);
        });

        options.filters = null;
        if (!self.sprite) {
            self.lasyEmoji = emojisList.find(".lazy-emoji");
        }

        filtersBtns = filters.find(selector("filter"));
        filtersBtns.eq(0).addClass("active");
        categories = emojisList.find(selector("category"));

        if (options.container) {
            $(options.container).wrapInner(app);
        } else {
            app.insertAfter(source);
        }

        if (options.hideSource) {
            source.hide();
        }

        self.setText(source[sourceValFunc]());
        calcButtonPosition.apply(self);

        // attach() must be called before any .on() methods !!!
        // 1) attach() stores events into possibleEvents{},
        // 2) .on() calls bindEvent() and stores handlers into eventStorage{},
        // 3) bindEvent() finds events in possibleEvents{} and bind founded via jQuery.on()
        // 4) attached events via jQuery.on() calls trigger()
        // 5) trigger() calls handlers stored into eventStorage{}

        attach(self, emojisList.find(".emojibtn"), {click: "emojibtn.click"});
        attach(self, window, {resize: "!resize"});
        attach(self, tones.children(), {click: "tone.click"});
        attach(self, [picker, button], {mousedown: "!mousedown"}, editor);
        attach(self, button, {click: "button.click"});
        attach(self, editor, {paste :"!paste"}, editor);
        attach(self, editor, ["focus", "blur"], function() { return self.stayFocused ? false : editor; });
        attach(self, picker, {mousedown: "picker.mousedown", mouseup: "picker.mouseup", click: "picker.click",
            keyup: "picker.keyup", keydown: "picker.keydown", keypress: "picker.keypress"});
        attach(self, editor, ["mousedown", "mouseup", "click", "keyup", "keydown", "keypress"]);
        attach(self, picker.find(".emojionearea-filter"), {click: "filter.click"});

        if (isObject(options.events) && !$.isEmptyObject(options.events)) {
            $.each(options.events, function(event, handler) {
                self.on(event.replace(/_/g, '.'), handler);
            });
        }

        var noListenScroll = false;
        scrollArea.on('scroll', function () {
            if (!noListenScroll) {
                lazyLoading.call(self);
                if (scrollArea.is(":not(.skinnable")) {
                    var item = categories.eq(0), scrollTop = scrollArea.offset().top;
                    categories.each(function (i, e) {
                        if ($(e).offset().top - scrollTop >= 3) {
                            return false;
                        }
                        item = $(e);
                    });
                    var filter = filtersBtns.filter('[data-filter="' + item.attr("name") + '"]');
                    if (filter[0] && !filter.is(".active")) {
                        filtersBtns.removeClass("active");
                        filter.addClass("active");
                    }
                }
            }
        });

        editor.on("input", function(){ console.log('input');});

        self.on("@filter.click", function(filter) {
            var isActive = filter.is(".active");
            if (scrollArea.is(".skinnable")) {
                if (isActive) return;
                tones.children().eq(0).click();
            }
            noListenScroll = true;
            if (!isActive) {
                filtersBtns.filter(".active").removeClass("active");
                filter.addClass("active");
            }
            var headerOffset = categories.filter('[name="' + filter.data('filter') + '"]').offset().top,
                scroll = scrollArea.scrollTop(),
                offsetTop = scrollArea.offset().top;
            scrollArea.stop().animate({
                scrollTop: headerOffset + scroll - offsetTop - 2
            }, 200, 'swing', function () {
                lazyLoading.call(self);
                noListenScroll = false;
            });
        })

        .on("@picker.show", lazyLoading)

        .on("@tone.click", function(tone) {
            tones.children().removeClass("active");
            var skin = tone.addClass("active").data("skin");
            if (skin) {
                scrollArea.addClass("skinnable");
                categories.hide().filter("[data-tone=" + skin + "]").show();
            } else {
                scrollArea.removeClass("skinnable");
                categories.hide().filter("[data-tone=0]").show();
            }
            lazyLoading.call(self);
        })

        .on("@button.click", function(button) {
            if (button.is(".active")) {
                self.hidePicker();
            } else {
                self.showPicker();
            }
        })

        .on("@!paste", function(editor) {
            self.stayFocused = true;
            // insert invisible character for fix caret position
            pasteHtmlAtCaret('');

            var sel = saveSelection(editor[0]),
                editorScrollTop = editor.scrollTop(),
                clipboard = $("
", {contenteditable: true})
                    .css({position: "fixed", left: "-999px", width: "1px", height: "1px", top: "20px", overflow: "hidden"})
                    .appendTo($("BODY"))
                    .focus();

            window.setTimeout(function() {
                var caretID = "caret-" + (new Date()).getTime();
                editor.focus();
                restoreSelection(editor[0], sel);
                var text = textFromHtml(clipboard.html().replace(/\r\n|\n|\r/g, '
'), self),
                    html = htmlFromText(text, self);
                pasteHtmlAtCaret(html);
                clipboard.remove();
                pasteHtmlAtCaret('');
                editor.scrollTop(editorScrollTop);
                var caret = $("#" + caretID),
                    top = caret.offset().top - editor.offset().top,
                    height = editor.height();
                if (editorScrollTop + top >= height || editorScrollTop > top) {
                    editor.scrollTop(editorScrollTop + top - 2 * height/3);
                }
                caret.remove();
                self.stayFocused = false;
                calcButtonPosition.apply(self);
                trigger(self, 'paste', [editor, text, html]);
            }, 200);
        })

        .on("@emojibtn.click", function(emojibtn) {
            if (!app.is(".focused")) {
                editor.focus();
            }
            saveSelection(editor[0]);
            pasteHtmlAtCaret(shortnameTo(emojibtn.data("name"), self.emojiTemplate));
        })

        .on("@!resize @keyup @emojibtn.click", calcButtonPosition)

        .on("@!mousedown", function(editor, event) {
            if (!app.is(".focused")) {
                editor.focus();
            }
            event.preventDefault();
            return false;
        })

        .on("@change", function(editor) {
            var html = editor.html().replace(/<\/?(?:div|span|p)[^>]*>/ig, '');
            // clear input: chrome adds
 when contenteditable is empty
            if (!html.length || /^
]*>$/i.test(html)) {
                self.setText('', false);
            }
            source[sourceValFunc](self.getText());
        })

        .on("@focus", function() {
            app.addClass("focused");
        })

        .on("@blur", function(editor) {
            app.removeClass("focused");

            if (options.hidePickerOnBlur) {
                self.hidePicker();
            }

            var content = editor.html();
            if (self.content !== content) {
                self.content = content;
                trigger(self, 'change', [editor]);
                source.blur().trigger("change");
            } else {
                source.blur();
            }
        });

        }, self.id === 1); // calcElapsedTime()
    };
});<|endoftext|>


<commit_before>
/**
 * Calculate the Earth Rotation Angle
 * @param  {number} days UT1 days since J2000.0
 * @return {number}      ERA (radians)
 */
export function era(days) {
  return 2 * Math.PI * ( 0.7790572732640 + 1.00273781191135448 * days );
}
<commit_msg>Return values module 2π<commit_after>
/**
 * Calculate the Earth Rotation Angle
 * @param  {number} days UT1 days since J2000.0
 * @return {number}      ERA (radians)
 */
export function era(days) {
  return 2 * Math.PI * ( 0.7790572732640 + 1.00273781191135448 * days ) % (2 * Math.PI);
}
<|endoftext|>


<commit_before>"use strict";

var utils = require("../utils");
var log = require("npmlog");

module.exports = function(defaultFuncs, api, ctx) {
  return function getThreadInfo(threadID, callback) {
    if(!callback) callback = function() {};

    var form = {
      'client' : 'mercury'
    };

    api.getUserInfo(threadID, function(err, userRes) {
      if(err) {
        return callback(err);
      }
      var key = (Object.keys(userRes).length > 0) ? "user_ids" : "thread_fbids";
      form['threads['+key+'][0]'] = threadID;

      if(ctx.globalOptions.pageId) form.request_user_id = ctx.globalOptions.pageId;

      defaultFuncs.post("https://www.facebook.com/ajax/mercury/thread_info.php", ctx.jar, form)
        .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
        .then(function(resData) {
          if (resData.error) {
            throw resData;
          } else if (!resData.payload){
            throw {error: "Could not retrieve thread Info."};
          }

          var threadData = resData.payload.threads[0];
          var userData = userRes[threadID];
          var info = {
            participantIDs: threadData.participants.map(id => id.split(':').pop()),
            name: userData.name != null ? userData.name : threadData.name,
            snippet: threadData.snippet,
            messageCount: threadData.message_count,
            emoji: threadData.custom_like_icon,
            nicknames: threadData.custom_nickname,
            color: threadData.customColor,
          };
          callback(null, info);

        }).catch(function(err) {
          log.error("Error in getThreadInfo", err);
          return callback(err);
        });
    });

  };
};
<commit_msg>Woops #277<commit_after>"use strict";

var utils = require("../utils");
var log = require("npmlog");

module.exports = function(defaultFuncs, api, ctx) {
  return function getThreadInfo(threadID, callback) {
    if(!callback) callback = function() {};

    var form = {
      'client' : 'mercury'
    };

    api.getUserInfo(threadID, function(err, userRes) {
      if(err) {
        return callback(err);
      }
      var key = (Object.keys(userRes).length > 0) ? "user_ids" : "thread_fbids";
      form['threads['+key+'][0]'] = threadID;

      if(ctx.globalOptions.pageId) form.request_user_id = ctx.globalOptions.pageId;

      defaultFuncs.post("https://www.facebook.com/ajax/mercury/thread_info.php", ctx.jar, form)
        .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
        .then(function(resData) {
          if (resData.error) {
            throw resData;
          } else if (!resData.payload){
            throw {error: "Could not retrieve thread Info."};
          }

          var threadData = resData.payload.threads[0];
          var userData = userRes[threadID];
          var info = {
            participantIDs: threadData.participants.map(id => id.split(':').pop()),
            name: userData != null && userData.name != null ? userData.name : threadData.name,
            snippet: threadData.snippet,
            messageCount: threadData.message_count,
            emoji: threadData.custom_like_icon,
            nicknames: threadData.custom_nickname,
            color: threadData.customColor,
          };
          callback(null, info);

        }).catch(function(err) {
          log.error("Error in getThreadInfo", err);
          return callback(err);
        });
    });

  };
};
<|endoftext|>


<commit_before>/* global GLProgram */
// Thin GL program wrapp to cache uniform locations/values, do compile-time pre-processing
// (injecting #defines and #pragma transforms into shaders), etc.
import Utils from '../utils';
import {GL} from './gl';
import GLTexture from './gl_texture';
import Queue from 'queue-async';

GLProgram.id = 0; // assign each program a unique id
GLProgram.programs = {}; // programs, by id

export default function GLProgram (gl, vertex_shader, fragment_shader, options)
{
    options = options || {};

    this.gl = gl;
    this.program = null;
    this.compiled = false;
    this.compiling = false;
    this.defines = options.defines || {}; // key/values inserted as #defines into shaders at compile-time
    this.transforms = options.transforms || {}; // key/values for URLs of blocks that can be injected into shaders at compile-time
    this.compiling = false;
    this.defines = Object.assign({}, options.defines||{}); // key/values inserted as #defines into shaders at compile-time
    this.transforms = Object.assign({}, options.transforms||{}); // key/values for URLs of blocks that can be injected into shaders at compile-time
    this.uniforms = {}; // program locations of uniforms, set/updated at compile-time
    this.attribs = {}; // program locations of vertex attributes

    this.vertex_shader = vertex_shader;
    this.fragment_shader = fragment_shader;

    this.id = GLProgram.id++;
    GLProgram.programs[this.id] = this;
    this.name = options.name; // can provide a program name (useful for debugging)

    this.compile({resolve: options.resolve, reject: options.reject});
}

GLProgram.prototype.destroy = function () {
    this.gl.useProgram(null);
    this.gl.deleteProgram(this.program);
    this.program = null;
    this.uniforms = {};
    this.attribs = {};
    delete GLProgram.programs[this.id];
    this.compiled = false;
};

// Use program wrapper with simple state cache
GLProgram.prototype.use = function ()
{
    if (!this.compiled) {
        return;
    }

    if (GLProgram.current !== this) {
        this.gl.useProgram(this.program);
    }
    GLProgram.current = this;
};
GLProgram.current = null;

// Global config applied to all programs (duplicate properties for a specific program will take precedence)
GLProgram.defines = {};
GLProgram.transforms = {};

GLProgram.addTransform = function (key, ...transforms) {
    GLProgram.transforms[key] = GLProgram.transforms[key] || [];
    GLProgram.transforms[key].push(...transforms);
};

// Remove all global shader transforms for a given key
GLProgram.removeTransform = function (key) {
    GLProgram.transforms[key] = [];
};

GLProgram.prototype.compile = function ({resolve, reject}) {

    if (this.compiling) {
        reject(new Error(`GLProgram.compile(): skipping for ${this.id} (${this.name}) because already compiling`));
        return;
    }
    this.compiling = true;
    this.compiled = false;

    // Copy sources from pre-modified template
    this.computed_vertex_shader = this.vertex_shader;
    this.computed_fragment_shader = this.fragment_shader;

    // Make list of defines to be injected later
    var defines = this.buildDefineList();

    // Inject user-defined transforms (arbitrary code points matching named #pragmas)
    // Replace according to this pattern:
    // #pragma tangram: [key]
    // e.g. #pragma tangram: globals

    // TODO: flag to avoid re-retrieving transform URLs over network when rebuilding?
    // TODO: support glslify #pragma export names for better compatibility? (e.g. rename main() functions)
    // TODO: auto-insert uniforms referenced in mode definition, but not in shader base or transforms? (problem: don't have access to uniform list/type here)

    // Gather all transform code snippets (can be either inline in the style file, or over the network via URL)
    // This is an async process, since code may be retrieved remotely
    var transforms = this.buildShaderTransformList();
    var loaded_transforms = {}; // master list of transforms, with an ordered list for each (since we want to guarantee order of transforms)
    var regexp;
    var queue = Queue();

    for (var key in transforms) {
        var transform = transforms[key];
        if (transform == null) {
            continue;
        }

        // Each code point can be a single item (string or hash object) or a list (array object with non-zero length)
        if (!(typeof transform === 'object' && transform.length >= 0)) {
            transform = [transform];
        }

        // First find code replace points in shaders
        regexp = new RegExp('^\\s*#pragma\\s+tangram:\\s+' + key + '\\s*$', 'm');
        var inject_vertex = this.computed_vertex_shader.match(regexp);
        var inject_fragment = this.computed_fragment_shader.match(regexp);

        // Avoid network request if nothing to replace
        if (inject_vertex == null && inject_fragment == null) {
            continue;
        }

        // Collect all transforms for this type
        loaded_transforms[key] = {};
        loaded_transforms[key].regexp = new RegExp(regexp); // save regexp so we can inject later without having to recreate it
        loaded_transforms[key].inject_vertex = (inject_vertex != null); // save regexp code point matches so we don't have to do them again
        loaded_transforms[key].inject_fragment = (inject_fragment != null);
        loaded_transforms[key].list = [];

        // Get the code (possibly over the network, so needs to be async)
        for (var u=0; u < transform.length; u++) {
            queue.defer(GLProgram.loadTransform, loaded_transforms, transform[u], key, u);
        }

        // Add a #define for this injection point
        defines['TANGRAM_TRANSFORM_' + key.replace(' ', '_').toUpperCase()] = true;
    }

    // When all transform code snippets are collected, combine and inject them
    queue.await(error => {
        this.compiling = false;

        if (error) {
            reject(new Error(`GLProgram.compile(): skipping for ${this.id} (${this.name}) errored: ${error.message}`));
            return;
        }

        // Do the code injection with the collected sources
        for (var t in loaded_transforms) {
            // Concatenate
            var combined_source = "";
            for (var s=0; s < loaded_transforms[t].list.length; s++) {
                combined_source += loaded_transforms[t].list[s] + '\n';
            }

            // Inject
            if (loaded_transforms[t].inject_vertex != null) {
                this.computed_vertex_shader = this.computed_vertex_shader.replace(loaded_transforms[t].regexp, combined_source);
            }
            if (loaded_transforms[t].inject_fragment != null) {
                this.computed_fragment_shader = this.computed_fragment_shader.replace(loaded_transforms[t].regexp, combined_source);
            }
        }

        // Clean-up any #pragmas that weren't replaced (to prevent compiler warnings)
        var regexp = new RegExp('^\\s*#pragma\\s+tangram:\\s+\\w+\\s*$', 'gm');
        this.computed_vertex_shader = this.computed_vertex_shader.replace(regexp, '');
        this.computed_fragment_shader = this.computed_fragment_shader.replace(regexp, '');

        // Build & inject defines
        // This is done *after* code injection so that we can add defines for which code points were injected
        var define_str = GLProgram.buildDefineString(defines);
        this.computed_vertex_shader = define_str + this.computed_vertex_shader;
        this.computed_fragment_shader = define_str + this.computed_fragment_shader;

        // Include program info useful for debugging
        var info = (this.name ? (this.name + ' / id ' + this.id) : ('id ' + this.id));
        this.computed_vertex_shader = '// Program: ' + info + '\n' + this.computed_vertex_shader;
        this.computed_fragment_shader = '// Program: ' + info + '\n' + this.computed_fragment_shader;

        // Compile & set uniforms to cached values
        try {
            this.program = GL.updateProgram(this.gl, this.program, this.computed_vertex_shader, this.computed_fragment_shader);
            this.compiled = true;
        }
        catch (e) {
            this.program = null;
            this.compiled = false;
        }

        this.use();
        this.refreshUniforms();
        this.refreshAttributes();

        resolve();
    });
};

// Retrieve a single transform, for a given injection point, at a certain index (to preserve original order)
// Can be async, calls 'complete' callback when done
GLProgram.loadTransform = function (transforms, block, key, index, complete) {
    // Can be an inline block of GLSL, or a URL to retrieve GLSL block from
    // Inline code
    if (typeof block === 'string') {
        transforms[key].list[index] = block;
        complete();
    }
    // Remote code
    else if (typeof block === 'object' && block.url) {
        Utils.io(Utils.cacheBusterForUrl(block.url)).then((body) => {
            transforms[key].list[index] = body;
            complete(null);
        }, (error) => {
            complete(error);
        });
    }
};

// Make list of defines (global, then program-specific)
GLProgram.prototype.buildDefineList = function () {
    var d, defines = {};
    for (d in GLProgram.defines) {
        defines[d] = GLProgram.defines[d];
    }
    for (d in this.defines) {
        defines[d] = this.defines[d];
    }
    return defines;
};

// Make list of shader transforms (global, then program-specific)
GLProgram.prototype.buildShaderTransformList = function () {
    var d, transforms = {};
    for (d in GLProgram.transforms) {
        transforms[d] = [];

        if (typeof GLProgram.transforms[d] === 'object' && GLProgram.transforms[d].length >= 0) {
            transforms[d].push(...GLProgram.transforms[d]);
        }
        else {
            transforms[d] = [GLProgram.transforms[d]];
        }
    }
    for (d in this.transforms) {
        transforms[d] = transforms[d] || [];

        if (typeof this.transforms[d] === 'object' && this.transforms[d].length >= 0) {
            transforms[d].push(...this.transforms[d]);
        }
        else {
            transforms[d].push(this.transforms[d]);
        }
    }
    return transforms;
};

// Turn #defines into a combined string
GLProgram.buildDefineString = function (defines) {
    var define_str = "";
    for (var d in defines) {
        if (defines[d] === false) {
            continue;
        }
        else if (typeof defines[d] === 'boolean' && defines[d] === true) { // booleans are simple defines with no value
            define_str += "#define " + d + "\n";
        }
        else if (typeof defines[d] === 'number' && Math.floor(defines[d]) === defines[d]) { // int to float conversion to satisfy GLSL floats
            define_str += "#define " + d + " " + defines[d].toFixed(1) + "\n";
        }
        else { // any other float or string value
            define_str += "#define " + d + " " + defines[d] + "\n";
        }
    }
    return define_str;
};

// Set uniforms from a JS object, with inferred types
GLProgram.prototype.setUniforms = function (uniforms)
{
    if (!this.compiled) {
        return;
    }

    // TODO: only update uniforms when changed
    var texture_unit = 0;

    for (var u in uniforms) {
        var uniform = uniforms[u];

        // Single float
        if (typeof uniform === 'number') {
            this.uniform('1f', u, uniform);
        }
        // Multiple floats - vector or array
        else if (typeof uniform === 'object') {
            // float vectors (vec2, vec3, vec4)
            if (uniform.length >= 2 && uniform.length <= 4) {
                this.uniform(uniform.length + 'fv', u, uniform);
            }
            // float array
            else if (uniform.length > 4) {
                this.uniform('1fv', u + '[0]', uniform);
            }
            // TODO: assume matrix for (typeof == Float32Array && length == 16)?
        }
        // Boolean
        else if (typeof uniform === 'boolean') {
            this.uniform('1i', u, uniform);
        }
        // Texture
        else if (typeof uniform === 'string') {
            var texture = GLTexture.textures[uniform];
            if (texture == null) {
                texture = new GLTexture(this.gl, uniform);
                texture.load(uniform);
            }

            texture.bind(texture_unit);
            this.uniform('1i', u, texture_unit);
            texture_unit++;
        }
        // TODO: support other non-float types? (int, etc.)
    }
};

// ex: program.uniform('3f', 'position', x, y, z);
// TODO: only update uniforms when changed
GLProgram.prototype.uniform = function (method, name, ...values) // 'values' is a method-appropriate arguments list
{
    if (!this.compiled) {
        return;
    }

    var uniform = (this.uniforms[name] = this.uniforms[name] || {});
    uniform.name = name;
    uniform.location = uniform.location || this.gl.getUniformLocation(this.program, name);
    uniform.method = 'uniform' + method;
    uniform.values = values;
    this.updateUniform(name);
};

// Set a single uniform
GLProgram.prototype.updateUniform = function (name)
{
    if (!this.compiled) {
        return;
    }

    var uniform = this.uniforms[name];
    if (uniform == null || uniform.location == null) {
        return;
    }

    this.use();
    this.gl[uniform.method].apply(this.gl, [uniform.location].concat(uniform.values)); // call appropriate GL uniform method and pass through arguments
};

// Refresh uniform locations and set to last cached values
GLProgram.prototype.refreshUniforms = function ()
{
    if (!this.compiled) {
        return;
    }

    for (var u in this.uniforms) {
        this.uniforms[u].location = this.gl.getUniformLocation(this.program, u);
        this.updateUniform(u);
    }
};

GLProgram.prototype.refreshAttributes = function ()
{
    // var len = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_ATTRIBUTES);
    // for (var i=0; i < len; i++) {
    //     var a = this.gl.getActiveAttrib(this.program, i);
    // }
    this.attribs = {};
};

// Get the location of a vertex attribute
GLProgram.prototype.attribute = function (name)
{
    if (!this.compiled) {
        return;
    }

    var attrib = (this.attribs[name] = this.attribs[name] || {});
    if (attrib.location != null) {
        return attrib;
    }

    attrib.name = name;
    attrib.location = this.gl.getAttribLocation(this.program, name);

    // var info = this.gl.getActiveAttrib(this.program, attrib.location);
    // attrib.type = info.type;
    // attrib.size = info.size;

    return attrib;
};
<commit_msg>Remove queue from gl_program<commit_after>/* global GLProgram */
// Thin GL program wrapp to cache uniform locations/values, do compile-time pre-processing
// (injecting #defines and #pragma transforms into shaders), etc.
import Utils from '../utils';
import {GL} from './gl';
import GLTexture from './gl_texture';

GLProgram.id = 0; // assign each program a unique id
GLProgram.programs = {}; // programs, by id

export default function GLProgram (gl, vertex_shader, fragment_shader, options)
{
    options = options || {};

    this.gl = gl;
    this.program = null;
    this.compiled = false;
    this.compiling = false;
    this.defines = options.defines || {}; // key/values inserted as #defines into shaders at compile-time
    this.transforms = options.transforms || {}; // key/values for URLs of blocks that can be injected into shaders at compile-time
    this.compiling = false;
    this.defines = Object.assign({}, options.defines||{}); // key/values inserted as #defines into shaders at compile-time
    this.transforms = Object.assign({}, options.transforms||{}); // key/values for URLs of blocks that can be injected into shaders at compile-time
    this.uniforms = {}; // program locations of uniforms, set/updated at compile-time
    this.attribs = {}; // program locations of vertex attributes

    this.vertex_shader = vertex_shader;
    this.fragment_shader = fragment_shader;

    this.id = GLProgram.id++;
    GLProgram.programs[this.id] = this;
    this.name = options.name; // can provide a program name (useful for debugging)

    this.compile({resolve: options.resolve, reject: options.reject});
}

GLProgram.prototype.destroy = function () {
    this.gl.useProgram(null);
    this.gl.deleteProgram(this.program);
    this.program = null;
    this.uniforms = {};
    this.attribs = {};
    delete GLProgram.programs[this.id];
    this.compiled = false;
};

// Use program wrapper with simple state cache
GLProgram.prototype.use = function ()
{
    if (!this.compiled) {
        return;
    }

    if (GLProgram.current !== this) {
        this.gl.useProgram(this.program);
    }
    GLProgram.current = this;
};
GLProgram.current = null;

// Global config applied to all programs (duplicate properties for a specific program will take precedence)
GLProgram.defines = {};
GLProgram.transforms = {};

GLProgram.addTransform = function (key, ...transforms) {
    GLProgram.transforms[key] = GLProgram.transforms[key] || [];
    GLProgram.transforms[key].push(...transforms);
};

// Remove all global shader transforms for a given key
GLProgram.removeTransform = function (key) {
    GLProgram.transforms[key] = [];
};

GLProgram.prototype.compile = function ({resolve, reject}) {

    if (this.compiling) {
        reject(new Error(`GLProgram.compile(): skipping for ${this.id} (${this.name}) because already compiling`));
        return;
    }
    this.compiling = true;
    this.compiled = false;

    // Copy sources from pre-modified template
    this.computed_vertex_shader = this.vertex_shader;
    this.computed_fragment_shader = this.fragment_shader;

    // Make list of defines to be injected later
    var defines = this.buildDefineList();

    // Inject user-defined transforms (arbitrary code points matching named #pragmas)
    // Replace according to this pattern:
    // #pragma tangram: [key]
    // e.g. #pragma tangram: globals

    // TODO: flag to avoid re-retrieving transform URLs over network when rebuilding?
    // TODO: support glslify #pragma export names for better compatibility? (e.g. rename main() functions)
    // TODO: auto-insert uniforms referenced in mode definition, but not in shader base or transforms? (problem: don't have access to uniform list/type here)

    // Gather all transform code snippets (can be either inline in the style file, or over the network via URL)
    // This is an async process, since code may be retrieved remotely
    var transforms = this.buildShaderTransformList();
    var loaded_transforms = {}; // master list of transforms, with an ordered list for each (since we want to guarantee order of transforms)
    var regexp;
    var queue = [];

    for (var key in transforms) {
        var transform = transforms[key];
        if (transform == null) {
            continue;
        }

        // Each code point can be a single item (string or hash object) or a list (array object with non-zero length)
        if (!(typeof transform === 'object' && transform.length >= 0)) {
            transform = [transform];
        }

        // First find code replace points in shaders
        regexp = new RegExp('^\\s*#pragma\\s+tangram:\\s+' + key + '\\s*$', 'm');
        var inject_vertex = this.computed_vertex_shader.match(regexp);
        var inject_fragment = this.computed_fragment_shader.match(regexp);

        // Avoid network request if nothing to replace
        if (inject_vertex == null && inject_fragment == null) {
            continue;
        }

        // Collect all transforms for this type
        loaded_transforms[key] = {};
        loaded_transforms[key].regexp = new RegExp(regexp); // save regexp so we can inject later without having to recreate it
        loaded_transforms[key].inject_vertex = (inject_vertex != null); // save regexp code point matches so we don't have to do them again
        loaded_transforms[key].inject_fragment = (inject_fragment != null);
        loaded_transforms[key].list = [];

        // Get the code (possibly over the network, so needs to be async)
        for (var u=0; u < transform.length; u++) {
            queue.push(new Promise((resolve, reject) => {
                GLProgram.loadTransform(loaded_transforms, transform[u], key, u, resolve, reject);
            }));
        }

        // Add a #define for this injection point
        defines['TANGRAM_TRANSFORM_' + key.replace(' ', '_').toUpperCase()] = true;
    }

    Promise.all(queue).then(() => {
        this.compiling = false;

        // Do the code injection with the collected sources
        for (var t in loaded_transforms) {
            // Concatenate
            var combined_source = "";
            for (var s=0; s < loaded_transforms[t].list.length; s++) {
                combined_source += loaded_transforms[t].list[s] + '\n';
            }

            // Inject
            if (loaded_transforms[t].inject_vertex != null) {
                this.computed_vertex_shader = this.computed_vertex_shader.replace(loaded_transforms[t].regexp, combined_source);
            }
            if (loaded_transforms[t].inject_fragment != null) {
                this.computed_fragment_shader = this.computed_fragment_shader.replace(loaded_transforms[t].regexp, combined_source);
            }
        }

        // Clean-up any #pragmas that weren't replaced (to prevent compiler warnings)
        var regexp = new RegExp('^\\s*#pragma\\s+tangram:\\s+\\w+\\s*$', 'gm');
        this.computed_vertex_shader = this.computed_vertex_shader.replace(regexp, '');
        this.computed_fragment_shader = this.computed_fragment_shader.replace(regexp, '');

        // Build & inject defines
        // This is done *after* code injection so that we can add defines for which code points were injected
        var define_str = GLProgram.buildDefineString(defines);
        this.computed_vertex_shader = define_str + this.computed_vertex_shader;
        this.computed_fragment_shader = define_str + this.computed_fragment_shader;

        // Include program info useful for debugging
        var info = (this.name ? (this.name + ' / id ' + this.id) : ('id ' + this.id));
        this.computed_vertex_shader = '// Program: ' + info + '\n' + this.computed_vertex_shader;
        this.computed_fragment_shader = '// Program: ' + info + '\n' + this.computed_fragment_shader;

        // Compile & set uniforms to cached values
        try {
            this.program = GL.updateProgram(this.gl, this.program, this.computed_vertex_shader, this.computed_fragment_shader);
            this.compiled = true;
        }
        catch (e) {
            this.program = null;
            this.compiled = false;
        }

        this.use();
        this.refreshUniforms();
        this.refreshAttributes();

        resolve();
    }, (error) => {
        reject(new Error(`GLProgram.compile(): skipping for ${this.id} (${this.name}) errored: ${error.message}`));
    });

};

// Retrieve a single transform, for a given injection point, at a certain index (to preserve original order)
// Can be async, calls 'complete' callback when done
GLProgram.loadTransform = function (transforms, block, key, index, resolve, reject) {
    // Can be an inline block of GLSL, or a URL to retrieve GLSL block from
    // Inline code
    if (typeof block === 'string') {
        transforms[key].list[index] = block;
        resolve();
    }
    // Remote code
    else if (typeof block === 'object' && block.url) {
        Utils.io(Utils.cacheBusterForUrl(block.url)).then((body) => {
            transforms[key].list[index] = body;
            resolve();
        }, (error) => {
            reject(error);
        });
    }
};

// Make list of defines (global, then program-specific)
GLProgram.prototype.buildDefineList = function () {
    var d, defines = {};
    for (d in GLProgram.defines) {
        defines[d] = GLProgram.defines[d];
    }
    for (d in this.defines) {
        defines[d] = this.defines[d];
    }
    return defines;
};

// Make list of shader transforms (global, then program-specific)
GLProgram.prototype.buildShaderTransformList = function () {
    var d, transforms = {};
    for (d in GLProgram.transforms) {
        transforms[d] = [];

        if (typeof GLProgram.transforms[d] === 'object' && GLProgram.transforms[d].length >= 0) {
            transforms[d].push(...GLProgram.transforms[d]);
        }
        else {
            transforms[d] = [GLProgram.transforms[d]];
        }
    }
    for (d in this.transforms) {
        transforms[d] = transforms[d] || [];

        if (typeof this.transforms[d] === 'object' && this.transforms[d].length >= 0) {
            transforms[d].push(...this.transforms[d]);
        }
        else {
            transforms[d].push(this.transforms[d]);
        }
    }
    return transforms;
};

// Turn #defines into a combined string
GLProgram.buildDefineString = function (defines) {
    var define_str = "";
    for (var d in defines) {
        if (defines[d] === false) {
            continue;
        }
        else if (typeof defines[d] === 'boolean' && defines[d] === true) { // booleans are simple defines with no value
            define_str += "#define " + d + "\n";
        }
        else if (typeof defines[d] === 'number' && Math.floor(defines[d]) === defines[d]) { // int to float conversion to satisfy GLSL floats
            define_str += "#define " + d + " " + defines[d].toFixed(1) + "\n";
        }
        else { // any other float or string value
            define_str += "#define " + d + " " + defines[d] + "\n";
        }
    }
    return define_str;
};

// Set uniforms from a JS object, with inferred types
GLProgram.prototype.setUniforms = function (uniforms)
{
    if (!this.compiled) {
        return;
    }

    // TODO: only update uniforms when changed
    var texture_unit = 0;

    for (var u in uniforms) {
        var uniform = uniforms[u];

        // Single float
        if (typeof uniform === 'number') {
            this.uniform('1f', u, uniform);
        }
        // Multiple floats - vector or array
        else if (typeof uniform === 'object') {
            // float vectors (vec2, vec3, vec4)
            if (uniform.length >= 2 && uniform.length <= 4) {
                this.uniform(uniform.length + 'fv', u, uniform);
            }
            // float array
            else if (uniform.length > 4) {
                this.uniform('1fv', u + '[0]', uniform);
            }
            // TODO: assume matrix for (typeof == Float32Array && length == 16)?
        }
        // Boolean
        else if (typeof uniform === 'boolean') {
            this.uniform('1i', u, uniform);
        }
        // Texture
        else if (typeof uniform === 'string') {
            var texture = GLTexture.textures[uniform];
            if (texture == null) {
                texture = new GLTexture(this.gl, uniform);
                texture.load(uniform);
            }

            texture.bind(texture_unit);
            this.uniform('1i', u, texture_unit);
            texture_unit++;
        }
        // TODO: support other non-float types? (int, etc.)
    }
};

// ex: program.uniform('3f', 'position', x, y, z);
// TODO: only update uniforms when changed
GLProgram.prototype.uniform = function (method, name, ...values) // 'values' is a method-appropriate arguments list
{
    if (!this.compiled) {
        return;
    }

    var uniform = (this.uniforms[name] = this.uniforms[name] || {});
    uniform.name = name;
    uniform.location = uniform.location || this.gl.getUniformLocation(this.program, name);
    uniform.method = 'uniform' + method;
    uniform.values = values;
    this.updateUniform(name);
};

// Set a single uniform
GLProgram.prototype.updateUniform = function (name)
{
    if (!this.compiled) {
        return;
    }

    var uniform = this.uniforms[name];
    if (uniform == null || uniform.location == null) {
        return;
    }

    this.use();
    this.gl[uniform.method].apply(this.gl, [uniform.location].concat(uniform.values)); // call appropriate GL uniform method and pass through arguments
};

// Refresh uniform locations and set to last cached values
GLProgram.prototype.refreshUniforms = function ()
{
    if (!this.compiled) {
        return;
    }

    for (var u in this.uniforms) {
        this.uniforms[u].location = this.gl.getUniformLocation(this.program, u);
        this.updateUniform(u);
    }
};

GLProgram.prototype.refreshAttributes = function ()
{
    // var len = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_ATTRIBUTES);
    // for (var i=0; i < len; i++) {
    //     var a = this.gl.getActiveAttrib(this.program, i);
    // }
    this.attribs = {};
};

// Get the location of a vertex attribute
GLProgram.prototype.attribute = function (name)
{
    if (!this.compiled) {
        return;
    }

    var attrib = (this.attribs[name] = this.attribs[name] || {});
    if (attrib.location != null) {
        return attrib;
    }

    attrib.name = name;
    attrib.location = this.gl.getAttribLocation(this.program, name);

    // var info = this.gl.getActiveAttrib(this.program, attrib.location);
    // attrib.type = info.type;
    // attrib.size = info.size;

    return attrib;
};
<|endoftext|>