<commit_before>'use babel';

import { CompositeDisposable } from 'atom';
import { AtomAutocompleteProvider } from './AtomAutocompleteProvider';
import { JavaClassLoader } from './JavaClassLoader';
import atomJavaUtil from './atomJavaUtil';
import ioUtil from './ioUtil';
const fs = require('fs');

class AtomAutocompletePackage {

  constructor() {
    this.config = require('./config.json');
    this.subscriptions = undefined;
    this.provider = undefined;
    this.classLoader = undefined;
    this.classpath = '.';
    this.initialized = false;
  }

  activate() {
    this.classLoader = new JavaClassLoader();
    this.provider = new AtomAutocompleteProvider(this.classLoader);
    this.subscriptions = new CompositeDisposable();

    // Listen for commands
    this.subscriptions.add(
      atom.commands.add('atom-workspace', 'autocomplete-java:organize-imports',
      () => {
        this._organizeImports();
      })
    );
    this.subscriptions.add(
      atom.commands.add('atom-workspace', 'autocomplete-java:refresh-project',
      () => {
        if (this.initialized) {
          this._refresh(false);
        }
      })
    );
    this.subscriptions.add(
      atom.commands.add('atom-workspace', 'autocomplete-java:full-refresh',
      () => {
        if (this.initialized) {
          this._refresh(true);
        }
      })
    );

    // Listen for save
    atom.workspace.observeTextEditors(editor => {
      this.subscriptions.add(editor.getBuffer().onWillSave(() => {
        this._onSave(editor);
      }));
    });

    // Start full refresh
    setTimeout(() => {
      // Refresh all classes
      this.initialized = true;
      this._refresh(true);
    }, 300);
  }

  deactivate() {
    this.subscriptions.dispose();
    this.provider = null;
    this.classLoader = null;
    this.subscriptions = null;
    this.classpath = '.';
    this.initialized = false;
  }

  getProvider() {
    return this.provider;
  }

  // Commands

  _refresh(fullRefresh) {
    // Refresh provider settings
    // TODO observe config changes
    this.provider.configure(atom.config.get('autocomplete-java'));

    // Load classes using classpath read from .classpath file
    const rootDir = this._getProjectRootDir();
    if (rootDir) {
      return ioUtil.readFile(rootDir + '/' +
        atom.config.get('autocomplete-java.classpathFilePath'), 'utf8')
      .then(classpath => {
        // Convert classpath to Windows format on Windows
        this.classpath = this._formatClassPath(rootDir, classpath);
        this.classLoader.loadClasses(this.classpath,
          atom.config.get('autocomplete-java.loadClassMembers'), fullRefresh);
      });
    }
  }

  _refreshClass(className, delayMillis) {
    setTimeout(() => {
      return this.classLoader.loadClass(className, this.classpath,
        atom.config.get('autocomplete-java.loadClassMembers'));
    }, delayMillis);
  }

  _organizeImports() {
    const editor = atom.workspace.getActiveTextEditor();
    if (this._isJavaFile(editor)) {
      atomJavaUtil.organizeImports(editor);
    }
  }

  _onSave(editor) {
    if (atom.config.get('autocomplete-java.organizeImportsOnSave')) {
      this._organizeImports();
    }

    // Refresh saved class after it has been compiled
    // TODO
    const fileMatch = editor.getPath().match(/\/([^\/]*)\.java/);
    const packageMatch = editor.getText().match(/package\s(.*);/);
    if (fileMatch && packageMatch) {
      // TODO use file watcher instead of hardcoded timeout
      const className = packageMatch[1] + '.' + fileMatch[1];
      this._refreshClass(className, 3000);
    }
  }

  // Util methods

  _isJavaFile(editor) {
    return editor.getPath().match(/\.java$/);
  }

  _formatClassPath(rootDir, classpath) {
    // Remove newlines
    let cp = classpath.replace(/[\r\n]*/gm, '');
    // Convert classpath to Windows format on Windows
    cp = rootDir.indexOf(':') !== -1 ? cp.replace(/\:/g, ';') : cp;
    // Add rootdir to paths
    cp = cp.replace(/\./g, rootDir + '/.').trim();
    // Just to make it nicer
    return cp.replace(/[//\\]\.[//\\]/g, '/');
  }

  _getProjectRootDir() {
    let dir = null;
    const textEditor = atom.workspace.getActiveTextEditor();
    if (textEditor) {
      dir = atom.project.getPaths().sort((a, b) => {
        return (b.length - a.length);
      }).find((p) => {
        const realpath = fs.realpathSync(p);
        return textEditor.getPath().substr(0, realpath.length) === realpath;
      });
    }
    return dir;
  }

}

export default new AtomAutocompletePackage();
<commit_msg>classpath fix<commit_after>'use babel';

import { CompositeDisposable } from 'atom';
import { AtomAutocompleteProvider } from './AtomAutocompleteProvider';
import { JavaClassLoader } from './JavaClassLoader';
import atomJavaUtil from './atomJavaUtil';
import ioUtil from './ioUtil';
const fs = require('fs');

class AtomAutocompletePackage {

  constructor() {
    this.config = require('./config.json');
    this.subscriptions = undefined;
    this.provider = undefined;
    this.classLoader = undefined;
    this.classpath = '.';
    this.initialized = false;
  }

  activate() {
    this.classLoader = new JavaClassLoader();
    this.provider = new AtomAutocompleteProvider(this.classLoader);
    this.subscriptions = new CompositeDisposable();

    // Listen for commands
    this.subscriptions.add(
      atom.commands.add('atom-workspace', 'autocomplete-java:organize-imports',
      () => {
        this._organizeImports();
      })
    );
    this.subscriptions.add(
      atom.commands.add('atom-workspace', 'autocomplete-java:refresh-project',
      () => {
        if (this.initialized) {
          this._refresh(false);
        }
      })
    );
    this.subscriptions.add(
      atom.commands.add('atom-workspace', 'autocomplete-java:full-refresh',
      () => {
        if (this.initialized) {
          this._refresh(true);
        }
      })
    );

    // Listen for save
    atom.workspace.observeTextEditors(editor => {
      this.subscriptions.add(editor.getBuffer().onWillSave(() => {
        this._onSave(editor);
      }));
    });

    // Start full refresh
    setTimeout(() => {
      // Refresh all classes
      this.initialized = true;
      this._refresh(true);
    }, 300);
  }

  deactivate() {
    this.subscriptions.dispose();
    this.provider = null;
    this.classLoader = null;
    this.subscriptions = null;
    this.classpath = '.';
    this.initialized = false;
  }

  getProvider() {
    return this.provider;
  }

  // Commands

  _refresh(fullRefresh) {
    // Refresh provider settings
    // TODO observe config changes
    this.provider.configure(atom.config.get('autocomplete-java'));

    // Load classes using classpath read from .classpath file
    const rootDir = this._getProjectRootDir();
    if (rootDir) {
      return ioUtil.readFile(rootDir + '/' +
        atom.config.get('autocomplete-java.classpathFilePath'), 'utf8')
      .then(classpath => {
        // Convert classpath to Windows format on Windows
        this.classpath = this._formatClassPath(rootDir, classpath);
        this.classLoader.loadClasses(this.classpath,
          atom.config.get('autocomplete-java.loadClassMembers'), fullRefresh);
      });
    }
  }

  _refreshClass(className, delayMillis) {
    setTimeout(() => {
      return this.classLoader.loadClass(className, this.classpath,
        atom.config.get('autocomplete-java.loadClassMembers'));
    }, delayMillis);
  }

  _organizeImports() {
    const editor = atom.workspace.getActiveTextEditor();
    if (this._isJavaFile(editor)) {
      atomJavaUtil.organizeImports(editor);
    }
  }

  _onSave(editor) {
    if (atom.config.get('autocomplete-java.organizeImportsOnSave')) {
      this._organizeImports();
    }

    // Refresh saved class after it has been compiled
    // TODO
    const fileMatch = editor.getPath().match(/\/([^\/]*)\.java/);
    const packageMatch = editor.getText().match(/package\s(.*);/);
    if (fileMatch && packageMatch) {
      // TODO use file watcher instead of hardcoded timeout
      const className = packageMatch[1] + '.' + fileMatch[1];
      this._refreshClass(className, 3000);
    }
  }

  // Util methods

  _isJavaFile(editor) {
    return editor.getPath().match(/\.java$/);
  }

  _formatClassPath(rootDir, classpath) {
    // Remove newlines
    let cp = classpath.replace(/[\r\n]*/gm, '').trim();
    // On Windows, convert classpath so that linux formatted .classpath file
    // works also in Windows without modifications
    const separator = rootDir.indexOf(':') !== -1 ? ';' : ':';
    if (separator === ';') {
      // Replace all path separators, but avoid touching Windows style
      // drive letters like C:\
      cp = cp.replace(/(\:)([^\\])/g, ';$2');
    }
    // Add rootdir to paths like :./ or :../
    cp = (':' + cp).replace(/([\:\;])(\.)/g, '$1' + rootDir + '/.')
      .substring(1);
    // Remove /./ just to make path nicer looking
    return cp.replace(/[//\\]\.[//\\]/g, '/');
  }

  _getProjectRootDir() {
    let dir = null;
    const textEditor = atom.workspace.getActiveTextEditor();
    if (textEditor) {
      dir = atom.project.getPaths().sort((a, b) => {
        return (b.length - a.length);
      }).find((p) => {
        const realpath = fs.realpathSync(p);
        return textEditor.getPath().substr(0, realpath.length) === realpath;
      });
    }
    return dir;
  }

}

export default new AtomAutocompletePackage();
<|endoftext|>


<commit_before>
internal = {}

exports.run = function run(flow, data) {

    if(flow.flags.hxml) {
        console.log(flow.project.hxml);
        return;
    }

    if(flow.flags.project_prepared) {
        console.log( JSON.stringify(flow.project.prepared.source, null, '  ') );
        return;
    }

    var result = {
        hxml : internal.escape_json(flow.project.hxml),
        paths : flow.project.paths,
        targets_known : flow.config.build.known_targets,
        targets_invalid : flow.config.build.invalid_targets[flow.system],
    }

    console.log(JSON.stringify(result, null, '  '));

} //run

exports.verify = function verify(flow, done) {

    flow.project.do_prepare(flow);

    done(null,null);

} //verify

exports.error = function(flow, err) {

} //error

internal.escape_json = function(str) {
   return str
        .replace(/[\"]/g, '\\\"')
        .replace(/[\n]/g, '\\n')
        .replace(/[\t]/g, '\\t')
}<commit_msg>info; don't try to escape invalid strings<commit_after>
internal = {}

exports.run = function run(flow, data) {

    if(flow.flags.hxml) {
        console.log(flow.project.hxml);
        return;
    }

    if(flow.flags.project_prepared) {
        console.log( JSON.stringify(flow.project.prepared.source, null, '  ') );
        return;
    }

    var result = {
        hxml : internal.escape_json(flow.project.hxml),
        paths : flow.project.paths,
        targets_known : flow.config.build.known_targets,
        targets_invalid : flow.config.build.invalid_targets[flow.system],
    }

    console.log(JSON.stringify(result, null, '  '));

} //run

exports.verify = function verify(flow, done) {

    flow.project.do_prepare(flow);

    done(null,null);

} //verify

exports.error = function(flow, err) {

} //error

internal.escape_json = function(str) {
    if(!str) return str;
    return str
        .replace(/[\"]/g, '\\\"')
        .replace(/[\n]/g, '\\n')
        .replace(/[\t]/g, '\\t')
}<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.dig',
  name: 'DIG',
  extends: 'foam.nanos.http.DefaultHttpParameters',

  documentation: 'Data Integration Gateway - Perform DAO operations against a web service',

  requires: [
    'foam.net.web.HTTPRequest'
  ],

  implements: [
    'foam.mlang.Expressions',
    'foam.nanos.auth.CreatedAware',
    'foam.nanos.auth.CreatedByAware',
    'foam.nanos.auth.LastModifiedAware',
    'foam.nanos.auth.LastModifiedByAware'
  ],

  tableColumns: [
    'id',
    'daoKey',
    'cmd',
    'format'
  ],

  searchColumns: [],

  constants: [
    {
      name: 'MAX_URL_SIZE',
      value: 2000,
      type: 'Integer'
    }
  ],

  imports: [
    'AuthenticatedNSpecDAO'
  ],

  sections: [
    {
      name: 'details'
    },
    {
      name: 'supportDetails'
    },
    {
      name: 'body'
    },
    {
      name: '_defaultSection',
      permissionRequired: true
    }
  ],

  properties: [
    {
      name: 'id',
      label: 'Request Name',
      section: 'details'
    },
    {
      class: 'Reference',
      of: 'foam.nanos.auth.User',
      name: 'createdBy',
      section: 'supportDetails'
    },
    {
      class: 'Reference',
      of: 'foam.nanos.auth.User',
      name: 'createdByAgent',
      section: 'supportDetails'
    },
    {
      class: 'DateTime',
      name: 'created',
      documentation: 'The date and time of when the User was created in the system.',
      createVisibility: 'HIDDEN',
      updateVisibility: 'RO',
      section: 'supportDetails',
      includeInDigest: true
    },
    {
      class: 'DateTime',
      name: 'lastModified',
      documentation: 'The date and time the User was last modified.',
      createVisibility: 'HIDDEN',
      updateVisibility: 'RO',
      section: 'supportDetails',
      storageOptional: true
    },
    {
      class: 'Reference',
      of: 'foam.nanos.auth.User',
      name: 'lastModifiedBy',
      section: 'supportDetails',
      readPermissionRequired: true,
      writePermissionRequired: true,
      storageOptional: true
    },
    {
      class: 'Reference',
      of: 'foam.nanos.boot.NSpec',
      label: 'Data Access Object (DAO)',
      name: 'daoKey',
      documentation: `The DAO in the DIG request.`,
      view: function(_, X) {
        var E = foam.mlang.Expressions.create();
        return {
          class: 'foam.u2.view.RichChoiceView',
          search: true,
          sections: [
            {
              heading: 'DAO',
              dao: X.AuthenticatedNSpecDAO
                .where(E.AND(
                  E.EQ(foam.nanos.boot.NSpec.SERVE, true),
                  E.ENDS_WITH(foam.nanos.boot.NSpec.ID, 'DAO')
                ))
                .orderBy(foam.nanos.boot.NSpec.ID)
            }
          ]
        };
      },
      value: 'userDAO',
      section: 'details'
    },
    {
      name: 'cmd',
      label: 'API Command',
      section: 'details'
    },
    {
      name: 'format',
      label: 'Data Format',
      section: 'details',
      visibility: function(cmd) {
        return ( cmd == 'SELECT' || cmd == 'PUT' ) ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
    },
    {
      class: 'String',
      name: 'key',
      label: 'Object ID',
      section: 'details',
      visibility: function(cmd) {
        return ( cmd == 'SELECT' || cmd == 'REMOVE' ) ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
  },
  {
      class: 'String',
      name: 'q',
      label: 'Select Query',
      section: 'details',
      visibility: function(cmd) {
        return (cmd == 'SELECT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
    },
    {
      class: 'Long',
      name: 'limit',
      section: 'details',
      visibility: function(cmd) {
        return (cmd == 'SELECT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      },
      value: 1000,
      max: 1000,
      min: 0
    },
    {
      class: 'Long',
      name: 'skip',
      section: 'details',
      visibility: function(cmd) {
        return (cmd == 'SELECT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      },
      min: 0
    },
    {
      name: 'data',
      section: 'body',
      visibility: function(cmd) {
        return (cmd == 'PUT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
    },
    {
      class: 'URL',
      name: 'postURL',
      hidden: true
    },
    {
      name: 'snippet',
      label: 'Snippet',
      documentation: 'show a specific type of request would look like in a given language.',
      section: 'details',
      view: { class: 'foam.nanos.dig.DigSnippetView' },
      expression: function(key, data, daoKey, cmd, format, q, limit, skip) {
        var query = false;
        var url = "/service/dig";

        if ( daoKey ) {
          url += "?";
          query = true;
          url += "dao=" + daoKey;
        }
        if ( cmd ) {
          url += query ? "&" : "?";
          query = true;
          url += "cmd=" + cmd.name.toLowerCase();
        }
        if ( format ) {
          url += query ? "&" : "?";
          query = true;
          url += "format=" + format.name.toLowerCase();
        }
        if ( key ) {
          url += query ? "&" : "?";
          query = true;
          url += "id=" + key;
        }
        if ( q ) {
          url += query ? "&" : "?";
          query = true;
          url += "q=" + encodeURIComponent(q);
        }
        if ( limit > 0 && limit != Number.MAX_SAFE_INTEGER && limit != 1000 ) {
          url += query ? "&" : "?";
          query = true;
          url += "limit=" + limit;
        }
        if ( skip > 0 && skip != Number.MAX_SAFE_INTEGER ) {
          url += query ? "&" : "?";
          query = true;
          url += "skip=" + skip;
        }
        this.postURL = url;

        if ( data ) {
          if ( data.length + url.length < this.MAX_URL_SIZE ) {
            url += query ? "&" : "?";
            query = true;
            url += "data=" + encodeURIComponent(data);
          }
        }

        return url;
      }
    },
    {
      class: 'String',
      name: 'result',
      value: 'No Request Sent Yet.',
      view: { class: 'foam.nanos.dig.ResultView' },
      section: 'details',
      visibility: 'RO'
    }
  ],

  actions: [
    {
      name: 'postButton',
      label: 'Send Request',
      code: async function() {
        var req = this.HTTPRequest.create({
          url: window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + this.postURL + "&sessionId=" + localStorage.defaultSession,
          method: 'POST',
          payload: this.data,
        }).send();

        var resp = await req.then(async function(resp) {
          var temp = await resp.payload.then(function(result) {
            return result;
          });
          return temp;
        }, async function(error) {
          var temp = await error.payload.then(function(result) {
            return result;
          });
          return temp;
        });
        this.result = resp;
      }
    }
  ]
});
<commit_msg>Moved body section above support details<commit_after>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.dig',
  name: 'DIG',
  extends: 'foam.nanos.http.DefaultHttpParameters',

  documentation: 'Data Integration Gateway - Perform DAO operations against a web service',

  requires: [
    'foam.net.web.HTTPRequest'
  ],

  implements: [
    'foam.mlang.Expressions',
    'foam.nanos.auth.CreatedAware',
    'foam.nanos.auth.CreatedByAware',
    'foam.nanos.auth.LastModifiedAware',
    'foam.nanos.auth.LastModifiedByAware'
  ],

  tableColumns: [
    'id',
    'daoKey',
    'cmd',
    'format'
  ],

  searchColumns: [],

  constants: [
    {
      name: 'MAX_URL_SIZE',
      value: 2000,
      type: 'Integer'
    }
  ],

  imports: [
    'AuthenticatedNSpecDAO'
  ],

  sections: [
    {
      name: 'details'
    },
    {
      name: 'body'
    },
    {
      name: 'supportDetails'
    },
    {
      name: '_defaultSection',
      permissionRequired: true
    }
  ],

  properties: [
    {
      name: 'id',
      label: 'Request Name',
      section: 'details'
    },
    {
      class: 'Reference',
      of: 'foam.nanos.auth.User',
      name: 'createdBy',
      section: 'supportDetails'
    },
    {
      class: 'Reference',
      of: 'foam.nanos.auth.User',
      name: 'createdByAgent',
      section: 'supportDetails'
    },
    {
      class: 'DateTime',
      name: 'created',
      documentation: 'The date and time of when the User was created in the system.',
      createVisibility: 'HIDDEN',
      updateVisibility: 'RO',
      section: 'supportDetails',
      includeInDigest: true
    },
    {
      class: 'DateTime',
      name: 'lastModified',
      documentation: 'The date and time the User was last modified.',
      createVisibility: 'HIDDEN',
      updateVisibility: 'RO',
      section: 'supportDetails',
      storageOptional: true
    },
    {
      class: 'Reference',
      of: 'foam.nanos.auth.User',
      name: 'lastModifiedBy',
      section: 'supportDetails',
      readPermissionRequired: true,
      writePermissionRequired: true,
      storageOptional: true
    },
    {
      class: 'Reference',
      of: 'foam.nanos.boot.NSpec',
      label: 'Data Access Object (DAO)',
      name: 'daoKey',
      documentation: `The DAO in the DIG request.`,
      view: function(_, X) {
        var E = foam.mlang.Expressions.create();
        return {
          class: 'foam.u2.view.RichChoiceView',
          search: true,
          sections: [
            {
              heading: 'DAO',
              dao: X.AuthenticatedNSpecDAO
                .where(E.AND(
                  E.EQ(foam.nanos.boot.NSpec.SERVE, true),
                  E.ENDS_WITH(foam.nanos.boot.NSpec.ID, 'DAO')
                ))
                .orderBy(foam.nanos.boot.NSpec.ID)
            }
          ]
        };
      },
      value: 'userDAO',
      section: 'details'
    },
    {
      name: 'cmd',
      label: 'API Command',
      section: 'details'
    },
    {
      name: 'format',
      label: 'Data Format',
      section: 'details',
      visibility: function(cmd) {
        return ( cmd == 'SELECT' || cmd == 'PUT' ) ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
    },
    {
      class: 'String',
      name: 'key',
      label: 'Object ID',
      section: 'details',
      visibility: function(cmd) {
        return ( cmd == 'SELECT' || cmd == 'REMOVE' ) ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
  },
  {
      class: 'String',
      name: 'q',
      label: 'Select Query',
      section: 'details',
      visibility: function(cmd) {
        return (cmd == 'SELECT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
    },
    {
      class: 'Long',
      name: 'limit',
      section: 'details',
      visibility: function(cmd) {
        return (cmd == 'SELECT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      },
      value: 1000,
      max: 1000,
      min: 0
    },
    {
      class: 'Long',
      name: 'skip',
      section: 'details',
      visibility: function(cmd) {
        return (cmd == 'SELECT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      },
      min: 0
    },
    {
      name: 'data',
      section: 'body',
      visibility: function(cmd) {
        return (cmd == 'PUT') ? foam.u2.DisplayMode.RW : foam.u2.DisplayMode.HIDDEN;
      }
    },
    {
      class: 'URL',
      name: 'postURL',
      hidden: true
    },
    {
      name: 'snippet',
      label: 'Snippet',
      documentation: 'show a specific type of request would look like in a given language.',
      section: 'details',
      view: { class: 'foam.nanos.dig.DigSnippetView' },
      expression: function(key, data, daoKey, cmd, format, q, limit, skip) {
        var query = false;
        var url = "/service/dig";

        if ( daoKey ) {
          url += "?";
          query = true;
          url += "dao=" + daoKey;
        }
        if ( cmd ) {
          url += query ? "&" : "?";
          query = true;
          url += "cmd=" + cmd.name.toLowerCase();
        }
        if ( format ) {
          url += query ? "&" : "?";
          query = true;
          url += "format=" + format.name.toLowerCase();
        }
        if ( key ) {
          url += query ? "&" : "?";
          query = true;
          url += "id=" + key;
        }
        if ( q ) {
          url += query ? "&" : "?";
          query = true;
          url += "q=" + encodeURIComponent(q);
        }
        if ( limit > 0 && limit != Number.MAX_SAFE_INTEGER && limit != 1000 ) {
          url += query ? "&" : "?";
          query = true;
          url += "limit=" + limit;
        }
        if ( skip > 0 && skip != Number.MAX_SAFE_INTEGER ) {
          url += query ? "&" : "?";
          query = true;
          url += "skip=" + skip;
        }
        this.postURL = url;

        if ( data ) {
          if ( data.length + url.length < this.MAX_URL_SIZE ) {
            url += query ? "&" : "?";
            query = true;
            url += "data=" + encodeURIComponent(data);
          }
        }

        return url;
      }
    },
    {
      class: 'String',
      name: 'result',
      value: 'No Request Sent Yet.',
      view: { class: 'foam.nanos.dig.ResultView' },
      section: 'details',
      visibility: 'RO'
    }
  ],

  actions: [
    {
      name: 'postButton',
      label: 'Send Request',
      code: async function() {
        var req = this.HTTPRequest.create({
          url: window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + this.postURL + "&sessionId=" + localStorage.defaultSession,
          method: 'POST',
          payload: this.data,
        }).send();

        var resp = await req.then(async function(resp) {
          var temp = await resp.payload.then(function(result) {
            return result;
          });
          return temp;
        }, async function(error) {
          var temp = await error.payload.then(function(result) {
            return result;
          });
          return temp;
        });
        this.result = resp;
      }
    }
  ]
});
<|endoftext|>


<commit_before>var channel = require('cordova/channel'),
    utils = require('cordova/utils');

// Tell cordova channel to wait on the CordovaInfoReady event
channel.waitForInitialization('onCordovaInfoReady');

function Device() {
    this.platform = null;
    this.version = null;
    this.name = null;
    this.uuid = null;
    this.cordova = null;

    var me = this;

    channel.onCordovaReady.subscribeOnce(function() {
       me.getDeviceInfo(function (device) {
           me.platform = device.platform;
           me.version  = device.version;
           me.name     = device.name;
           me.uuid     = device.uuid;
           me.cordova  = device.cordova;

           channel.onCordovaInfoReady.fire();
       },
       function (e) {
           me.available = false;
           utils.alert("error initializing cordova: " + e);
       });
    });
}

Device.prototype.getDeviceInfo = function(success, fail, args) {
   var info = deviceapis.devicestatus;
   var properties = ["name", "uuid", "os_name", "os_vendor", "os_version"];

   var me = this;

   var name = null,
       platform = null,
       uuid = null,
       os_name = null,
       os_version = null,
       os_vendor = null;

   var checkProperties = function() {
       properties.pop();
       if(properties.length === 0) {
           me.name = name;
           me.platform = os_vendor + " " + os_name;
           me.version = os_version;
           me.uuid = uuid;
           me.cordova = "2.0.0";
           success(me);
       }
   };

   info.getPropertyValue(function(value) {
           //console.log("Device IMEI: "+value);
           uuid = value;
           checkProperties();
           }, fail, {aspect: "Device", property: "imei"});
   info.getPropertyValue(function(value) {
           //console.log("Device name: "+value);
           name = value;
           checkProperties();
           }, fail, {aspect: "Device", property: "version"});
   info.getPropertyValue(function(value) {
           //console.log("OperatingSystem name: "+value);
           os_name = value;
           checkProperties();
           }, fail, {aspect: "OperatingSystem", property: "name"});
   info.getPropertyValue(function(value) {
           //console.log("OperatingSystem version: "+value);
           os_version = value;
           checkProperties();
           }, fail, {aspect: "OperatingSystem", property: "version"});
   info.getPropertyValue(function(value) {
           //console.log("OperatingSystem vendor: "+value);
           os_vendor = value;
           checkProperties();
           }, fail, {aspect: "OperatingSystem", property: "vendor"});
};

module.exports = new Device();
<commit_msg>updating version<commit_after>var channel = require('cordova/channel'),
    utils = require('cordova/utils');

// Tell cordova channel to wait on the CordovaInfoReady event
channel.waitForInitialization('onCordovaInfoReady');

function Device() {
    this.platform = null;
    this.version = null;
    this.name = null;
    this.uuid = null;
    this.cordova = null;

    var me = this;

    channel.onCordovaReady.subscribeOnce(function() {
       me.getDeviceInfo(function (device) {
           me.platform = device.platform;
           me.version  = device.version;
           me.name     = device.name;
           me.uuid     = device.uuid;
           me.cordova  = device.cordova;

           channel.onCordovaInfoReady.fire();
       },
       function (e) {
           me.available = false;
           utils.alert("error initializing cordova: " + e);
       });
    });
}

Device.prototype.getDeviceInfo = function(success, fail, args) {
   var info = deviceapis.devicestatus;
   var properties = ["name", "uuid", "os_name", "os_vendor", "os_version"];

   var me = this;

   var name = null,
       platform = null,
       uuid = null,
       os_name = null,
       os_version = null,
       os_vendor = null;

   var checkProperties = function() {
       properties.pop();
       if(properties.length === 0) {
           me.name = name;
           me.platform = os_vendor + " " + os_name;
           me.version = os_version;
           me.uuid = uuid;
           me.cordova = "2.1.0";
           success(me);
       }
   };

   info.getPropertyValue(function(value) {
           //console.log("Device IMEI: "+value);
           uuid = value;
           checkProperties();
           }, fail, {aspect: "Device", property: "imei"});
   info.getPropertyValue(function(value) {
           //console.log("Device name: "+value);
           name = value;
           checkProperties();
           }, fail, {aspect: "Device", property: "version"});
   info.getPropertyValue(function(value) {
           //console.log("OperatingSystem name: "+value);
           os_name = value;
           checkProperties();
           }, fail, {aspect: "OperatingSystem", property: "name"});
   info.getPropertyValue(function(value) {
           //console.log("OperatingSystem version: "+value);
           os_version = value;
           checkProperties();
           }, fail, {aspect: "OperatingSystem", property: "version"});
   info.getPropertyValue(function(value) {
           //console.log("OperatingSystem vendor: "+value);
           os_vendor = value;
           checkProperties();
           }, fail, {aspect: "OperatingSystem", property: "vendor"});
};

module.exports = new Device();
<|endoftext|>


<commit_before>
// Opinion: I think it's gross to start written English independant clauses with "so"
//          most of the time. Maybe it's okay in spoken English.
//
// More on "so:"
// * http://www.nytimes.com/2010/05/22/us/22iht-currents.html?_r=0
// * http://comminfo.rutgers.edu/images/comprofiler/plug_profilegallery/84/pg_2103855866.pdf

// this implementation is really naive
var re = new RegExp('([^\n\\.;!?]+)([\\.;!?]+)', 'gi');
var startsWithSo = new RegExp('^(\\s)*so\\b[\\s\\S]', 'i');
module.exports = function (text) {
  var suggestions = [];
  var match, innerMatch;

  while (match = re.exec(text)) {
    if (innerMatch = startsWithSo.exec(match[1])) {
      suggestions.push({
        index: match.index + (innerMatch[1] || '').length,
        offset: 2
      });
    }
  }
  return suggestions;
};
<commit_msg>Fix typo<commit_after>
// Opinion: I think it's gross to start written English independent clauses with "so"
//          most of the time. Maybe it's okay in spoken English.
//
// More on "so:"
// * http://www.nytimes.com/2010/05/22/us/22iht-currents.html?_r=0
// * http://comminfo.rutgers.edu/images/comprofiler/plug_profilegallery/84/pg_2103855866.pdf

// this implementation is really naive
var re = new RegExp('([^\n\\.;!?]+)([\\.;!?]+)', 'gi');
var startsWithSo = new RegExp('^(\\s)*so\\b[\\s\\S]', 'i');
module.exports = function (text) {
  var suggestions = [];
  var match, innerMatch;

  while (match = re.exec(text)) {
    if (innerMatch = startsWithSo.exec(match[1])) {
      suggestions.push({
        index: match.index + (innerMatch[1] || '').length,
        offset: 2
      });
    }
  }
  return suggestions;
};
<|endoftext|>


<commit_before>var sm = new (require('sphericalmercator'))();
var Stats = require('./stream-util').Stats;
var Tile = require('./stream-util').Tile;
var Info = require('./stream-util').Info;
var isEmpty = require('./stream-util').isEmpty;
var addChildren = require('./stream-util').addChildren;
var sumChildren = require('./stream-util').sumChildren;
var multiread = require('./stream-util').multiread;
var stream = require('stream');
var util = require('util');

module.exports = Pyramid;
util.inherits(Pyramid, stream.Readable);

function Pyramid(source, options) {
    if (!source) throw new TypeError('Tilesource required');

    options = options || {};

    if (options.bounds !== undefined && !Array.isArray(options.bounds))
        throw new TypeError('options.bounds must be an array of the form [w,s,e,n]');
    if (options.minzoom !== undefined && typeof options.minzoom !== 'number')
        throw new TypeError('options.minzoom must be a positive integer');
    if (options.maxzoom !== undefined && typeof options.maxzoom !== 'number')
        throw new TypeError('options.maxzoom must be a positive integer');

    this.source = source;
    this.bounds = options.bounds;
    this.minzoom = options.minzoom;
    this.maxzoom = options.maxzoom;
    this.pending = 0;
    this.queue = [];
    this.bboxes = undefined;
    this.cursor = undefined;
    this.stats = new Stats();
    this.length = 0;

    stream.Readable.call(this, { objectMode: true });
}

Pyramid.prototype._params = function(callback) {
    var stream = this;
    stream.source.getInfo(function(err, info) {
        if (err) return stream.emit('error', err);
        stream.bounds = stream.bounds !== undefined ? stream.bounds : info.bounds;
        stream.minzoom = stream.minzoom !== undefined ? stream.minzoom : info.minzoom;
        stream.maxzoom = stream.maxzoom !== undefined ? stream.maxzoom : info.maxzoom;
        if (stream.bounds === undefined) return stream.emit('error', new Error('No bounds determined'));
        if (stream.minzoom === undefined) return stream.emit('error', new Error('No minzoom determined'));
        if (stream.maxzoom === undefined) return stream.emit('error', new Error('No maxzoom determined'));

        stream.bboxes = {};
        for (var z = stream.minzoom; z <= stream.maxzoom; z++) {
            stream.bboxes[z] = sm.xyz(stream.bounds, z);
            stream.stats.total +=
                (stream.bboxes[z].maxX - stream.bboxes[z].minX + 1) *
                (stream.bboxes[z].maxY - stream.bboxes[z].minY + 1);
        }

        stream.cursor = {
            z: stream.minzoom,
            x: stream.bboxes[stream.minzoom].minX,
            y: stream.bboxes[stream.minzoom].minY
        };

        callback(null, info);
    });
};

Pyramid.prototype._read = function(size) {
    var stream = this;

    // Defer gets until info is retrieved and there is a cursor to be used.
    if (!stream.bboxes) return stream._params(function(err, info) {
        if (err) return stream.emit('error', err);
        stream.length = stream.stats.total;
        stream.emit('length', stream.length);
        stream.push(new Info(info));
    });

    multiread(stream, function get(push) {
        var z, x, y, tile;
        if (stream.queue.length) {
            var queued = stream.queue.shift();
            z = queued.z;
            x = queued.x;
            y = queued.y;
            stream.pending++;
            stream.stats.ops++;
            if (queued.buffer) {
                done(null, queued.buffer);
            } else {
                stream.source.getTile(z, x, y, done);
            }
            return true;
        } else if (stream.cursor) {
            z = stream.cursor.z;
            x = stream.cursor.x;
            y = stream.cursor.y;
            nextShallow(stream);
            stream.pending++;
            stream.stats.ops++;
            stream.source.getTile(z, x, y, done);
            return true;
        } else {
            return push(null) && false;
        }
        function done(err, buffer) {
            stream.pending--;
            if (err && !(/does not exist$/).test(err.message)) {
                stream.emit('error', err);
            } else if (err || isEmpty(buffer)) {
                tile = new Tile(z, x, y);
                var sum = sumChildren(tile, stream.bboxes);
                stream.stats.skipped += 1 + sum;
                stream.stats.done += 1 + sum;
                // Update length
                stream.length -= 1 + sum;
                stream.emit('length', stream.length);
                get(push);
            } else {
                tile = new Tile(z, x, y, buffer);
                addChildren(tile, stream.bboxes, stream.queue);
                stream.stats.done++;
                push(tile);
            }
        }
    });
};

// Increment a tile cursor to the next position on the lowest zoom level.
function nextShallow(stream) {
    if (!stream.cursor) return false;
    var cursor = stream.cursor;
    cursor.x++;
    var bbox = stream.bboxes[cursor.z];
    if (cursor.x > bbox.maxX) {
        cursor.x = bbox.minX;
        cursor.y++;
    }
    if (cursor.y > bbox.maxY) {
        stream.cursor = false;
        return false;
    }
    return true;
}

<commit_msg>Smarter skip logic that tolerates empty tiles until first nonempty is found. Refs #83.<commit_after>var sm = new (require('sphericalmercator'))();
var Stats = require('./stream-util').Stats;
var Tile = require('./stream-util').Tile;
var Info = require('./stream-util').Info;
var isEmpty = require('./stream-util').isEmpty;
var addChildren = require('./stream-util').addChildren;
var sumChildren = require('./stream-util').sumChildren;
var multiread = require('./stream-util').multiread;
var stream = require('stream');
var util = require('util');

module.exports = Pyramid;
util.inherits(Pyramid, stream.Readable);

function Pyramid(source, options) {
    if (!source) throw new TypeError('Tilesource required');

    options = options || {};

    if (options.bounds !== undefined && !Array.isArray(options.bounds))
        throw new TypeError('options.bounds must be an array of the form [w,s,e,n]');
    if (options.minzoom !== undefined && typeof options.minzoom !== 'number')
        throw new TypeError('options.minzoom must be a positive integer');
    if (options.maxzoom !== undefined && typeof options.maxzoom !== 'number')
        throw new TypeError('options.maxzoom must be a positive integer');

    this.source = source;
    this.bounds = options.bounds;
    this.minzoom = options.minzoom;
    this.maxzoom = options.maxzoom;
    this.pending = 0;
    this.queue = [];
    this.bboxes = undefined;
    this.cursor = undefined;
    this.stats = new Stats();
    this.length = 0;
    this.empty = true;

    stream.Readable.call(this, { objectMode: true });
}

Pyramid.prototype._params = function(callback) {
    var stream = this;
    stream.source.getInfo(function(err, info) {
        if (err) return stream.emit('error', err);
        stream.bounds = stream.bounds !== undefined ? stream.bounds : info.bounds;
        stream.minzoom = stream.minzoom !== undefined ? stream.minzoom : info.minzoom;
        stream.maxzoom = stream.maxzoom !== undefined ? stream.maxzoom : info.maxzoom;
        if (stream.bounds === undefined) return stream.emit('error', new Error('No bounds determined'));
        if (stream.minzoom === undefined) return stream.emit('error', new Error('No minzoom determined'));
        if (stream.maxzoom === undefined) return stream.emit('error', new Error('No maxzoom determined'));

        stream.bboxes = {};
        for (var z = stream.minzoom; z <= stream.maxzoom; z++) {
            stream.bboxes[z] = sm.xyz(stream.bounds, z);
            stream.stats.total +=
                (stream.bboxes[z].maxX - stream.bboxes[z].minX + 1) *
                (stream.bboxes[z].maxY - stream.bboxes[z].minY + 1);
        }

        stream.cursor = {
            z: stream.minzoom,
            x: stream.bboxes[stream.minzoom].minX,
            y: stream.bboxes[stream.minzoom].minY
        };

        callback(null, info);
    });
};

Pyramid.prototype._read = function(size) {
    var stream = this;

    // Defer gets until info is retrieved and there is a cursor to be used.
    if (!stream.bboxes) return stream._params(function(err, info) {
        if (err) return stream.emit('error', err);
        stream.length = stream.stats.total;
        stream.emit('length', stream.length);
        stream.push(new Info(info));
    });

    multiread(stream, function get(push) {
        var z, x, y, tile;
        if (stream.queue.length) {
            var queued = stream.queue.shift();
            z = queued.z;
            x = queued.x;
            y = queued.y;
            stream.pending++;
            stream.stats.ops++;
            if (queued.buffer) {
                done(null, queued.buffer);
            } else {
                stream.source.getTile(z, x, y, done);
            }
            return true;
        } else if (stream.cursor) {
            z = stream.cursor.z;
            x = stream.cursor.x;
            y = stream.cursor.y;
            nextShallow(stream);
            stream.pending++;
            stream.stats.ops++;
            stream.source.getTile(z, x, y, done);
            return true;
        } else {
            return push(null) && false;
        }
        function done(err, buffer) {
            stream.pending--;
            if (err && !(/does not exist$/).test(err.message)) {
                stream.emit('error', err);
            } else if (err || isEmpty(buffer)) {
                tile = new Tile(z, x, y);
                // stream.empty flag marks whether the stream has only
                // consisted of empty tiles to this point. Until the
                // first tile with content has been seen pyramid scheme
                // will not skip tiles.
                var sum = 0;
                if (stream.empty) {
                    addChildren(tile, stream.bboxes, stream.queue);
                } else {
                    sum = sumChildren(tile, stream.bboxes);
                }
                stream.stats.skipped += 1 + sum;
                stream.stats.done += 1 + sum;
                // Update length
                stream.length -= 1 + sum;
                stream.emit('length', stream.length);
                get(push);
            } else {
                stream.empty = false;
                tile = new Tile(z, x, y, buffer);
                addChildren(tile, stream.bboxes, stream.queue);
                stream.stats.done++;
                push(tile);
            }
        }
    });
};

// Increment a tile cursor to the next position on the lowest zoom level.
function nextShallow(stream) {
    if (!stream.cursor) return false;
    var cursor = stream.cursor;
    cursor.x++;
    var bbox = stream.bboxes[cursor.z];
    if (cursor.x > bbox.maxX) {
        cursor.x = bbox.minX;
        cursor.y++;
    }
    if (cursor.y > bbox.maxY) {
        stream.cursor = false;
        return false;
    }
    return true;
}

<|endoftext|>


<commit_before><commit_msg>Update Babylonjs Version<commit_after><|endoftext|>


<commit_before>var ot = require('./ot');
var projections = require('./projections');

function SubmitRequest(backend, agent, index, id, op, options) {
  this.backend = backend;
  this.agent = agent;
  // If a projection, rewrite the call into a call against the collection
  var projection = backend.projections[index];
  this.index = index;
  this.projection = projection;
  this.collection = (projection) ? projection.target : index;
  this.id = id;
  this.op = op;
  this.options = options;

  this.start = Date.now();
  this._addOpMeta();

  // Set as this request is sent through middleware
  this.action = null;
  // For custom use in middleware
  this.custom = {};

  this.suppressPublish = backend.suppressPublish;
  this.maxRetries = backend.maxSubmitRetries;
  this.retries = 0;

  // return values
  this.snapshot = null;
  this.ops = [];
  this.channels = null;
}
module.exports = SubmitRequest;

SubmitRequest.prototype.submit = function(callback) {
  var request = this;
  var backend = this.backend;
  var collection = this.collection;
  var id = this.id;
  var op = this.op;
  // Send a special projection so that getSnapshot knows to return all fields.
  // With a null projection, it strips document metadata
  var fields = {$submit: true};

  backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) {
    if (err) return callback(err);

    request.snapshot = snapshot;
    request._addSnapshotMeta();

    if (op.v == null) {

      if (op.create && snapshot.type && op.src) {
        // If the document was already created by another op, we will return a
        // 'Document already exists' error in response and fail to submit this
        // op. However, this could also happen in the case that the op was
        // already committed and the create op was simply resent. In that
        // case, we should return a non-fatal 'Op already submitted' error. We
        // must get the past ops and check their src and seq values to
        // differentiate.
        backend.db.getCommittedOpVersion(collection, id, snapshot, op, null, function(err, version) {
          if (err) return callback(err);
          if (version == null) {
            callback(request.alreadyCreatedError());
          } else {
            op.v = version;
            callback(request.alreadySubmittedError());
          }
        });
        return;
      }

      // Submitting an op with a null version means that it should get the
      // version from the latest snapshot. Generally this will mean the op
      // won't be transformed, though transform could be called on it in the
      // case of a retry from a simultaneous submit
      op.v = snapshot.v;
    }

    if (op.v === snapshot.v) {
      // The snapshot hasn't changed since the op's base version. Apply
      // without transforming the op
      return request.apply(callback);
    }

    if (op.v > snapshot.v) {
      // The op version should be from a previous snapshot, so it should never
      // never exceed the current snapshot's version
      return callback(request.newerVersionError());
    }

    // Transform the op up to the current snapshot version, then apply
    var from = op.v;
    backend.db.getOpsToSnapshot(collection, id, from, snapshot, null, function(err, ops) {
      if (err) return callback(err);

      if (ops.length !== snapshot.v - from) {
        return callback(request.missingOpsError());
      }

      err = request._transformOp(ops);
      if (err) return callback(err);

      if (op.v !== snapshot.v) {
        // This shouldn't happen, but is just a final sanity check to make
        // sure we have transformed the op to the current snapshot version
        return callback(request.versionAfterTransformError());
      }

      request.apply(callback);
    });
  });
};

SubmitRequest.prototype.apply = function(callback) {
  // If we're being projected, verify that the op is allowed
  var projection = this.projection;
  if (projection && !projections.isOpAllowed(this.snapshot.type, projection.fields, this.op)) {
    return callback(this.projectionError());
  }

  // Always set the channels before each attempt to apply. If the channels are
  // modified in a middleware and we retry, we want to reset to a new array
  this.channels = this.backend.getChannels(this.collection, this.id);

  var request = this;
  this.backend.trigger('apply', this.agent, this, function(err) {
    if (err) return callback(err);

    // Apply the submitted op to the snapshot
    err = ot.apply(request.snapshot, request.op);
    if (err) return callback(err);

    request.commit(callback);
  });
};

SubmitRequest.prototype.commit = function(callback) {
  var request = this;
  var backend = this.backend;
  backend.trigger('commit', this.agent, this, function(err) {
    if (err) return callback(err);

    // Try committing the operation and snapshot to the database atomically
    backend.db.commit(request.collection, request.id, request.op, request.snapshot, request.options, function(err, succeeded) {
      if (err) return callback(err);
      if (!succeeded) {
        // Between our fetch and our call to commit, another client committed an
        // operation. We expect this to be relatively infrequent but normal.
        return request.retry(callback);
      }
      if (!request.suppressPublish) {
        var op = request.op;
        op.c = request.collection;
        op.d = request.id;
        op.m = undefined;
        // Needed for agent to detect if it can ignore sending the op back to
        // the client that submitted it in subscriptions
        if (request.collection !== request.index) op.i = request.index;
        backend.pubsub.publish(request.channels, op);
      }
      callback();
    });
  });
};

SubmitRequest.prototype.retry = function(callback) {
  this.retries++;
  if (this.maxRetries != null && this.retries > this.maxRetries) {
    return callback(this.maxRetriesError());
  }
  this.backend.emit('timing', 'submit.retry', Date.now() - this.start, this);
  this.submit(callback);
};

SubmitRequest.prototype._transformOp = function(ops) {
  var type = this.snapshot.type;
  for (var i = 0; i < ops.length; i++) {
    var op = ops[i];

    if (this.op.src && this.op.src === op.src && this.op.seq === op.seq) {
      // The op has already been submitted. There are a variety of ways this
      // can happen in normal operation, such as a client resending an
      // unacknowledged operation at reconnect. It's important we don't apply
      // the same op twice
      return this.alreadySubmittedError();
    }

    if (this.op.v !== op.v) {
      return this.versionDuringTransformError();
    }

    var err = ot.transform(type, this.op, op);
    if (err) return err;
    this.ops.push(op);
  }
};

SubmitRequest.prototype._addOpMeta = function() {
  this.op.m = {
    ts: this.start
  };
  if (this.op.create) {
    // Consistently store the full URI of the type, not just its short name
    this.op.create.type = ot.normalizeType(this.op.create.type);
  }
};

SubmitRequest.prototype._addSnapshotMeta = function() {
  var meta = this.snapshot.m || (this.snapshot.m = {});
  if (this.op.create) {
    meta.ctime = this.start;
  } else if (this.op.del) {
    this.op.m.data = this.snapshot.data;
  }
  meta.mtime = this.start;
};

// Non-fatal client errors:
SubmitRequest.prototype.alreadySubmittedError = function() {
  return {code: 4001, message: 'Op already submitted'};
};
SubmitRequest.prototype.rejectedError = function() {
  return {code: 4002, message: 'Op submit rejected'};
};
// Fatal client errors:
SubmitRequest.prototype.alreadyCreatedError = function() {
  return {code: 4010, message: 'Invalid op submitted. Document already created'};
};
SubmitRequest.prototype.newerVersionError = function() {
  return {code: 4011, message: 'Invalid op submitted. Op version newer than current snapshot'};
};
SubmitRequest.prototype.projectionError = function() {
  return {code: 4012, message: 'Invalid op submitted. Operation invalid in projected collection'};
};
// Fatal internal errors:
SubmitRequest.prototype.missingOpsError = function() {
  return {code: 5001, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'};
};
SubmitRequest.prototype.versionDuringTransformError = function() {
  return {code: 5002, message: 'Op submit failed. Versions mismatched during op transform'};
};
SubmitRequest.prototype.versionAfterTransformError = function() {
  return {code: 5003, message: 'Op submit failed. Op version mismatches snapshot after op transform'};
};
SubmitRequest.prototype.maxRetriesError = function() {
  return {code: 5004, message: 'Op submit failed. Maximum submit retries exceeded'};
};
<commit_msg>Fix server crashing on failed publish<commit_after>var ot = require('./ot');
var projections = require('./projections');

function SubmitRequest(backend, agent, index, id, op, options) {
  this.backend = backend;
  this.agent = agent;
  // If a projection, rewrite the call into a call against the collection
  var projection = backend.projections[index];
  this.index = index;
  this.projection = projection;
  this.collection = (projection) ? projection.target : index;
  this.id = id;
  this.op = op;
  this.options = options;

  this.start = Date.now();
  this._addOpMeta();

  // Set as this request is sent through middleware
  this.action = null;
  // For custom use in middleware
  this.custom = {};

  this.suppressPublish = backend.suppressPublish;
  this.maxRetries = backend.maxSubmitRetries;
  this.retries = 0;

  // return values
  this.snapshot = null;
  this.ops = [];
  this.channels = null;
}
module.exports = SubmitRequest;

SubmitRequest.prototype.submit = function(callback) {
  var request = this;
  var backend = this.backend;
  var collection = this.collection;
  var id = this.id;
  var op = this.op;
  // Send a special projection so that getSnapshot knows to return all fields.
  // With a null projection, it strips document metadata
  var fields = {$submit: true};

  backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) {
    if (err) return callback(err);

    request.snapshot = snapshot;
    request._addSnapshotMeta();

    if (op.v == null) {

      if (op.create && snapshot.type && op.src) {
        // If the document was already created by another op, we will return a
        // 'Document already exists' error in response and fail to submit this
        // op. However, this could also happen in the case that the op was
        // already committed and the create op was simply resent. In that
        // case, we should return a non-fatal 'Op already submitted' error. We
        // must get the past ops and check their src and seq values to
        // differentiate.
        backend.db.getCommittedOpVersion(collection, id, snapshot, op, null, function(err, version) {
          if (err) return callback(err);
          if (version == null) {
            callback(request.alreadyCreatedError());
          } else {
            op.v = version;
            callback(request.alreadySubmittedError());
          }
        });
        return;
      }

      // Submitting an op with a null version means that it should get the
      // version from the latest snapshot. Generally this will mean the op
      // won't be transformed, though transform could be called on it in the
      // case of a retry from a simultaneous submit
      op.v = snapshot.v;
    }

    if (op.v === snapshot.v) {
      // The snapshot hasn't changed since the op's base version. Apply
      // without transforming the op
      return request.apply(callback);
    }

    if (op.v > snapshot.v) {
      // The op version should be from a previous snapshot, so it should never
      // never exceed the current snapshot's version
      return callback(request.newerVersionError());
    }

    // Transform the op up to the current snapshot version, then apply
    var from = op.v;
    backend.db.getOpsToSnapshot(collection, id, from, snapshot, null, function(err, ops) {
      if (err) return callback(err);

      if (ops.length !== snapshot.v - from) {
        return callback(request.missingOpsError());
      }

      err = request._transformOp(ops);
      if (err) return callback(err);

      if (op.v !== snapshot.v) {
        // This shouldn't happen, but is just a final sanity check to make
        // sure we have transformed the op to the current snapshot version
        return callback(request.versionAfterTransformError());
      }

      request.apply(callback);
    });
  });
};

SubmitRequest.prototype.apply = function(callback) {
  // If we're being projected, verify that the op is allowed
  var projection = this.projection;
  if (projection && !projections.isOpAllowed(this.snapshot.type, projection.fields, this.op)) {
    return callback(this.projectionError());
  }

  // Always set the channels before each attempt to apply. If the channels are
  // modified in a middleware and we retry, we want to reset to a new array
  this.channels = this.backend.getChannels(this.collection, this.id);

  var request = this;
  this.backend.trigger('apply', this.agent, this, function(err) {
    if (err) return callback(err);

    // Apply the submitted op to the snapshot
    err = ot.apply(request.snapshot, request.op);
    if (err) return callback(err);

    request.commit(callback);
  });
};

SubmitRequest.prototype.commit = function(callback) {
  var request = this;
  var backend = this.backend;
  backend.trigger('commit', this.agent, this, function(err) {
    if (err) return callback(err);

    // Try committing the operation and snapshot to the database atomically
    backend.db.commit(request.collection, request.id, request.op, request.snapshot, request.options, function(err, succeeded) {
      if (err) return callback(err);
      if (!succeeded) {
        // Between our fetch and our call to commit, another client committed an
        // operation. We expect this to be relatively infrequent but normal.
        return request.retry(callback);
      }
      if (!request.suppressPublish) {
        var op = request.op;
        op.c = request.collection;
        op.d = request.id;
        op.m = undefined;
        // Needed for agent to detect if it can ignore sending the op back to
        // the client that submitted it in subscriptions
        if (request.collection !== request.index) op.i = request.index;
        backend.pubsub.publish(request.channels, op, noop);
      }
      callback();
    });
  });
};

SubmitRequest.prototype.retry = function(callback) {
  this.retries++;
  if (this.maxRetries != null && this.retries > this.maxRetries) {
    return callback(this.maxRetriesError());
  }
  this.backend.emit('timing', 'submit.retry', Date.now() - this.start, this);
  this.submit(callback);
};

SubmitRequest.prototype._transformOp = function(ops) {
  var type = this.snapshot.type;
  for (var i = 0; i < ops.length; i++) {
    var op = ops[i];

    if (this.op.src && this.op.src === op.src && this.op.seq === op.seq) {
      // The op has already been submitted. There are a variety of ways this
      // can happen in normal operation, such as a client resending an
      // unacknowledged operation at reconnect. It's important we don't apply
      // the same op twice
      return this.alreadySubmittedError();
    }

    if (this.op.v !== op.v) {
      return this.versionDuringTransformError();
    }

    var err = ot.transform(type, this.op, op);
    if (err) return err;
    this.ops.push(op);
  }
};

SubmitRequest.prototype._addOpMeta = function() {
  this.op.m = {
    ts: this.start
  };
  if (this.op.create) {
    // Consistently store the full URI of the type, not just its short name
    this.op.create.type = ot.normalizeType(this.op.create.type);
  }
};

SubmitRequest.prototype._addSnapshotMeta = function() {
  var meta = this.snapshot.m || (this.snapshot.m = {});
  if (this.op.create) {
    meta.ctime = this.start;
  } else if (this.op.del) {
    this.op.m.data = this.snapshot.data;
  }
  meta.mtime = this.start;
};

// Non-fatal client errors:
SubmitRequest.prototype.alreadySubmittedError = function() {
  return {code: 4001, message: 'Op already submitted'};
};
SubmitRequest.prototype.rejectedError = function() {
  return {code: 4002, message: 'Op submit rejected'};
};
// Fatal client errors:
SubmitRequest.prototype.alreadyCreatedError = function() {
  return {code: 4010, message: 'Invalid op submitted. Document already created'};
};
SubmitRequest.prototype.newerVersionError = function() {
  return {code: 4011, message: 'Invalid op submitted. Op version newer than current snapshot'};
};
SubmitRequest.prototype.projectionError = function() {
  return {code: 4012, message: 'Invalid op submitted. Operation invalid in projected collection'};
};
// Fatal internal errors:
SubmitRequest.prototype.missingOpsError = function() {
  return {code: 5001, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'};
};
SubmitRequest.prototype.versionDuringTransformError = function() {
  return {code: 5002, message: 'Op submit failed. Versions mismatched during op transform'};
};
SubmitRequest.prototype.versionAfterTransformError = function() {
  return {code: 5003, message: 'Op submit failed. Op version mismatches snapshot after op transform'};
};
SubmitRequest.prototype.maxRetriesError = function() {
  return {code: 5004, message: 'Op submit failed. Maximum submit retries exceeded'};
};

function noop() {}
<|endoftext|>


<commit_before>/*eslint no-invalid-this: 0*/ // eslint doesn't understand Promise.coroutine wrapping

// Controls the logic for determining which membership lists should be synced and
// handles the sequence of events until the lists are in sync.
"use strict";

var Promise = require("bluebird");
var promiseutil = require("../promiseutil");
var log = require("../logging").get("MemberListSyncer");
var stats = require("../config/stats");

function MemberListSyncer(ircBridge, appServiceBot, server, appServiceUserId, injectJoinFn) {
    this.ircBridge = ircBridge;
    this.appServiceBot = appServiceBot;
    this.server = server;
    this.appServiceUserId = appServiceUserId;
    this.injectJoinFn = injectJoinFn;
    this._syncableRoomsPromise = null;
    this._memberLists = {
        matrix: {
            //$roomId : {
            //    id: roomId,
            //    state: stateEvents,
            //    realJoinedUsers: [],
            //    remoteJoinedUsers: []
            //  }
        },
        irc: {
            //$channel : nick[]
        }
    }
}

MemberListSyncer.prototype.sync = Promise.coroutine(function*() {
    let server = this.server;
    if (!server.isMembershipListsEnabled()) {
        log.info("%s does not have membership list syncing enabled.", server.domain);
        return;
    }
    if (!server.shouldSyncMembershipToIrc("initial")) {
        log.info("%s shouldn't sync initial memberships to irc.", server.domain);
        return;
    }
    log.info("Checking membership lists for syncing on %s", server.domain);
    let start = Date.now();
    let rooms = yield this._getSyncableRooms(server);
    log.info("Found %s syncable rooms (%sms)", rooms.length, Date.now() - start);
    this.leaveIrcUsersFromRooms(rooms, server);
    start = Date.now();
    log.info("Joining Matrix users to IRC channels...");
    yield joinMatrixUsersToChannels(rooms, server, this.injectJoinFn);
    log.info("Joined Matrix users to IRC channels. (%sms)", Date.now() - start);
    // NB: We do not need to explicitly join IRC users to Matrix rooms
    // because we get all of the NAMEs/JOINs as events when we connect to
    // the IRC server. This effectively "injects" the list for us.
});

MemberListSyncer.prototype.getChannelsToJoin = Promise.coroutine(function*() {
    let server = this.server;
    log.debug("getChannelsToJoin => %s", server.domain);
    let rooms = yield this._getSyncableRooms(server);

    // map room IDs to channels on this server.
    let channels = new Set();
    let roomInfoMap = {};
    let roomIds = rooms.map((roomInfo) => {
        roomInfoMap[roomInfo.id] = roomInfo;
        return roomInfo.id;
    });
    yield this.ircBridge.getStore().getIrcChannelsForRoomIds(roomIds).then((roomIdToIrcRoom) => {
        Object.keys(roomIdToIrcRoom).forEach((roomId) => {
            // only interested in rooms for this server
            let ircRooms = roomIdToIrcRoom[roomId].filter((ircRoom) => {
                return ircRoom.server.domain === server.domain;
            });
            ircRooms.forEach((ircRoom) => {
                channels.add(ircRoom.channel);
                log.debug(
                    "%s should be joined because %s real Matrix users are in room %s",
                    ircRoom.channel, roomInfoMap[roomId].realJoinedUsers.length, roomId
                );
                if (roomInfoMap[roomId].realJoinedUsers.length < 5) {
                    log.debug("These are: %s", JSON.stringify(roomInfoMap[roomId].realJoinedUsers));
                }
            });
        })
    });

    let channelsArray = Array.from(channels);
    log.debug(
        "getChannelsToJoin => %s should be synced: %s",
        channelsArray.length, JSON.stringify(channelsArray)
    );
    return channelsArray;
});

// map irc channel to a list of room IDs. If all of those
// room IDs have no real users in them, then part the bridge bot too.
MemberListSyncer.prototype.checkBotPartRoom = Promise.coroutine(function*(ircRoom, req) {
    if (ircRoom.channel.indexOf("#") !== 0) {
        return; // don't leave PM rooms
    }
    let matrixRooms = yield this.ircBridge.getStore().getMatrixRoomsForChannel(
        ircRoom.server, ircRoom.channel
    );

    if (matrixRooms.length === 0) {
        // no mapped rooms, leave the channel.
        yield this.ircBridge.partBot(ircRoom);
        return;
    }

    // At least 1 mapped room - query for the membership list in each room. If there are
    // any real users still left in the room, then do not part the bot from the channel.
    // Query via /$room_id/state rather than /initialSync as the latter can cause
    // the bridge to spin for minutes if the response is large.

    let shouldPart = true;
    for (let i = 0; i < matrixRooms.length; i++) {
        let roomId = matrixRooms[i].getId();
        req.log.debug("checkBotPartRoom: Querying room state in room %s", roomId);
        let res = yield this.appServiceBot.getClient().roomState(roomId);
        let data = getRoomMemberData(ircRoom.server, roomId, res, this.appServiceUserId);
        req.log.debug(
            "checkBotPartRoom: %s Matrix users are in room %s", data.reals.length, roomId
        );
        if (data.reals.length > 0) {
            shouldPart = false;
            break;
        }
    }

    if (shouldPart) {
        yield this.ircBridge.partBot(ircRoom);
    }
});

// grab all rooms the bot knows about which have at least 1 real user in them.
// On startup, this can be called multiple times, so we cache the first request's promise
// and return that instead of making double hits.
//
// returns [
//   {
//       id: roomId,
//       state: stateEvents,
//       realJoinedUsers: [],
//       remoteJoinedUsers: []
//   },
//   ...
// ]
MemberListSyncer.prototype._getSyncableRooms = function(server, ignoreCache) {
    if (!ignoreCache && this._syncableRoomsPromise) {
        log.debug("Returning existing _getSyncableRooms Promise");
        return this._syncableRoomsPromise;
    }

    let self = this;
    let fetchRooms = Promise.coroutine(function*() {
        let roomInfoList = [];
        let joinedRoomIds = yield self.appServiceBot.getJoinedRooms();
        // fetch joined members allowing 5 in-flight reqs at a time
        let pool = new QueuePool(5, Promise.coroutine(function*(roomId) {
            let userMap = null;
            while (!userMap) {
                try {
                    userMap = yield self.appServiceBot.getJoinedMembers(roomId);
                }
                catch (err) {
                    log.error(`Failed to getJoinedMembers in room ${roomId}: ${err}`);
                }
            }
            let roomInfo = {
                id: roomId,
                state: [], // JSON objects (Matrix events)
                realJoinedUsers: [], // user IDs
                remoteJoinedUsers: [], // user IDs
            };
            let userIds = Object.keys(userMap);
            for (let j = 0; j < userIds.length; j++) {
                let userId = userIds[j];
                if (self.appServiceBot.getUserId() === userId) {
                    continue;
                }
                // TODO: Make this function public, it's useful!
                if (self.appServiceBot._isRemoteUser(roomId, userId)) {
                    roomInfo.remoteJoinedUsers.push(userId);
                }
                else {
                    roomInfo.realJoinedUsers.push(userId);
                }

                roomInfo.state.push({
                    room_id: roomId,
                    state_key: userId,
                    user_id: userId,
                    content: {
                        membership: "join",
                        display_name: userMap[userId].display_name,
                        avatar_url: userMap[userId].avatar_url,
                    }
                });
            }
            roomInfoList.push(roomInfo);
        }));
        // wait for all the requests to go through
        yield Promise.all(joinedRoomIds.map((roomId) => {
            return pool.enqueue(roomId, roomId);
        }));

        return roomInfoList.filter(function(roomInfo) {
            // filter out rooms with no real matrix users in them.
            return roomInfo.realJoinedUsers.length > 0;
        });
    });

    this._syncableRoomsPromise = fetchRooms();
    return this._syncableRoomsPromise;
};

function joinMatrixUsersToChannels(rooms, server, injectJoinFn) {
    var d = promiseutil.defer();

    // filter out rooms listed in the rules
    var filteredRooms = [];
    rooms.forEach(function(roomInfo) {
        if (!server.shouldSyncMembershipToIrc("initial", roomInfo.id)) {
            log.debug(
                "Trimming room %s according to config rules (matrixToIrc=false)",
                roomInfo.id
            );
            if (!roomInfo.realJoinedUsers[0]) {
                return; // no joined users at all
            }
            // trim the list to a single user. We do this rather than filter the
            // room out entirely because otherwise there will be NO matrix users
            // on the IRC-side resulting in no traffic whatsoever.
            roomInfo.realJoinedUsers = [roomInfo.realJoinedUsers[0]];
            log.debug("Trimmed to " + roomInfo.realJoinedUsers);
        }
        filteredRooms.push(roomInfo);
    });

    log.debug("%s rooms passed the config rules", filteredRooms.length);

    // map the filtered rooms to a list of users to join
    // [Room:{reals:[uid,uid]}, ...] => [{uid,roomid}, ...]
    var entries = [];
    filteredRooms.forEach(function(roomInfo) {
        roomInfo.realJoinedUsers.forEach(function(uid, index) {
            entries.push({
                roomId: roomInfo.id,
                userId: uid,
                // Mark the first real matrix user f.e room so we can inject
                // them first to get back up and running more quickly when there
                // is no bot.
                frontier: (index === 0)
            });
        });
    });
    // sort frontier markers to the front of the array
    entries.sort(function(a, b) {
        if (a.frontier && !b.frontier) {
            return -1; // a comes first
        }
        else if (b.frontier && !a.frontier) {
            return 1; // b comes first
        }
        return 0; // don't care
    });

    log.debug("Got %s matrix join events to inject.", entries.length);
    // take the first entry and inject a join event
    function joinNextUser() {
        var entry = entries.shift();
        if (!entry) {
            d.resolve();
            return;
        }
        if (entry.userId.indexOf("@-") === 0) {
            joinNextUser();
            return;
        }
        log.debug(
            "Injecting join event for %s in %s (%s left) is_frontier=%s",
            entry.userId, entry.roomId, entries.length, entry.frontier
        );
        injectJoinFn(entry.roomId, entry.userId, entry.frontier).timeout(
            server.getMemberListFloodDelayMs()
        ).then(() => {
            joinNextUser();
        }, (err) => { // discard error, this will be due to timeouts which we don't want to log
            joinNextUser();
        });
    }

    joinNextUser();

    return d.promise;
}

MemberListSyncer.prototype.leaveIrcUsersFromRooms = function(rooms, server) {
    log.info(
        `leaveIrcUsersFromRooms: storing member list info for ${rooms.length} ` +
        `rooms for server ${server.domain}`
    );

    // Store the matrix room info in memory for later retrieval when NAMES is received
    // and updateIrcMemberList is called. At that point, we have enough information to
    // leave users from the channel that the NAMES is for.
    rooms.forEach((roomInfo) => {
        this._memberLists.matrix[roomInfo.id] = roomInfo;
    });
}

// Update the MemberListSyncer with the IRC NAMES_RPL that has been received for channel.
// This will leave any matrix users that do not have their associated IRC nick in the list
// of names for this channel.
MemberListSyncer.prototype.updateIrcMemberList = Promise.coroutine(function*(channel, names) {
    if (this._memberLists.irc[channel] !== undefined ||
            !this.server.shouldSyncMembershipToMatrix("initial", channel)) {
        return;
    }
    this._memberLists.irc[channel] = Object.keys(names);

    log.info(
        `updateIrcMemberList: Updating IRC member list for ${channel} with ` +
        `${this._memberLists.irc[channel].length} IRC nicks`
    );

    // Convert the IRC channels nicks to userIds
    let ircUserIds = this._memberLists.irc[channel].map(
        (nick) => this.server.getUserIdFromNick(nick)
    );

    // For all bridged rooms, leave users from matrix that are not in the channel
    let roomsForChannel = yield this.ircBridge.getStore().getMatrixRoomsForChannel(
        this.server, channel
    );

    if (roomsForChannel.length === 0) {
        log.info(`updateIrcMemberList: No bridged rooms for channel ${channel}`);
        return;
    }

    // If a userId is in remoteJoinedUsers, but not ircUserIds, intend on leaving roomId
    let promises = [];
    roomsForChannel.forEach((matrixRoom) => {
        let roomId = matrixRoom.getId();
        if (!(
                this._memberLists.matrix[roomId] &&
                this._memberLists.matrix[roomId].remoteJoinedUsers
            )) {
                return;
        }
        this._memberLists.matrix[roomId].remoteJoinedUsers.forEach(
            (userId) => {
                if (ircUserIds.indexOf(userId) === -1) {
                    promises.push(
                        this.ircBridge.getAppServiceBridge().getIntent(userId).leave(roomId)
                    );
                    stats.membership(true, "part");
                }
            }
        );
    });
    log.info(
        `updateIrcMemberList: Leaving ${promises.length} users as they are not in ${channel}.`
    );
    yield Promise.all(promises);
});

function getRoomMemberData(server, roomId, stateEvents, appServiceUserId) {
    stateEvents = stateEvents || [];
    var data = {
        roomId: roomId,
        virtuals: [],
        reals: []
    };
    stateEvents.forEach(function(event) {
        if (event.type !== "m.room.member" || event.content.membership !== "join") {
            return;
        }
        var userId = event.state_key;
        if (userId === appServiceUserId) {
            return;
        }
        if (server.claimsUserId(userId)) {
            data.virtuals.push(userId);
        }
        else if (userId.indexOf("@-") === 0) {
            // Ignore guest user IDs -- TODO: Do this properly by passing them through
        }
        else {
            data.reals.push(userId);
        }
    });
    return data;
}

module.exports = MemberListSyncer;
<commit_msg>Bug fixes, logging, # in-flight reqs tweak<commit_after>/*eslint no-invalid-this: 0*/ // eslint doesn't understand Promise.coroutine wrapping

// Controls the logic for determining which membership lists should be synced and
// handles the sequence of events until the lists are in sync.
"use strict";

var Promise = require("bluebird");
var promiseutil = require("../promiseutil");
var log = require("../logging").get("MemberListSyncer");
var stats = require("../config/stats");
var QueuePool = require("../util/QueuePool");

function MemberListSyncer(ircBridge, appServiceBot, server, appServiceUserId, injectJoinFn) {
    this.ircBridge = ircBridge;
    this.appServiceBot = appServiceBot;
    this.server = server;
    this.appServiceUserId = appServiceUserId;
    this.injectJoinFn = injectJoinFn;
    this._syncableRoomsPromise = null;
    this._memberLists = {
        matrix: {
            //$roomId : {
            //    id: roomId,
            //    state: stateEvents,
            //    realJoinedUsers: [],
            //    remoteJoinedUsers: []
            //  }
        },
        irc: {
            //$channel : nick[]
        }
    }
}

MemberListSyncer.prototype.sync = Promise.coroutine(function*() {
    let server = this.server;
    if (!server.isMembershipListsEnabled()) {
        log.info("%s does not have membership list syncing enabled.", server.domain);
        return;
    }
    if (!server.shouldSyncMembershipToIrc("initial")) {
        log.info("%s shouldn't sync initial memberships to irc.", server.domain);
        return;
    }
    log.info("Checking membership lists for syncing on %s", server.domain);
    let start = Date.now();
    let rooms = yield this._getSyncableRooms();
    log.info("Found %s syncable rooms (%sms)", rooms.length, Date.now() - start);
    this.leaveIrcUsersFromRooms(rooms, server);
    start = Date.now();
    log.info("Joining Matrix users to IRC channels...");
    yield joinMatrixUsersToChannels(rooms, server, this.injectJoinFn);
    log.info("Joined Matrix users to IRC channels. (%sms)", Date.now() - start);
    // NB: We do not need to explicitly join IRC users to Matrix rooms
    // because we get all of the NAMEs/JOINs as events when we connect to
    // the IRC server. This effectively "injects" the list for us.
});

MemberListSyncer.prototype.getChannelsToJoin = Promise.coroutine(function*() {
    let server = this.server;
    log.debug("getChannelsToJoin => %s", server.domain);
    let rooms = yield this._getSyncableRooms();

    // map room IDs to channels on this server.
    let channels = new Set();
    let roomInfoMap = {};
    let roomIds = rooms.map((roomInfo) => {
        roomInfoMap[roomInfo.id] = roomInfo;
        return roomInfo.id;
    });
    yield this.ircBridge.getStore().getIrcChannelsForRoomIds(roomIds).then((roomIdToIrcRoom) => {
        Object.keys(roomIdToIrcRoom).forEach((roomId) => {
            // only interested in rooms for this server
            let ircRooms = roomIdToIrcRoom[roomId].filter((ircRoom) => {
                return ircRoom.server.domain === server.domain;
            });
            ircRooms.forEach((ircRoom) => {
                channels.add(ircRoom.channel);
                log.debug(
                    "%s should be joined because %s real Matrix users are in room %s",
                    ircRoom.channel, roomInfoMap[roomId].realJoinedUsers.length, roomId
                );
                if (roomInfoMap[roomId].realJoinedUsers.length < 5) {
                    log.debug("These are: %s", JSON.stringify(roomInfoMap[roomId].realJoinedUsers));
                }
            });
        })
    });

    let channelsArray = Array.from(channels);
    log.debug(
        "getChannelsToJoin => %s should be synced: %s",
        channelsArray.length, JSON.stringify(channelsArray)
    );
    return channelsArray;
});

// map irc channel to a list of room IDs. If all of those
// room IDs have no real users in them, then part the bridge bot too.
MemberListSyncer.prototype.checkBotPartRoom = Promise.coroutine(function*(ircRoom, req) {
    if (ircRoom.channel.indexOf("#") !== 0) {
        return; // don't leave PM rooms
    }
    let matrixRooms = yield this.ircBridge.getStore().getMatrixRoomsForChannel(
        ircRoom.server, ircRoom.channel
    );

    if (matrixRooms.length === 0) {
        // no mapped rooms, leave the channel.
        yield this.ircBridge.partBot(ircRoom);
        return;
    }

    // At least 1 mapped room - query for the membership list in each room. If there are
    // any real users still left in the room, then do not part the bot from the channel.
    // Query via /$room_id/state rather than /initialSync as the latter can cause
    // the bridge to spin for minutes if the response is large.

    let shouldPart = true;
    for (let i = 0; i < matrixRooms.length; i++) {
        let roomId = matrixRooms[i].getId();
        req.log.debug("checkBotPartRoom: Querying room state in room %s", roomId);
        let res = yield this.appServiceBot.getClient().roomState(roomId);
        let data = getRoomMemberData(ircRoom.server, roomId, res, this.appServiceUserId);
        req.log.debug(
            "checkBotPartRoom: %s Matrix users are in room %s", data.reals.length, roomId
        );
        if (data.reals.length > 0) {
            shouldPart = false;
            break;
        }
    }

    if (shouldPart) {
        yield this.ircBridge.partBot(ircRoom);
    }
});

// grab all rooms the bot knows about which have at least 1 real user in them.
// On startup, this can be called multiple times, so we cache the first request's promise
// and return that instead of making double hits.
//
// returns [
//   {
//       id: roomId,
//       state: stateEvents,
//       realJoinedUsers: [],
//       remoteJoinedUsers: []
//   },
//   ...
// ]
MemberListSyncer.prototype._getSyncableRooms = function() {
    if (this._syncableRoomsPromise) {
        log.debug("Returning existing _getSyncableRooms Promise");
        return this._syncableRoomsPromise;
    }

    let self = this;
    let fetchRooms = Promise.coroutine(function*() {
        let roomInfoList = [];
        let joinedRoomIds = yield self.appServiceBot.getJoinedRooms();
        // fetch joined members allowing 5 in-flight reqs at a time
        let pool = new QueuePool(50, Promise.coroutine(function*(roomId) {
            let userMap = null;
            while (!userMap) {
                try {
                    userMap = yield self.appServiceBot.getJoinedMembers(roomId);
                }
                catch (err) {
                    log.error(`Failed to getJoinedMembers in room ${roomId}: ${err}`);
                }
            }
            let roomInfo = {
                id: roomId,
                state: [], // JSON objects (Matrix events)
                realJoinedUsers: [], // user IDs
                remoteJoinedUsers: [], // user IDs
            };
            let userIds = Object.keys(userMap);
            for (let j = 0; j < userIds.length; j++) {
                let userId = userIds[j];
                if (self.appServiceBot.getUserId() === userId) {
                    continue;
                }
                // TODO: Make this function public, it's useful!
                if (self.appServiceBot._isRemoteUser(roomId, userId)) {
                    roomInfo.remoteJoinedUsers.push(userId);
                }
                else {
                    roomInfo.realJoinedUsers.push(userId);
                }

                roomInfo.state.push({
                    room_id: roomId,
                    state_key: userId,
                    user_id: userId,
                    content: {
                        membership: "join",
                        display_name: userMap[userId].display_name,
                        avatar_url: userMap[userId].avatar_url,
                    }
                });
            }
            roomInfoList.push(roomInfo);
            log.info(
                "%s has %s real Matrix users and %s remote users (%s/%s)",
                roomId, roomInfo.realJoinedUsers.length, roomInfo.remoteJoinedUsers.length,
                roomInfoList.length, joinedRoomIds.length
            );
        }));
        // wait for all the requests to go through
        yield Promise.all(joinedRoomIds.map((roomId) => {
            return pool.enqueue(roomId, roomId);
        }));

        return roomInfoList.filter(function(roomInfo) {
            // filter out rooms with no real matrix users in them.
            return roomInfo.realJoinedUsers.length > 0;
        });
    });

    this._syncableRoomsPromise = fetchRooms();
    return this._syncableRoomsPromise;
};

function joinMatrixUsersToChannels(rooms, server, injectJoinFn) {
    var d = promiseutil.defer();

    // filter out rooms listed in the rules
    var filteredRooms = [];
    rooms.forEach(function(roomInfo) {
        if (!server.shouldSyncMembershipToIrc("initial", roomInfo.id)) {
            log.debug(
                "Trimming room %s according to config rules (matrixToIrc=false)",
                roomInfo.id
            );
            if (!roomInfo.realJoinedUsers[0]) {
                return; // no joined users at all
            }
            // trim the list to a single user. We do this rather than filter the
            // room out entirely because otherwise there will be NO matrix users
            // on the IRC-side resulting in no traffic whatsoever.
            roomInfo.realJoinedUsers = [roomInfo.realJoinedUsers[0]];
            log.debug("Trimmed to " + roomInfo.realJoinedUsers);
        }
        filteredRooms.push(roomInfo);
    });

    log.debug("%s rooms passed the config rules", filteredRooms.length);

    // map the filtered rooms to a list of users to join
    // [Room:{reals:[uid,uid]}, ...] => [{uid,roomid}, ...]
    var entries = [];
    filteredRooms.forEach(function(roomInfo) {
        roomInfo.realJoinedUsers.forEach(function(uid, index) {
            entries.push({
                roomId: roomInfo.id,
                userId: uid,
                // Mark the first real matrix user f.e room so we can inject
                // them first to get back up and running more quickly when there
                // is no bot.
                frontier: (index === 0)
            });
        });
    });
    // sort frontier markers to the front of the array
    entries.sort(function(a, b) {
        if (a.frontier && !b.frontier) {
            return -1; // a comes first
        }
        else if (b.frontier && !a.frontier) {
            return 1; // b comes first
        }
        return 0; // don't care
    });

    log.debug("Got %s matrix join events to inject.", entries.length);
    // take the first entry and inject a join event
    function joinNextUser() {
        var entry = entries.shift();
        if (!entry) {
            d.resolve();
            return;
        }
        if (entry.userId.indexOf("@-") === 0) {
            joinNextUser();
            return;
        }
        log.debug(
            "Injecting join event for %s in %s (%s left) is_frontier=%s",
            entry.userId, entry.roomId, entries.length, entry.frontier
        );
        injectJoinFn(entry.roomId, entry.userId, entry.frontier).timeout(
            server.getMemberListFloodDelayMs()
        ).then(() => {
            joinNextUser();
        }, (err) => { // discard error, this will be due to timeouts which we don't want to log
            joinNextUser();
        });
    }

    joinNextUser();

    return d.promise;
}

MemberListSyncer.prototype.leaveIrcUsersFromRooms = function(rooms, server) {
    log.info(
        `leaveIrcUsersFromRooms: storing member list info for ${rooms.length} ` +
        `rooms for server ${server.domain}`
    );

    // Store the matrix room info in memory for later retrieval when NAMES is received
    // and updateIrcMemberList is called. At that point, we have enough information to
    // leave users from the channel that the NAMES is for.
    rooms.forEach((roomInfo) => {
        this._memberLists.matrix[roomInfo.id] = roomInfo;
    });
}

// Update the MemberListSyncer with the IRC NAMES_RPL that has been received for channel.
// This will leave any matrix users that do not have their associated IRC nick in the list
// of names for this channel.
MemberListSyncer.prototype.updateIrcMemberList = Promise.coroutine(function*(channel, names) {
    if (this._memberLists.irc[channel] !== undefined ||
            !this.server.shouldSyncMembershipToMatrix("initial", channel)) {
        return;
    }
    this._memberLists.irc[channel] = Object.keys(names);

    log.info(
        `updateIrcMemberList: Updating IRC member list for ${channel} with ` +
        `${this._memberLists.irc[channel].length} IRC nicks`
    );

    // Convert the IRC channels nicks to userIds
    let ircUserIds = this._memberLists.irc[channel].map(
        (nick) => this.server.getUserIdFromNick(nick)
    );

    // For all bridged rooms, leave users from matrix that are not in the channel
    let roomsForChannel = yield this.ircBridge.getStore().getMatrixRoomsForChannel(
        this.server, channel
    );

    if (roomsForChannel.length === 0) {
        log.info(`updateIrcMemberList: No bridged rooms for channel ${channel}`);
        return;
    }

    // If a userId is in remoteJoinedUsers, but not ircUserIds, intend on leaving roomId
    let promises = [];
    roomsForChannel.forEach((matrixRoom) => {
        let roomId = matrixRoom.getId();
        if (!(
                this._memberLists.matrix[roomId] &&
                this._memberLists.matrix[roomId].remoteJoinedUsers
            )) {
                return;
        }
        this._memberLists.matrix[roomId].remoteJoinedUsers.forEach(
            (userId) => {
                if (ircUserIds.indexOf(userId) === -1) {
                    promises.push(
                        this.ircBridge.getAppServiceBridge().getIntent(userId).leave(roomId)
                    );
                    stats.membership(true, "part");
                }
            }
        );
    });
    log.info(
        `updateIrcMemberList: Leaving ${promises.length} users as they are not in ${channel}.`
    );
    yield Promise.all(promises);
});

function getRoomMemberData(server, roomId, stateEvents, appServiceUserId) {
    stateEvents = stateEvents || [];
    var data = {
        roomId: roomId,
        virtuals: [],
        reals: []
    };
    stateEvents.forEach(function(event) {
        if (event.type !== "m.room.member" || event.content.membership !== "join") {
            return;
        }
        var userId = event.state_key;
        if (userId === appServiceUserId) {
            return;
        }
        if (server.claimsUserId(userId)) {
            data.virtuals.push(userId);
        }
        else if (userId.indexOf("@-") === 0) {
            // Ignore guest user IDs -- TODO: Do this properly by passing them through
        }
        else {
            data.reals.push(userId);
        }
    });
    return data;
}

module.exports = MemberListSyncer;
<|endoftext|>