<commit_before>window.cls || (window.cls = {});
cls.EcmascriptDebugger || (cls.EcmascriptDebugger = {});
cls.EcmascriptDebugger["5.0"] || (cls.EcmascriptDebugger["5.0"] = {});
cls.EcmascriptDebugger["6.0"] || (cls.EcmascriptDebugger["6.0"] = {});

/**
  * @constructor
  */

cls.EcmascriptDebugger["6.0"].InspectableDOMNode =
cls.EcmascriptDebugger["5.0"].InspectableDOMNode = function(rt_id, obj_id)
{
  this._init(rt_id, obj_id);
}

cls.EcmascriptDebugger["6.0"].InspectableDOMNode.prototype = new function()
{

  const
  NODE_LIST = 0,
  ID = 0,
  TYPE = 1,
  NAME = 2,
  DEPTH = 3,

  ATTRS = 5,
  ATTR_PREFIX = 0,
  ATTR_KEY = 1,
  ATTR_VALUE = 2,
  CHILDREN_LENGTH = 6,
  PUBLIC_ID = 4,
  SYSTEM_ID = 5;

  this._set_mime = function()
  {
    if (this._data)
      for (var node = null, i = 0; node = this._data[i]; i++)
      {
        if (node[TYPE] == 1 )
        {
          // TODO take in account doctype if present
          return /^[A-Z]*$/.test(node[NAME]) && "text/html" || "application/xml";
        }
      }
    return "";
  };

  this.get_mime = function()
  {
    return this._mime;
  }

  this.isTextHtml = function()
  {
    return this._data.length && this._mime == "text/html" || false;
  };

  // workaround for bug CORE-16147
  this.getDoctypeName = function()
  {
    for (var node = null, i = 0; node = this._data[i]; i++)
    {
      if (node[TYPE] == 1)
        return node[NAME];
    }
    return null;
  }

  this.expand = function(cb, object_id, traverse_type)
  {
    this._get_dom(object_id, traverse_type || "children", cb);
  }

  this._get_dom = function(object_id, traverse_type, cb)
  {
    this._isprocessing = true;
    var tag = window.tag_manager.set_callback(this, this.__handle_dom, [object_id, traverse_type, cb]);
    services['ecmascript-debugger'].requestInspectDom(tag, [object_id, traverse_type]);
  };

  this.__handle_dom = function(status, message, object_id, traverse_type, cb)
  {
    var
    _data = message[NODE_LIST],
    error_ms = ui_strings.DRAGONFLY_INFO_MESSAGE + 'this.__handle_dom failed in DOMBaseData',
    splice_args = null,
    i = 0;

    if (!status)
    {
      switch (traverse_type)
      {
        // traverse_type 'node' so far not supported
        case "parent-node-chain-with-children":
        {
          this._data = _data;
          break;
        }
        case "subtree":
        case "children":
        case "node":
        {
          for (; this._data[i] && this._data[i][ID] != object_id; i++);
          if (this._data[i])
          {
            // if object_id matches the one of the first node
            // of the return data the traversal was subtree
            splice_args = object_id == _data[0][ID] ? [i, 1] : [i + 1, 0];
            Array.prototype.splice.apply(this._data, splice_args.concat(_data));
          }
          else if (!this._data.length)
            this._data = _data;
          else
            opera.postError(error_ms);
          break;
        }
      }
      this._mime = this._set_mime();
      if (cb)
        cb();
    }
    else
      opera.postError(error_ms + ' ' + JSON.stringify(message));
    this._isprocessing = false;
  };

  this.__defineGetter__ = function('isprocessing', function()
  {
    return this._isprocessing;
  };

  this.__defineSetter__ = function('isprocessing', function(){};

  this.collapse = function(object_id)
  {
    var i = 0, j = 0, level = 0;
    for (; this._data[i] && this._data[i][ID] != object_id; i++ );
    if (this._data[i])
    {
      level = this._data[i][DEPTH];
      i += 1;
      j = i;
      while (this._data[j] && this._data[j][DEPTH] > level)
        j++;
      this._data.splice(i, j - i);
    }
    else
    {
      opera.postError(ui_strings.DRAGONFLY_INFO_MESSAGE +
                      'missing refrence in collapse_node in DOMBaseData');
    }
  };

  this._get_element_name = function(data_entry, force_lower_case, with_ids_and_classes)
  {
    var name = data_entry[NAME], attrs = data_entry[ATTRS], id = '', class_name = '';
    if (force_lower_case)
      name = name.toLowerCase();
    if (with_ids_and_classes)
    {
      for (var attr, i = 0; attr = attrs[i]; i++)
      {
        if (attr[ATTR_KEY] == 'id')
          id = "#" + attr[ATTR_VALUE];
        if (attr[ATTR_KEY] == 'class' && attr[ATTR_VALUE].trim())
          class_name = "." + attr[ATTR_VALUE].trim().replace(/\s+/g, ".");
      }
    }
    return name + id + class_name;
  }

  this._parse_parent_offset = function(chain)
  {
    var ret = false, cur = null;
    if (chain)
    {
      cur = chain.pop();
      if (cur)
        ret = cur[1] == '1';
      else
        opera.postError(ui_strings.DRAGONFLY_INFO_MESSAGE +
                        "failed in this._parse_parent_offset in InspectableDOMNode");
    }
    return ret;
  }

  this.get_css_path =
  this._get_css_path = function(object_id, parent_offset_chain,
                                force_lower_case, show_id_and_classes, show_siblings)
  {
    var i = 0, j = 0, path = [];
    if (object_id)
    {
      for ( ; this._data[i] && this._data[i][ID] != object_id; i++);
      if (this._data[i])
      {
        path.push({
          name: this._get_element_name(this._data[i], force_lower_case, show_id_and_classes),
          id: this._data[i][ID],
          combinator: "",
          is_parent_offset: this._parse_parent_offset(parent_offset_chain)
        });
        j = i;
        i--;
        for (  ; this._data[i]; i--)
        {
          if(this._data[i][TYPE] == 1)
          {
            if(this._data[i][DEPTH] <= this._data[j][DEPTH])
            {
              if (this._data[i][DEPTH] < this._data[j][DEPTH])
                path.push({
                  name: this._get_element_name(this._data[i], force_lower_case, show_id_and_classes),
                  id: this._data[i][ID],
                  combinator: ">" ,
                  is_parent_offset: this._parse_parent_offset(parent_offset_chain)
                });
              else if (show_siblings)
                path.push({
                  name: this._get_element_name(this._data[i], force_lower_case, show_id_and_classes),
                  id: this._data[i][ID],
                  combinator: "+",
                  is_parent_offset: false
                });
              j = i;
            }
          }
        }
      }
    }
    return path.reverse();
  }

  this.has_data = function()
  {
    return Boolean(this._data.length);
  }

  this.getData = function()
  {
    return this._data;
  }

  this.getParentElement = function(obj_id)
  {
    var i = 0, depth = 0;
    for ( ; this._data[i] && this._data[i][ID] != obj_id; i++);
    if (this._data[i])
    {
      depth = this._data[i][DEPTH];
      for ( ; this._data[i] && !((this._data[i][TYPE] == 1 || this._data[i][TYPE] == 9) && this._data[i][DEPTH] < depth); i--);
      return this._data[i] && this._data[i][ID] || 0;
    }
  }

  this.getRootElement = function()
  {
    for (var i = 0; this._data[i] && this._data[i][TYPE] != 1; i++);
    return this._data[i] && this._data[i][ID] || 0;
  }

  this.getDataRuntimeId = function()
  {
    return this._data_runtime_id;
  }

  this._get_id = (function()
  {
    var id_counter = 0;
    return function()
    {
      id_counter++;
      return "dom-inspection-id-" + id_counter.toString();
    };
  })();

  this._init = function(rt_id, obj_id)
  {
    this.id = this._get_id();
    this._data_runtime_id = rt_id || 0;  // data of a dom tree has always just one runtime
    this._root_obj_id = obj_id || 0;
    this._data = [];
    this._mime = '';
    if (!window.dominspections)
    {
      new cls.Namespace("dominspections");
    }
    window.dominspections.add(this);
  };

};

<commit_msg>Tentative fix for DFL-1304, Exception when expanding/collapsing nodes fast.<commit_after>window.cls || (window.cls = {});
cls.EcmascriptDebugger || (cls.EcmascriptDebugger = {});
cls.EcmascriptDebugger["5.0"] || (cls.EcmascriptDebugger["5.0"] = {});
cls.EcmascriptDebugger["6.0"] || (cls.EcmascriptDebugger["6.0"] = {});

/**
  * @constructor
  */

cls.EcmascriptDebugger["6.0"].InspectableDOMNode =
cls.EcmascriptDebugger["5.0"].InspectableDOMNode = function(rt_id, obj_id)
{
  this._init(rt_id, obj_id);
}

cls.EcmascriptDebugger["6.0"].InspectableDOMNode.prototype = new function()
{

  const
  NODE_LIST = 0,
  ID = 0,
  TYPE = 1,
  NAME = 2,
  DEPTH = 3,

  ATTRS = 5,
  ATTR_PREFIX = 0,
  ATTR_KEY = 1,
  ATTR_VALUE = 2,
  CHILDREN_LENGTH = 6,
  PUBLIC_ID = 4,
  SYSTEM_ID = 5;

  this._set_mime = function()
  {
    if (this._data)
      for (var node = null, i = 0; node = this._data[i]; i++)
      {
        if (node[TYPE] == 1 )
        {
          // TODO take in account doctype if present
          return /^[A-Z]*$/.test(node[NAME]) && "text/html" || "application/xml";
        }
      }
    return "";
  };

  this.get_mime = function()
  {
    return this._mime;
  }

  this.isTextHtml = function()
  {
    return this._data.length && this._mime == "text/html" || false;
  };

  // workaround for bug CORE-16147
  this.getDoctypeName = function()
  {
    for (var node = null, i = 0; node = this._data[i]; i++)
    {
      if (node[TYPE] == 1)
        return node[NAME];
    }
    return null;
  }

  this.expand = function(cb, object_id, traverse_type)
  {
    this._get_dom(object_id, traverse_type || "children", cb);
  }

  this._get_dom = function(object_id, traverse_type, cb)
  {
    this._isprocessing = true;
    var tag = window.tag_manager.set_callback(this, this.__handle_dom, [object_id, traverse_type, cb]);
    services['ecmascript-debugger'].requestInspectDom(tag, [object_id, traverse_type]);
  };

  this.__handle_dom = function(status, message, object_id, traverse_type, cb)
  {
    var
    _data = message[NODE_LIST],
    error_ms = ui_strings.DRAGONFLY_INFO_MESSAGE + 'this.__handle_dom failed in DOMBaseData',
    splice_args = null,
    i = 0;

    if (!status)
    {
      switch (traverse_type)
      {
        // traverse_type 'node' so far not supported
        case "parent-node-chain-with-children":
        {
          this._data = _data;
          break;
        }
        case "subtree":
        case "children":
        case "node":
        {
          for (; this._data[i] && this._data[i][ID] != object_id; i++);
          if (this._data[i])
          {
            // if object_id matches the one of the first node
            // of the return data the traversal was subtree
            splice_args = object_id == _data[0][ID] ? [i, 1] : [i + 1, 0];
            Array.prototype.splice.apply(this._data, splice_args.concat(_data));
          }
          else if (!this._data.length)
            this._data = _data;
          else
            opera.postError(error_ms);
          break;
        }
      }
      this._mime = this._set_mime();
      if (cb)
        cb();
    }
    else
      opera.postError(error_ms + ' ' + JSON.stringify(message));
    this._isprocessing = false;
  };

  this.__defineGetter__('isprocessing', function()
  {
    return this._isprocessing;
  });

  this.__defineSetter__('isprocessing', function(){});

  this.collapse = function(object_id)
  {
    var i = 0, j = 0, level = 0;
    for (; this._data[i] && this._data[i][ID] != object_id; i++ );
    if (this._data[i])
    {
      level = this._data[i][DEPTH];
      i += 1;
      j = i;
      while (this._data[j] && this._data[j][DEPTH] > level)
        j++;
      this._data.splice(i, j - i);
    }
    else
    {
      opera.postError(ui_strings.DRAGONFLY_INFO_MESSAGE +
                      'missing refrence in collapse_node in DOMBaseData');
    }
  };

  this._get_element_name = function(data_entry, force_lower_case, with_ids_and_classes)
  {
    var name = data_entry[NAME], attrs = data_entry[ATTRS], id = '', class_name = '';
    if (force_lower_case)
      name = name.toLowerCase();
    if (with_ids_and_classes)
    {
      for (var attr, i = 0; attr = attrs[i]; i++)
      {
        if (attr[ATTR_KEY] == 'id')
          id = "#" + attr[ATTR_VALUE];
        if (attr[ATTR_KEY] == 'class' && attr[ATTR_VALUE].trim())
          class_name = "." + attr[ATTR_VALUE].trim().replace(/\s+/g, ".");
      }
    }
    return name + id + class_name;
  }

  this._parse_parent_offset = function(chain)
  {
    var ret = false, cur = null;
    if (chain)
    {
      cur = chain.pop();
      if (cur)
        ret = cur[1] == '1';
      else
        opera.postError(ui_strings.DRAGONFLY_INFO_MESSAGE +
                        "failed in this._parse_parent_offset in InspectableDOMNode");
    }
    return ret;
  }

  this.get_css_path =
  this._get_css_path = function(object_id, parent_offset_chain,
                                force_lower_case, show_id_and_classes, show_siblings)
  {
    var i = 0, j = 0, path = [];
    if (object_id)
    {
      for ( ; this._data[i] && this._data[i][ID] != object_id; i++);
      if (this._data[i])
      {
        path.push({
          name: this._get_element_name(this._data[i], force_lower_case, show_id_and_classes),
          id: this._data[i][ID],
          combinator: "",
          is_parent_offset: this._parse_parent_offset(parent_offset_chain)
        });
        j = i;
        i--;
        for (  ; this._data[i]; i--)
        {
          if(this._data[i][TYPE] == 1)
          {
            if(this._data[i][DEPTH] <= this._data[j][DEPTH])
            {
              if (this._data[i][DEPTH] < this._data[j][DEPTH])
                path.push({
                  name: this._get_element_name(this._data[i], force_lower_case, show_id_and_classes),
                  id: this._data[i][ID],
                  combinator: ">" ,
                  is_parent_offset: this._parse_parent_offset(parent_offset_chain)
                });
              else if (show_siblings)
                path.push({
                  name: this._get_element_name(this._data[i], force_lower_case, show_id_and_classes),
                  id: this._data[i][ID],
                  combinator: "+",
                  is_parent_offset: false
                });
              j = i;
            }
          }
        }
      }
    }
    return path.reverse();
  }

  this.has_data = function()
  {
    return Boolean(this._data.length);
  }

  this.getData = function()
  {
    return this._data;
  }

  this.getParentElement = function(obj_id)
  {
    var i = 0, depth = 0;
    for ( ; this._data[i] && this._data[i][ID] != obj_id; i++);
    if (this._data[i])
    {
      depth = this._data[i][DEPTH];
      for ( ; this._data[i] && !((this._data[i][TYPE] == 1 || this._data[i][TYPE] == 9) && this._data[i][DEPTH] < depth); i--);
      return this._data[i] && this._data[i][ID] || 0;
    }
  }

  this.getRootElement = function()
  {
    for (var i = 0; this._data[i] && this._data[i][TYPE] != 1; i++);
    return this._data[i] && this._data[i][ID] || 0;
  }

  this.getDataRuntimeId = function()
  {
    return this._data_runtime_id;
  }

  this._get_id = (function()
  {
    var id_counter = 0;
    return function()
    {
      id_counter++;
      return "dom-inspection-id-" + id_counter.toString();
    };
  })();

  this._init = function(rt_id, obj_id)
  {
    this.id = this._get_id();
    this._data_runtime_id = rt_id || 0;  // data of a dom tree has always just one runtime
    this._root_obj_id = obj_id || 0;
    this._data = [];
    this._mime = '';
    if (!window.dominspections)
    {
      new cls.Namespace("dominspections");
    }
    window.dominspections.add(this);
  };

};

<|endoftext|>


<commit_before>#!/usr/bin/env node

'use strict';

const clear = require('clear');
const {readJsonSync} = require('fs-extra');
const theme = require('../theme');

const run = async ({cwd, packages, tags}) => {
  // All packages are built from a single source revision,
  // so it is safe to read the commit number from any one of them.
  const {commit, environment} = readJsonSync(
    `${cwd}/build/node_modules/react/build-info.json`
  );

  // Tags are named after the react version.
  const {version} = readJsonSync(
    `${cwd}/build/node_modules/react/package.json`
  );

  clear();

  if (tags.length === 1 && tags[0] === 'canary') {
    console.log(theme.header`A canary release has been pulbished!`);
  } else {
    console.log(
      theme.caution`The release has been published but you're not done yet!`
    );

    if (tags.includes('latest')) {
      console.log();
      console.log(
        theme.header`Please review and commit all local, staged changes.`
      );

      console.log();
      console.log('Version numbers have been updated in the following files:');
      for (let i = 0; i < packages.length; i++) {
        const packageName = packages[i];
        console.log(theme.path`• packages/%s/package.json`, packageName);
      }
      console.log(theme.path`• packages/shared/ReactVersion.js`);

      console.log();
      if (environment === 'ci') {
        console.log('Auto-generated error codes have been updated as well:');
        console.log(theme.path`• scripts/error-codes/codes.json`);
      } else {
        console.log(
          theme`{caution The release that was just published was created locally.} ` +
            theme`Because of this, you will need to update the generated ` +
            theme`{path scripts/error-codes/codes.json} file manually:`
        );
        console.log(theme`  {command git checkout} {version ${commit}}`);
        console.log(theme`  {command yarn build -- --extract-errors}`);
      }
    }

    console.log();
    console.log(
      theme`{header Don't forget to update and commit the }{path CHANGELOG}`
    );

    // Prompt the release engineer to tag the commit and update the CHANGELOG.
    // (The script could automatically do this, but this seems safer.)
    console.log();
    console.log(
      theme.header`Tag the source for this release in Git with the following command:`
    );
    console.log(
      theme`  {command git tag -a v}{version %s} {command -m "v%s"} {version %s}`,
      version,
      version,
      commit
    );
    console.log(theme.command`  git push origin --tags`);

    console.log();
    console.log(theme.header`Lastly, please fill in the release on GitHub:`);
    console.log(
      theme.link`https://github.com/facebook/react/releases/tag/v%s`,
      version
    );
    console.log();
  }
};

module.exports = run;
<commit_msg>Tweaked publish canary message to show newly published version<commit_after>#!/usr/bin/env node

'use strict';

const clear = require('clear');
const {readJsonSync} = require('fs-extra');
const theme = require('../theme');

const run = async ({cwd, packages, tags}) => {
  // All packages are built from a single source revision,
  // so it is safe to read the commit number from any one of them.
  const {commit, environment} = readJsonSync(
    `${cwd}/build/node_modules/react/build-info.json`
  );

  // Tags are named after the react version.
  const {version} = readJsonSync(
    `${cwd}/build/node_modules/react/package.json`
  );

  clear();

  if (tags.length === 1 && tags[0] === 'canary') {
    console.log(
      theme`{header A canary release} {version ${version}} {header has been published!}`
    );
  } else {
    console.log(
      theme.caution`The release has been published but you're not done yet!`
    );

    if (tags.includes('latest')) {
      console.log();
      console.log(
        theme.header`Please review and commit all local, staged changes.`
      );

      console.log();
      console.log('Version numbers have been updated in the following files:');
      for (let i = 0; i < packages.length; i++) {
        const packageName = packages[i];
        console.log(theme.path`• packages/%s/package.json`, packageName);
      }
      console.log(theme.path`• packages/shared/ReactVersion.js`);

      console.log();
      if (environment === 'ci') {
        console.log('Auto-generated error codes have been updated as well:');
        console.log(theme.path`• scripts/error-codes/codes.json`);
      } else {
        console.log(
          theme`{caution The release that was just published was created locally.} ` +
            theme`Because of this, you will need to update the generated ` +
            theme`{path scripts/error-codes/codes.json} file manually:`
        );
        console.log(theme`  {command git checkout} {version ${commit}}`);
        console.log(theme`  {command yarn build -- --extract-errors}`);
      }
    }

    console.log();
    console.log(
      theme`{header Don't forget to update and commit the }{path CHANGELOG}`
    );

    // Prompt the release engineer to tag the commit and update the CHANGELOG.
    // (The script could automatically do this, but this seems safer.)
    console.log();
    console.log(
      theme.header`Tag the source for this release in Git with the following command:`
    );
    console.log(
      theme`  {command git tag -a v}{version %s} {command -m "v%s"} {version %s}`,
      version,
      version,
      commit
    );
    console.log(theme.command`  git push origin --tags`);

    console.log();
    console.log(theme.header`Lastly, please fill in the release on GitHub:`);
    console.log(
      theme.link`https://github.com/facebook/react/releases/tag/v%s`,
      version
    );
    console.log();
  }
};

module.exports = run;
<|endoftext|>


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

export { default as SetupAccountApproved } from './SetupAccountApproved';
export { default as SetupAccountCreate } from './SetupAccountCreate';
export { default as SetupAccountDisapproved } from './SetupAccountDisapproved';
export { default as SetupAccountNoClient } from './SetupAccountNoClient';
export { default as SetupAccountPending } from './SetupAccountPending';
export { default as SetupAccountSelect } from './SetupAccountSelect';
export { default as SetupMain } from './SetupMain';
export { default as SetupSiteAdd } from './SetupSiteAdd';
export { default as SetupSiteAdded } from './SetupSiteAdded';
<commit_msg>Export v2 as default. That fixes the all modules tests.<commit_after>/**
 * AdSense Setup components.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

export { default as SetupAccountApproved } from './SetupAccountApproved';
export { default as SetupAccountCreate } from './SetupAccountCreate';
export { default as SetupAccountDisapproved } from './SetupAccountDisapproved';
export { default as SetupAccountNoClient } from './SetupAccountNoClient';
export { default as SetupAccountPending } from './SetupAccountPending';
export { default as SetupAccountSelect } from './SetupAccountSelect';
export { default as SetupMain } from './SetupMain';
export { default as SetupSiteAdd } from './SetupSiteAdd';
export { default as SetupSiteAdded } from './SetupSiteAdded';
export { default as v2 } from './v2/SetupUseSnippetSwitch';
<|endoftext|>


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

/**
 * External dependencies
 */
import invariant from 'invariant';
import { groupBy } from 'lodash';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { isValidAccountID, isValidPropertyID, parsePropertyID, isValidPropertySelection } from '../util';
import { STORE_NAME, PROPERTY_CREATE, PROFILE_CREATE } from './constants';
const { createRegistryControl } = Data;

// Actions
const FETCH_CREATE_PROPERTY = 'FETCH_CREATE_PROPERTY';
const START_FETCH_CREATE_PROPERTY = 'START_FETCH_CREATE_PROPERTY';
const FINISH_FETCH_CREATE_PROPERTY = 'FINISH_FETCH_CREATE_PROPERTY';
const CATCH_FETCH_CREATE_PROPERTY = 'CATCH_FETCH_CREATE_PROPERTY';

const FETCH_PROPERTIES_PROFILES = 'FETCH_PROPERTIES_PROFILES';
const START_FETCH_PROPERTIES_PROFILES = 'START_FETCH_PROPERTIES_PROFILES';
const FINISH_FETCH_PROPERTIES_PROFILES = 'FINISH_FETCH_PROPERTIES_PROFILES';
const CATCH_FETCH_PROPERTIES_PROFILES = 'CATCH_FETCH_PROPERTIES_PROFILES';

const RECEIVE_CREATE_PROPERTY = 'RECEIVE_CREATE_PROPERTY';
const RECEIVE_MATCHED_PROPERTY = 'RECEIVE_MATCHED_PROPERTY';
const RECEIVE_PROPERTIES = 'RECEIVE_PROPERTIES';
const WAIT_FOR_PROPERTIES = 'WAIT_FOR_PROPERTIES';

export const INITIAL_STATE = {
	isFetchingCreateProperty: {},
	isFetchingPropertiesProfiles: {},
	properties: {},
	matchedProperty: undefined,
};

export const actions = {
	/**
	 * Creates a new Analytics property.
	 *
	 * Creates a new Analytics property for an existing Google Analytics account.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} accountID Google Analytics account ID.
	 * @return {Object} Redux-style action.
	 */
	*createProperty( accountID ) {
		invariant( accountID, 'accountID is required.' );
		let response, error;

		yield {
			payload: { accountID },
			type: START_FETCH_CREATE_PROPERTY,
		};

		try {
			response = yield {
				payload: { accountID },
				type: FETCH_CREATE_PROPERTY,
			};
			const property = response;

			yield actions.receiveCreateProperty( { accountID, property } );

			yield {
				payload: { accountID },
				type: FINISH_FETCH_CREATE_PROPERTY,
			};
		} catch ( e ) {
			error = e;
			yield {
				payload: {
					accountID,
					error,
				},
				type: CATCH_FETCH_CREATE_PROPERTY,
			};
		}

		return { response, error };
	},

	*fetchPropertiesProfiles( accountID ) {
		invariant( accountID, 'accountID is required.' );
		let response, error;

		yield {
			payload: { accountID },
			type: START_FETCH_PROPERTIES_PROFILES,
		};

		try {
			response = yield {
				payload: { accountID },
				type: FETCH_PROPERTIES_PROFILES,
			};
			const { properties, profiles, matchedProperty } = response;
			const { dispatch } = yield Data.commonActions.getRegistry();
			yield actions.receiveProperties( properties );
			dispatch( STORE_NAME ).receiveProfiles( profiles );

			if ( matchedProperty ) {
				yield actions.receiveMatchedProperty( matchedProperty );
			}

			yield {
				payload: { accountID },
				type: FINISH_FETCH_PROPERTIES_PROFILES,
			};
		} catch ( e ) {
			error = e;
			yield {
				payload: {
					accountID,
					error,
				},
				type: CATCH_FETCH_PROPERTIES_PROFILES,
			};
		}

		return { response, error };
	},

	/**
	 * Adds a property to the data store.
	 *
	 * Adds the newly-created property to the existing properties in
	 * the data store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} args           Argument params.
	 * @param {string} args.accountID Google Analytics account ID.
	 * @param {Object} args.property  Google Analytics property object.
	 * @return {Object} Redux-style action.
	 */
	receiveCreateProperty( { accountID, property } ) {
		invariant( accountID, 'accountID is required.' );
		invariant( property, 'property is required.' );

		return {
			payload: { accountID, property },
			type: RECEIVE_CREATE_PROPERTY,
		};
	},

	/**
	 * Adds a matchedProperty to the store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} matchedProperty Property object.
	 * @return {Object} Redux-style action.
	 */
	receiveMatchedProperty( matchedProperty ) {
		invariant( matchedProperty, 'matchedProperty is required.' );

		return {
			payload: { matchedProperty },
			type: RECEIVE_MATCHED_PROPERTY,
		};
	},

	/**
	 * Sets the given property and related fields in the store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {string} propertyID Property ID to select.
	 * @param {string} [internalPropertyID] Internal property ID (if available).
	 */
	*selectProperty( propertyID, internalPropertyID = '' ) {
		invariant( isValidPropertySelection( propertyID ), 'A valid propertyID selection is required.' );

		const registry = yield Data.commonActions.getRegistry();
		registry.dispatch( STORE_NAME ).setPropertyID( propertyID );

		if ( PROPERTY_CREATE === propertyID ) {
			registry.dispatch( STORE_NAME ).setProfileID( PROFILE_CREATE );
			return;
		}

		const { accountID } = parsePropertyID( propertyID );

		yield actions.waitForProperties( accountID );
		const property = registry.select( STORE_NAME ).getPropertyByID( propertyID ) || {};

		if ( ! internalPropertyID ) {
			internalPropertyID = property.internalWebPropertyId;
		}

		registry.dispatch( STORE_NAME ).setInternalWebPropertyID( internalPropertyID || '' );

		// If defaultProfileId is set then set the	 profileID and return
		if ( property.defaultProfileId ) {
			const profileID = property.defaultProfileId;
			registry.dispatch( STORE_NAME ).setProfileID( profileID );
			return;
		}

		// Clear any profile ID selection in the case that selection falls to the getProfiles resolver.
		registry.dispatch( STORE_NAME ).setProfileID( '' );

		const profiles = registry.select( STORE_NAME ).getProfiles( propertyID );
		if ( profiles === undefined ) {
			return; // Selection will happen in in getProfiles resolver.
		}

		const matchedProfile = profiles.find( ( { webPropertyId } ) => webPropertyId === propertyID ) || { id: PROFILE_CREATE }; // Capitalization rule exception: webPropertyId

		registry.dispatch( STORE_NAME ).setProfileID( matchedProfile.id );
	},

	/**
	 * Adds properties to the store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Array} properties Properties to add.
	 * @return {Object} Redux-style action.
	 */
	receiveProperties( properties ) {
		invariant( Array.isArray( properties ), 'properties must be an array.' );

		return {
			payload: { properties },
			type: RECEIVE_PROPERTIES,
		};
	},

	waitForProperties( accountID ) {
		return {
			payload: { accountID },
			type: WAIT_FOR_PROPERTIES,
		};
	},
};

export const controls = {
	[ FETCH_CREATE_PROPERTY ]: ( { payload: { accountID } } ) => {
		return API.set( 'modules', 'analytics', 'create-property', { accountID } );
	},
	[ FETCH_PROPERTIES_PROFILES ]: ( { payload: { accountID } } ) => {
		return API.get( 'modules', 'analytics', 'properties-profiles', { accountID }, {
			useCache: false,
		} );
	},
	[ WAIT_FOR_PROPERTIES ]: createRegistryControl( ( registry ) => ( { payload: { accountID } } ) => {
		const arePropertiesLoaded = () => registry.select( STORE_NAME ).getProperties( accountID ) !== undefined;

		if ( arePropertiesLoaded() ) {
			return true;
		}

		return new Promise( ( resolve ) => {
			const unsubscribe = registry.subscribe( () => {
				if ( arePropertiesLoaded() ) {
					unsubscribe();
					resolve();
				}
			} );
		} );
	} ),
};

export const reducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case START_FETCH_CREATE_PROPERTY: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingCreateProperty: {
					...state.isFetchingCreateProperty,
					[ accountID ]: true,
				},
			};
		}

		case START_FETCH_PROPERTIES_PROFILES: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingPropertiesProfiles: {
					...state.isFetchingPropertiesProfiles,
					[ accountID ]: true,
				},
			};
		}

		case FINISH_FETCH_CREATE_PROPERTY: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingCreateProperty: {
					...state.isFetchingCreateProperty,
					[ accountID ]: false,
				},
			};
		}

		case RECEIVE_CREATE_PROPERTY: {
			const { accountID, property } = payload;

			return {
				...state,
				properties: {
					...state.properties || {},
					[ accountID ]: [
						...( state.properties || {} )[ accountID ] || [],
						property,
					],
				},
			};
		}

		case CATCH_FETCH_CREATE_PROPERTY: {
			const { accountID, error } = payload;

			return {
				...state,
				error,
				isFetchingCreateProperty: {
					...state.isFetchingCreateProperty,
					[ accountID ]: false,
				},
			};
		}

		case RECEIVE_MATCHED_PROPERTY: {
			const { matchedProperty } = payload;

			return {
				...state,
				matchedProperty,
			};
		}

		case RECEIVE_PROPERTIES: {
			const { properties } = payload;

			return {
				...state,
				properties: {
					...state.properties,
					...groupBy( properties, 'accountId' ), // Capitalization rule exception: `accountId` is a property of an API returned value.
				},
			};
		}

		case FINISH_FETCH_PROPERTIES_PROFILES: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingPropertiesProfiles: {
					...state.isFetchingPropertiesProfiles,
					[ accountID ]: false,
				},
			};
		}

		case CATCH_FETCH_PROPERTIES_PROFILES: {
			const { accountID, error } = payload;

			return {
				...state,
				error,
				isFetchingPropertiesProfiles: {
					...state.isFetchingPropertiesProfiles,
					[ accountID ]: false,
				},
			};
		}

		default: {
			return { ...state };
		}
	}
};

export const resolvers = {
	*getProperties( accountID ) {
		if ( ! isValidAccountID( accountID ) ) {
			return;
		}

		const registry = yield Data.commonActions.getRegistry();
		let properties = registry.select( STORE_NAME ).getProperties( accountID );

		// Only fetch properties if there are none in the store for the given account.
		if ( ! properties ) {
			const { response } = yield actions.fetchPropertiesProfiles( accountID );
			if ( response && response.properties ) {
				( { properties } = response );
			}
		}

		const propertyID = registry.select( STORE_NAME ).getPropertyID();
		if ( ! propertyID ) {
			const property = properties[ 0 ] || { id: PROPERTY_CREATE };
			yield actions.selectProperty( property.id, property.internalWebPropertyId ); // Capitalization rule exception: internalWebPropertyId
		}
	},
};

export const selectors = {
	/**
	 * Gets the property object by the property ID.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @param {string} propertyID Property ID.
	 * @return {?Object} Property object, or undefined if not present in store.
	 */
	getPropertyByID( state, propertyID ) {
		if ( ! isValidPropertyID( propertyID ) ) {
			return undefined;
		}
		const { accountID } = parsePropertyID( propertyID );

		return ( state.properties[ accountID ] || [] ).find( ( { id } ) => id === propertyID );
	},
	/**
	 * Gets the matched property, if any.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {?Object} Matched property if set, otherwise `undefined`.
	 */
	getMatchedProperty( state ) {
		return state.matchedProperty;
	},
	/**
	 * Get all Google Analytics properties this account can access.
	 *
	 * Returns an array of all analytics properties.
	 *
	 * Returns `undefined` if accounts have not yet loaded.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to fetch properties for.
	 * @return {?Array.<Object>} An array of Analytics properties; `undefined` if not loaded.
	 */
	getProperties( state, accountID ) {
		const { properties } = state;

		return properties[ accountID ];
	},

	/**
	 * Checks if a property is being created for an account.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if creating a property, `false` if not.
	 */
	isDoingCreateProperty( state, accountID ) {
		const { isFetchingCreateProperty } = state;

		return !! isFetchingCreateProperty[ accountID ];
	},

	/**
	 * Checks if properties are being fetched for the given account.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if fetching a properties, `false` if not.
	 */
	isDoingGetProperties( state, accountID ) {
		const { isFetchingPropertiesProfiles } = state;

		return !! isFetchingPropertiesProfiles[ accountID ];
	},
};

export default {
	INITIAL_STATE,
	actions,
	controls,
	reducer,
	resolvers,
	selectors,
};
<commit_msg>PR fixes to comments and streemlining code<commit_after>/**
 * modules/analytics data store: properties.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';
import { groupBy } from 'lodash';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { isValidAccountID, isValidPropertyID, parsePropertyID, isValidPropertySelection } from '../util';
import { STORE_NAME, PROPERTY_CREATE, PROFILE_CREATE } from './constants';
const { createRegistryControl } = Data;

// Actions
const FETCH_CREATE_PROPERTY = 'FETCH_CREATE_PROPERTY';
const START_FETCH_CREATE_PROPERTY = 'START_FETCH_CREATE_PROPERTY';
const FINISH_FETCH_CREATE_PROPERTY = 'FINISH_FETCH_CREATE_PROPERTY';
const CATCH_FETCH_CREATE_PROPERTY = 'CATCH_FETCH_CREATE_PROPERTY';

const FETCH_PROPERTIES_PROFILES = 'FETCH_PROPERTIES_PROFILES';
const START_FETCH_PROPERTIES_PROFILES = 'START_FETCH_PROPERTIES_PROFILES';
const FINISH_FETCH_PROPERTIES_PROFILES = 'FINISH_FETCH_PROPERTIES_PROFILES';
const CATCH_FETCH_PROPERTIES_PROFILES = 'CATCH_FETCH_PROPERTIES_PROFILES';

const RECEIVE_CREATE_PROPERTY = 'RECEIVE_CREATE_PROPERTY';
const RECEIVE_MATCHED_PROPERTY = 'RECEIVE_MATCHED_PROPERTY';
const RECEIVE_PROPERTIES = 'RECEIVE_PROPERTIES';
const WAIT_FOR_PROPERTIES = 'WAIT_FOR_PROPERTIES';

export const INITIAL_STATE = {
	isFetchingCreateProperty: {},
	isFetchingPropertiesProfiles: {},
	properties: {},
	matchedProperty: undefined,
};

export const actions = {
	/**
	 * Creates a new Analytics property.
	 *
	 * Creates a new Analytics property for an existing Google Analytics account.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} accountID Google Analytics account ID.
	 * @return {Object} Redux-style action.
	 */
	*createProperty( accountID ) {
		invariant( accountID, 'accountID is required.' );
		let response, error;

		yield {
			payload: { accountID },
			type: START_FETCH_CREATE_PROPERTY,
		};

		try {
			response = yield {
				payload: { accountID },
				type: FETCH_CREATE_PROPERTY,
			};
			const property = response;

			yield actions.receiveCreateProperty( { accountID, property } );

			yield {
				payload: { accountID },
				type: FINISH_FETCH_CREATE_PROPERTY,
			};
		} catch ( e ) {
			error = e;
			yield {
				payload: {
					accountID,
					error,
				},
				type: CATCH_FETCH_CREATE_PROPERTY,
			};
		}

		return { response, error };
	},

	*fetchPropertiesProfiles( accountID ) {
		invariant( accountID, 'accountID is required.' );
		let response, error;

		yield {
			payload: { accountID },
			type: START_FETCH_PROPERTIES_PROFILES,
		};

		try {
			response = yield {
				payload: { accountID },
				type: FETCH_PROPERTIES_PROFILES,
			};
			const { properties, profiles, matchedProperty } = response;
			const { dispatch } = yield Data.commonActions.getRegistry();
			yield actions.receiveProperties( properties );
			dispatch( STORE_NAME ).receiveProfiles( profiles );

			if ( matchedProperty ) {
				yield actions.receiveMatchedProperty( matchedProperty );
			}

			yield {
				payload: { accountID },
				type: FINISH_FETCH_PROPERTIES_PROFILES,
			};
		} catch ( e ) {
			error = e;
			yield {
				payload: {
					accountID,
					error,
				},
				type: CATCH_FETCH_PROPERTIES_PROFILES,
			};
		}

		return { response, error };
	},

	/**
	 * Adds a property to the data store.
	 *
	 * Adds the newly-created property to the existing properties in
	 * the data store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} args           Argument params.
	 * @param {string} args.accountID Google Analytics account ID.
	 * @param {Object} args.property  Google Analytics property object.
	 * @return {Object} Redux-style action.
	 */
	receiveCreateProperty( { accountID, property } ) {
		invariant( accountID, 'accountID is required.' );
		invariant( property, 'property is required.' );

		return {
			payload: { accountID, property },
			type: RECEIVE_CREATE_PROPERTY,
		};
	},

	/**
	 * Adds a matchedProperty to the store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} matchedProperty Property object.
	 * @return {Object} Redux-style action.
	 */
	receiveMatchedProperty( matchedProperty ) {
		invariant( matchedProperty, 'matchedProperty is required.' );

		return {
			payload: { matchedProperty },
			type: RECEIVE_MATCHED_PROPERTY,
		};
	},

	/**
	 * Sets the given property and related fields in the store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {string} propertyID Property ID to select.
	 * @param {string} [internalPropertyID] Internal property ID (if available).
	 */
	*selectProperty( propertyID, internalPropertyID = '' ) {
		invariant( isValidPropertySelection( propertyID ), 'A valid propertyID selection is required.' );

		const registry = yield Data.commonActions.getRegistry();
		registry.dispatch( STORE_NAME ).setPropertyID( propertyID );

		if ( PROPERTY_CREATE === propertyID ) {
			registry.dispatch( STORE_NAME ).setProfileID( PROFILE_CREATE );
			return;
		}

		const { accountID } = parsePropertyID( propertyID );

		yield actions.waitForProperties( accountID );
		const property = registry.select( STORE_NAME ).getPropertyByID( propertyID ) || {};

		if ( ! internalPropertyID ) {
			internalPropertyID = property.internalWebPropertyId;
		}

		registry.dispatch( STORE_NAME ).setInternalWebPropertyID( internalPropertyID || '' );

		if ( property.defaultProfileId ) {
			registry.dispatch( STORE_NAME ).setProfileID( property.defaultProfileId ); // Capitalization rule exception: defaultProfileId
			return;
		}

		// Clear any profile ID selection in the case that selection falls to the getProfiles resolver.
		registry.dispatch( STORE_NAME ).setProfileID( '' );

		const profiles = registry.select( STORE_NAME ).getProfiles( propertyID );
		if ( profiles === undefined ) {
			return; // Selection will happen in in getProfiles resolver.
		}

		const matchedProfile = profiles.find( ( { webPropertyId } ) => webPropertyId === propertyID ) || { id: PROFILE_CREATE }; // Capitalization rule exception: webPropertyId

		registry.dispatch( STORE_NAME ).setProfileID( matchedProfile.id );
	},

	/**
	 * Adds properties to the store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Array} properties Properties to add.
	 * @return {Object} Redux-style action.
	 */
	receiveProperties( properties ) {
		invariant( Array.isArray( properties ), 'properties must be an array.' );

		return {
			payload: { properties },
			type: RECEIVE_PROPERTIES,
		};
	},

	waitForProperties( accountID ) {
		return {
			payload: { accountID },
			type: WAIT_FOR_PROPERTIES,
		};
	},
};

export const controls = {
	[ FETCH_CREATE_PROPERTY ]: ( { payload: { accountID } } ) => {
		return API.set( 'modules', 'analytics', 'create-property', { accountID } );
	},
	[ FETCH_PROPERTIES_PROFILES ]: ( { payload: { accountID } } ) => {
		return API.get( 'modules', 'analytics', 'properties-profiles', { accountID }, {
			useCache: false,
		} );
	},
	[ WAIT_FOR_PROPERTIES ]: createRegistryControl( ( registry ) => ( { payload: { accountID } } ) => {
		const arePropertiesLoaded = () => registry.select( STORE_NAME ).getProperties( accountID ) !== undefined;

		if ( arePropertiesLoaded() ) {
			return true;
		}

		return new Promise( ( resolve ) => {
			const unsubscribe = registry.subscribe( () => {
				if ( arePropertiesLoaded() ) {
					unsubscribe();
					resolve();
				}
			} );
		} );
	} ),
};

export const reducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case START_FETCH_CREATE_PROPERTY: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingCreateProperty: {
					...state.isFetchingCreateProperty,
					[ accountID ]: true,
				},
			};
		}

		case START_FETCH_PROPERTIES_PROFILES: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingPropertiesProfiles: {
					...state.isFetchingPropertiesProfiles,
					[ accountID ]: true,
				},
			};
		}

		case FINISH_FETCH_CREATE_PROPERTY: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingCreateProperty: {
					...state.isFetchingCreateProperty,
					[ accountID ]: false,
				},
			};
		}

		case RECEIVE_CREATE_PROPERTY: {
			const { accountID, property } = payload;

			return {
				...state,
				properties: {
					...state.properties || {},
					[ accountID ]: [
						...( state.properties || {} )[ accountID ] || [],
						property,
					],
				},
			};
		}

		case CATCH_FETCH_CREATE_PROPERTY: {
			const { accountID, error } = payload;

			return {
				...state,
				error,
				isFetchingCreateProperty: {
					...state.isFetchingCreateProperty,
					[ accountID ]: false,
				},
			};
		}

		case RECEIVE_MATCHED_PROPERTY: {
			const { matchedProperty } = payload;

			return {
				...state,
				matchedProperty,
			};
		}

		case RECEIVE_PROPERTIES: {
			const { properties } = payload;

			return {
				...state,
				properties: {
					...state.properties,
					...groupBy( properties, 'accountId' ), // Capitalization rule exception: `accountId` is a property of an API returned value.
				},
			};
		}

		case FINISH_FETCH_PROPERTIES_PROFILES: {
			const { accountID } = payload;

			return {
				...state,
				isFetchingPropertiesProfiles: {
					...state.isFetchingPropertiesProfiles,
					[ accountID ]: false,
				},
			};
		}

		case CATCH_FETCH_PROPERTIES_PROFILES: {
			const { accountID, error } = payload;

			return {
				...state,
				error,
				isFetchingPropertiesProfiles: {
					...state.isFetchingPropertiesProfiles,
					[ accountID ]: false,
				},
			};
		}

		default: {
			return { ...state };
		}
	}
};

export const resolvers = {
	*getProperties( accountID ) {
		if ( ! isValidAccountID( accountID ) ) {
			return;
		}

		const registry = yield Data.commonActions.getRegistry();
		let properties = registry.select( STORE_NAME ).getProperties( accountID );

		// Only fetch properties if there are none in the store for the given account.
		if ( ! properties ) {
			const { response } = yield actions.fetchPropertiesProfiles( accountID );
			if ( response && response.properties ) {
				( { properties } = response );
			}
		}

		const propertyID = registry.select( STORE_NAME ).getPropertyID();
		if ( ! propertyID ) {
			const property = properties[ 0 ] || { id: PROPERTY_CREATE };
			yield actions.selectProperty( property.id, property.internalWebPropertyId ); // Capitalization rule exception: internalWebPropertyId
		}
	},
};

export const selectors = {
	/**
	 * Gets the property object by the property ID.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @param {string} propertyID Property ID.
	 * @return {?Object} Property object, or undefined if not present in store.
	 */
	getPropertyByID( state, propertyID ) {
		if ( ! isValidPropertyID( propertyID ) ) {
			return undefined;
		}
		const { accountID } = parsePropertyID( propertyID );

		return ( state.properties[ accountID ] || [] ).find( ( { id } ) => id === propertyID );
	},
	/**
	 * Gets the matched property, if any.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {?Object} Matched property if set, otherwise `undefined`.
	 */
	getMatchedProperty( state ) {
		return state.matchedProperty;
	},
	/**
	 * Get all Google Analytics properties this account can access.
	 *
	 * Returns an array of all analytics properties.
	 *
	 * Returns `undefined` if accounts have not yet loaded.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to fetch properties for.
	 * @return {?Array.<Object>} An array of Analytics properties; `undefined` if not loaded.
	 */
	getProperties( state, accountID ) {
		const { properties } = state;

		return properties[ accountID ];
	},

	/**
	 * Checks if a property is being created for an account.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if creating a property, `false` if not.
	 */
	isDoingCreateProperty( state, accountID ) {
		const { isFetchingCreateProperty } = state;

		return !! isFetchingCreateProperty[ accountID ];
	},

	/**
	 * Checks if properties are being fetched for the given account.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state     Data store's state.
	 * @param {string} accountID The Analytics Account ID to check for property creation.
	 * @return {boolean} `true` if fetching a properties, `false` if not.
	 */
	isDoingGetProperties( state, accountID ) {
		const { isFetchingPropertiesProfiles } = state;

		return !! isFetchingPropertiesProfiles[ accountID ];
	},
};

export default {
	INITIAL_STATE,
	actions,
	controls,
	reducer,
	resolvers,
	selectors,
};
<|endoftext|>


<commit_before>var DatosActividad = '<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?><actividad tipo="RelacionarColumnas">\n<!-- Parte generica -->\n	<!-- Indica que color de fondo se le pondra al flash (por defecto 0x8993A0) -->\n	<color_fondo>0x8199d0</color_fondo>\n	<!-- Indica que color se le pondra a los elementos del flash (por defecto 0x728F61) -->\n	<color_botones>0xF3F3F3</color_botones>\n    <!-- Indica que color de fondo se le pondra al html5 (por defecto 0x7cb139) -->\n	<color_fondo_h>0x8199d0</color_fondo_h>\n	<!-- Indica que color se le pondra a los elementos del html5 (por defecto 0xFCA800) -->\n	<color_botones_h>0xF3F3F3</color_botones_h>\n    <!-- Indica el logo personalizado si lo hay -->\n    <logoPersonalizado>no</logoPersonalizado>\n	<!-- Indica si se mostrara al alumno la respuesta correcta o no, en caso de que el alumno no supere el ejercicio -->\n	<correccion>si</correccion>\n	<numero_intentos>3</numero_intentos>\n	<!-- Indica si tiene un tiempo maximo para realizar la actividad, estaria definido en segundos -->\n	<tiempo maximo="0">no</tiempo>\n	\n	<!-- Parametros nuevos - dic 2012 -->\n	<autor>Gabriel Durán</autor>\n	<descripcionUsuario>Agrupa as diferentes formas verbais con cadansúa persoa</descripcionUsuario>\n    <descripcionUsuarioLimpio>Agrupa as diferentes formas verbais con cadansúa persoa</descripcionUsuarioLimpio>\n	<origen_recursos></origen_recursos><xorigen_recursos>https://d3c41a1a64ri6i.cloudfront.net/recursos/89/2859682/</xorigen_recursos>\n	\n      <registro>\n		<!-- Indica el tipo de registro:\n		- script -> registro a traves de un script (php, jsp, etx)\n		- scorm -> registreo a traves de scorp\n		-->\n		<tipo>scorm</tipo>\n		<!-- Indica la url del scrip, en caso de que el tipo sea script	-->\n		<url></url>\n		<!-- Indica los parametros a pasar al script, en caso de que el tipo sea script	-->\n		<parametros>\n			<!-- La etiqueta nombre contiene el nombre del parametro a pasar -->\n			<!-- La etiqueta valor contiene el valor al que hace referencia el parametro (buscara el nombre de la varible en flash):\n			- SCORE: puntuacion de la actividad, en porcentaje\n			- TIME: tiempo de la actividad, en segundos\n			- URL: direccion de la actividad\n			-->\n			<parametro id="1">\n				<nombre>score</nombre>\n				<valor>SCORE</valor>\n			</parametro>\n			<parametro id="2">\n				<nombre>time</nombre>\n				<valor>TIME</valor>\n			</parametro>\n			<parametro id="3">\n				<nombre>url</nombre>\n				<valor>URL</valor>\n			</parametro>\n		</parametros>\n	</registro>\n    \n<!-- Parte especifica -->\n	<titulo>Instrucións</titulo>\n	<!-- Indica la forma de realizar la actividad, es decir la ayuda de la actividad -->\n	<descripcion>Fornar as parellas correctamente. Para este clic sobre un dos elementos da primeira columna e, a continuación, sobre un dos elementos da segunda columna. Deben completar os pares para completar o exercicio.</descripcion>\n	<enunciado>Relaciona estos elementos</enunciado>\n    <!-- parametro solo para la importación a ADR -->\n    <enunciado_edu>Alternancia na 1ª conxugación</enunciado_edu>\n    \n	<tituloApli>Alternancia na 1ª conxugación</tituloApli>\n    <parejas>\n    		<pareja id="1">\n			<elementoA tipo="TEXTO">1ª persoa do singular, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">Bebo</elementoB>\n		</pareja>\n    		<pareja id="2">\n			<elementoA tipo="TEXTO">2ª persoa do singular, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">B&#603;bes</elementoB>\n		</pareja>\n    		<pareja id="3">\n			<elementoA tipo="TEXTO">3ª persoa do singular, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">B&#603;be</elementoB>\n		</pareja>\n    		<pareja id="4">\n			<elementoA tipo="TEXTO">1ª persoa do plural, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">Bebemos</elementoB>\n		</pareja>\n    		<pareja id="5">\n			<elementoA tipo="TEXTO">2ª persoa do plural, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">Bebedes</elementoB>\n		</pareja>\n    		<pareja id="6">\n			<elementoA tipo="TEXTO">3ª persoa do plural, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">B&#603;ben</elementoB>\n		</pareja>\n    		<pareja id="7">\n			<elementoA tipo="TEXTO">1ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Colla</elementoB>\n		</pareja>\n    		<pareja id="8">\n			<elementoA tipo="TEXTO">2ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collas</elementoB>\n		</pareja>\n    		<pareja id="9">\n			<elementoA tipo="TEXTO">3ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Colla</elementoB>\n		</pareja>\n    		<pareja id="10">\n			<elementoA tipo="TEXTO">1ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collamos</elementoB>\n		</pareja>\n    		<pareja id="11">\n			<elementoA tipo="TEXTO">2ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collades</elementoB>\n		</pareja>\n    		<pareja id="12">\n			<elementoA tipo="TEXTO">3ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collan</elementoB>\n		</pareja>\n        </parejas>\n	<puntos>100</puntos>\n	<puntos_resta>33</puntos_resta>\n    \n	<idioma>\n		<nIntentos>intentos</nIntentos>\n		<txtPuntos>Puntuación</txtPuntos>\n		<txtTiempo>Tempo</txtTiempo>\n		<txtTiempoRestante>Tempo Restante</txtTiempoRestante>\n		<txtTituloActividadNoSuperada>Actividade non superada</txtTituloActividadNoSuperada>\n		<txtTiempoSuperado>Superaches o tempo máximo para esta actividade</txtTiempoSuperado>\n		<txtTituloRespuestaCorrecta>Resposta correcta</txtTituloRespuestaCorrecta>\n		<txtCerrar>Pechar</txtCerrar>\n    	<txtSuperadoNumeroIntentos>Unha mágoa! Para a próxima seguro que pasas</txtSuperadoNumeroIntentos>\n		<txtTituloActividadSuperada>Actividade Pasado</txtTituloActividadSuperada>\n		<txtActividadSuperada>Noraboa! Superaches esta actividade</txtActividadSuperada>\n		<txtRespuestaIncorrecta>Non completou correctamente o grupo</txtRespuestaIncorrecta>\n  		<txtTituloRespuestaIncorrecta>Resposta incorrecta</txtTituloRespuestaIncorrecta>\n  		\n  		<txtGrupoCorrecto>Grupo Certo</txtGrupoCorrecto>\n		<txtBoxRespuestaCorrecta>Noraboa! Superaches esta actividade</txtBoxRespuestaCorrecta>\n		<txtTituloSuperadoNumeroIntentos>Unha mágoa! Para á próxima</txtTituloSuperadoNumeroIntentos>    \n			\n		<txtAceptar>Aceptar</txtAceptar>\n		<txtTiempoMaximo>Tempo máximo</txtTiempoMaximo>\n		<txtSensible>Sensible</txtSensible>\n		<txtMayusculasMinusculas>Maiúsculas/ minúsculas</txtMayusculasMinusculas>\n		<txtAcentos>Acentos</txtAcentos>\n		<txtComenzar>Comezar</txtComenzar>\n		<txtAutor>Autor</txtAutor>\n		<txtAyuda>Axuda</txtAyuda>\n		<txtMostrarMas>Amosar máis</txtMostrarMas>\n		<txtMostrarMenos>Amosar menos</txtMostrarMenos>\n		<txtReiniciar>Reinicie</txtReiniciar>\n		<txtVolverJugar>Xoga outra vez</txtVolverJugar>\n		<txtResponder>Responder</txtResponder>\n		<txtRegistrarse>Rexistrarse</txtRegistrarse>\n		<txtAcceder>Acceder</txtAcceder>\n		<txtCompartirResultado>Comparte os resultados</txtCompartirResultado>\n		<txtPantallaCompleta>Pantalla completa</txtPantallaCompleta>\n		<txtImprimir>Imprimir</txtImprimir>\n		<txtGrupo>Grupo</txtGrupo>\n		<txtHuecos>Buratos</txtHuecos>\n		<txtNormal>Normal</txtNormal>\n		<txtElementos>Elementos</txtElementos>\n		<txtTradicional>Tradicional</txtTradicional>\n	</idioma>\n\n</actividad>';<commit_msg>revisión de actividades<commit_after>var DatosActividad = '<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?><actividad tipo="RelacionarColumnas">\n<!-- Parte generica -->\n	<!-- Indica que color de fondo se le pondra al flash (por defecto 0x8993A0) -->\n	<color_fondo>0x8199d0</color_fondo>\n	<!-- Indica que color se le pondra a los elementos del flash (por defecto 0x728F61) -->\n	<color_botones>0xF3F3F3</color_botones>\n    <!-- Indica que color de fondo se le pondra al html5 (por defecto 0x7cb139) -->\n	<color_fondo_h>0x8199d0</color_fondo_h>\n	<!-- Indica que color se le pondra a los elementos del html5 (por defecto 0xFCA800) -->\n	<color_botones_h>0xF3F3F3</color_botones_h>\n    <!-- Indica el logo personalizado si lo hay -->\n    <logoPersonalizado>no</logoPersonalizado>\n	<!-- Indica si se mostrara al alumno la respuesta correcta o no, en caso de que el alumno no supere el ejercicio -->\n	<correccion>si</correccion>\n	<numero_intentos>3</numero_intentos>\n	<!-- Indica si tiene un tiempo maximo para realizar la actividad, estaria definido en segundos -->\n	<tiempo maximo="0">no</tiempo>\n	\n	<!-- Parametros nuevos - dic 2012 -->\n	<autor>Gabriel Durán</autor>\n	<descripcionUsuario>Agrupa as diferentes formas verbais con cadansúa persoa</descripcionUsuario>\n    <descripcionUsuarioLimpio>Agrupa as diferentes formas verbais con cadansúa persoa</descripcionUsuarioLimpio>\n	<origen_recursos></origen_recursos><xorigen_recursos>https://d3c41a1a64ri6i.cloudfront.net/recursos/89/2859682/</xorigen_recursos>\n	\n      <registro>\n		<!-- Indica el tipo de registro:\n		- script -> registro a traves de un script (php, jsp, etx)\n		- scorm -> registreo a traves de scorp\n		-->\n		<tipo>scorm</tipo>\n		<!-- Indica la url del scrip, en caso de que el tipo sea script	-->\n		<url></url>\n		<!-- Indica los parametros a pasar al script, en caso de que el tipo sea script	-->\n		<parametros>\n			<!-- La etiqueta nombre contiene el nombre del parametro a pasar -->\n			<!-- La etiqueta valor contiene el valor al que hace referencia el parametro (buscara el nombre de la varible en flash):\n			- SCORE: puntuacion de la actividad, en porcentaje\n			- TIME: tiempo de la actividad, en segundos\n			- URL: direccion de la actividad\n			-->\n			<parametro id="1">\n				<nombre>score</nombre>\n				<valor>SCORE</valor>\n			</parametro>\n			<parametro id="2">\n				<nombre>time</nombre>\n				<valor>TIME</valor>\n			</parametro>\n			<parametro id="3">\n				<nombre>url</nombre>\n				<valor>URL</valor>\n			</parametro>\n		</parametros>\n	</registro>\n    \n<!-- Parte especifica -->\n	<titulo>Instrucións</titulo>\n	<!-- Indica la forma de realizar la actividad, es decir la ayuda de la actividad -->\n	<descripcion>Fornar as parellas correctamente. Para este clic sobre un dos elementos da primeira columna e, a continuación, sobre un dos elementos da segunda columna. Deben completar os pares para completar o exercicio.</descripcion>\n	<enunciado>Relaciona estos elementos</enunciado>\n    <!-- parametro solo para la importación a ADR -->\n    <enunciado_edu>Alternancia na 2ª conxugación</enunciado_edu>\n    \n	<tituloApli>Alternancia na 2ª conxugación</tituloApli>\n    <parejas>\n    		<pareja id="1">\n			<elementoA tipo="TEXTO">1ª persoa do singular, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">Bebo</elementoB>\n		</pareja>\n    		<pareja id="2">\n			<elementoA tipo="TEXTO">2ª persoa do singular, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">B&#603;bes</elementoB>\n		</pareja>\n    		<pareja id="3">\n			<elementoA tipo="TEXTO">3ª persoa do singular, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">B&#603;be</elementoB>\n		</pareja>\n    		<pareja id="4">\n			<elementoA tipo="TEXTO">1ª persoa do plural, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">Bebemos</elementoB>\n		</pareja>\n    		<pareja id="5">\n			<elementoA tipo="TEXTO">2ª persoa do plural, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">Bebedes</elementoB>\n		</pareja>\n    		<pareja id="6">\n			<elementoA tipo="TEXTO">3ª persoa do plural, presente de indicativo</elementoA>\n            <elementoB tipo="TEXTO">B&#603;ben</elementoB>\n		</pareja>\n    		<pareja id="7">\n			<elementoA tipo="TEXTO">1ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Colla (1ª persoa)</elementoB>\n		</pareja>\n    		<pareja id="8">\n			<elementoA tipo="TEXTO">2ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collas</elementoB>\n		</pareja>\n    		<pareja id="9">\n			<elementoA tipo="TEXTO">3ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Colla (3º persoa)</elementoB>\n		</pareja>\n    		<pareja id="10">\n			<elementoA tipo="TEXTO">1ª persoa do plural, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collamos</elementoB>\n		</pareja>\n    		<pareja id="11">\n			<elementoA tipo="TEXTO">2ª persoa do plural, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collades</elementoB>\n		</pareja>\n    		<pareja id="12">\n			<elementoA tipo="TEXTO">3ª persoa do singular, presente de subxuntivo</elementoA>\n            <elementoB tipo="TEXTO">Collan</elementoB>\n		</pareja>\n        </parejas>\n	<puntos>100</puntos>\n	<puntos_resta>33</puntos_resta>\n    \n	<idioma>\n		<nIntentos>intentos</nIntentos>\n		<txtPuntos>Puntuación</txtPuntos>\n		<txtTiempo>Tempo</txtTiempo>\n		<txtTiempoRestante>Tempo Restante</txtTiempoRestante>\n		<txtTituloActividadNoSuperada>Actividade non superada</txtTituloActividadNoSuperada>\n		<txtTiempoSuperado>Superaches o tempo máximo para esta actividade</txtTiempoSuperado>\n		<txtTituloRespuestaCorrecta>Resposta correcta</txtTituloRespuestaCorrecta>\n		<txtCerrar>Pechar</txtCerrar>\n    	<txtSuperadoNumeroIntentos>Unha mágoa! Para a próxima seguro que pasas</txtSuperadoNumeroIntentos>\n		<txtTituloActividadSuperada>Actividade Pasado</txtTituloActividadSuperada>\n		<txtActividadSuperada>Noraboa! Superaches esta actividade</txtActividadSuperada>\n		<txtRespuestaIncorrecta>Non completou correctamente o grupo</txtRespuestaIncorrecta>\n  		<txtTituloRespuestaIncorrecta>Resposta incorrecta</txtTituloRespuestaIncorrecta>\n  		\n  		<txtGrupoCorrecto>Grupo Certo</txtGrupoCorrecto>\n		<txtBoxRespuestaCorrecta>Noraboa! Superaches esta actividade</txtBoxRespuestaCorrecta>\n		<txtTituloSuperadoNumeroIntentos>Unha mágoa! Para á próxima</txtTituloSuperadoNumeroIntentos>    \n			\n		<txtAceptar>Aceptar</txtAceptar>\n		<txtTiempoMaximo>Tempo máximo</txtTiempoMaximo>\n		<txtSensible>Sensible</txtSensible>\n		<txtMayusculasMinusculas>Maiúsculas/ minúsculas</txtMayusculasMinusculas>\n		<txtAcentos>Acentos</txtAcentos>\n		<txtComenzar>Comezar</txtComenzar>\n		<txtAutor>Autor</txtAutor>\n		<txtAyuda>Axuda</txtAyuda>\n		<txtMostrarMas>Amosar máis</txtMostrarMas>\n		<txtMostrarMenos>Amosar menos</txtMostrarMenos>\n		<txtReiniciar>Reinicie</txtReiniciar>\n		<txtVolverJugar>Xoga outra vez</txtVolverJugar>\n		<txtResponder>Responder</txtResponder>\n		<txtRegistrarse>Rexistrarse</txtRegistrarse>\n		<txtAcceder>Acceder</txtAcceder>\n		<txtCompartirResultado>Comparte os resultados</txtCompartirResultado>\n		<txtPantallaCompleta>Pantalla completa</txtPantallaCompleta>\n		<txtImprimir>Imprimir</txtImprimir>\n		<txtGrupo>Grupo</txtGrupo>\n		<txtHuecos>Buratos</txtHuecos>\n		<txtNormal>Normal</txtNormal>\n		<txtElementos>Elementos</txtElementos>\n		<txtTradicional>Tradicional</txtTradicional>\n	</idioma>\n\n</actividad>';<|endoftext|>


<commit_before>'use strict';

let util = require('./lib/util');
let Splitwise = require('./lib/splitwise').Splitwise;
let imgur = require('imgur');
let graphviz = require('graphviz');
let fs = require('fs');

let sw = null;
let _logger = null;
let swIdMap = null;  // a map of slack id => splitwise id

let addDebt = (from, to, amount, config, cb) => {

}

let listDebts = (target, fromOrTo, config, cb) => {
  let swTargetId = swIdMap[target.id]
  if (typeof swTargetId === 'undefined') {
    _logger.error(`User id ${target.id} (${target.name}) doesn't match anyone`);
  }

  sw.getGroup((error, data, _) => {
    if (error) {
      _logger.error(`Splitwise API error ${JSON.stringify(error)}`);
      return;
    }

    let groupData = data.group;

    // let debits = group.original_debts
    //   .filter(entry => entry.from == swTargetId)
    //   .map(entry => `${util.formatDebtLabel(entry, config.defaultCurrencyCode)} to ${entry.to}`)

    let credits = group.original_debts
      .filter(entry => entry.to == swTargetId)
      .map(entry => `${util.formatDebtLabel(entry, config.defaultCurrencyCode)} from ${entry.from}`)

    let formattedOutput = [util.listFormat(debits), util.listFormat(credits)].join();

    cb(formattedOutput);
  });
};

let getBalance = (target, config, cb) => {
  sw.getGroup((error, data, _) => {
    if (error) {
      _logger.error(`Splitwise API error ${JSON.stringify(error)}`);
      return;
    }

    if (!config.swIdMap[target.id]) {
      cb(null, `I don't know who ${target.real_name} is on Splitwise!`);
    }

    let memberData = JSON.parse(data).group.members
      .find(member => member.id === config.swIdMap[target.id]);

    if (memberData) {
      let processBalance = (bal) => {
        return {
          amount: util.parseAmount(bal.amount),
          currency: bal.currency_code
        };
      };
      let processedBalances = memberData.balance.map(processBalance);

      let result = {
        owes: processedBalances.filter(bal => bal.amount < 0),
        owed: processedBalances.filter(bal => bal.amount > 0)
      };

      cb(result);
    }
  });
}

let graphDebts = (fullGraph, config, cb) => {
  sw.getGroup((error, data, _) => {
    if (error) {
      _logger.error(`Splitwise API error ${JSON.stringify(error)}`);
      return;
    }

    let groupData = JSON.parse(data).group;
    let groupMembers = {};
    let balanceGraph = graphviz.digraph('Debts');

    groupData.members.forEach(member => {
      member.fullName = util.formatName(member);
      groupMembers[member.id] = member.fullName;
      member.graphNode = balanceGraph.addNode(`${member.id}`, {
        label: member.fullName
      });
    });

    (fullGraph ? groupData.original_debts : groupData.simplified_debts)
      .forEach(debt => {
        let edge = balanceGraph.addEdge(`${debt.from}`, `${debt.to}`, {
          label: util.formatAmount(debt.amount, debt.currency_code,
            config.currencies)
        });
      });

    let imgPath = `/tmp/balanceGraph${Date.now()}.png`;
    balanceGraph.output('png', (rendered) => {
      fs.writeFile(imgPath, rendered, () => {
        imgur.uploadFile(imgPath)
          .then(res => cb(res.data.link))
          .catch(error => {
            _logger.error(`Error uploading splitwise graph to imgur: ${error.message}`);
          });
      });
    });
  });
};

let slackwise = (user, users, argv, config, logger, response) => {
  argv.shift();
  let subcmd = argv.shift();
  if (subcmd === 'graph') {
    let drawFullGraph = !!argv.some(item => item === '--full');
    graphDebts(drawFullGraph, config, imgUrl => response.end(imgUrl));
  }
  else if (subcmd === 'balance') {
    let arg = argv.shift();
    let target;

    try {
      target = users[arg ? util.parseUserTag(arg) : user.id];

      if (typeof target === 'undefined') {
        throw 'Don\'t know who this is lol';
      }
    }
    catch (e) {
      response.end(`I don't know who "${arg}" is!`);
      return;
    }

    getBalance(target, config, (balances, err) => {
      if (err) {
        response.end(err);
        return;
      }

      if (balances.owes.length) {
        response.write(`${target.real_name} owes:\n`);
        response.write(balances.owes.map(bal => {
          return `• ${util.formatAmount(-bal.amount, bal.currency,
            config.currencies)}`
        }).join('\n'));
        response.write('\n\n');
      }

      if (balances.owed.length) {
        response.write(`${target.real_name} is owed:\n`);
        response.write(balances.owed.map(bal => {
          return `• ${util.formatAmount(bal.amount, bal.currency,
            config.currencies)}`
        }).join('\n'));
      }

      response.end();
    });
  }
};

slackwise.setup = (config, logger) => {
  sw = new Splitwise(config.swConsumerKey, config.swConsumerSecret,
    config.swAccessToken, config.swAccessTokenSecret, config.swGroupId);
  imgur.setClientId(config.imgurClientId);
  _logger = logger;
  swIdMap = config.swIdMap;
};

slackwise.metadata = require('./plugin');
module.exports = slackwise;
<commit_msg>Show help page from helpService<commit_after>'use strict';

let util = require('./lib/util');
let Splitwise = require('./lib/splitwise').Splitwise;
let imgur = require('imgur');
let graphviz = require('graphviz');
let fs = require('fs');

let sw = null;
let _logger = null;
let swIdMap = null;  // a map of slack id => splitwise id

let addDebt = (from, to, amount, config, cb) => {

}

let listDebts = (target, fromOrTo, config, cb) => {
  let swTargetId = swIdMap[target.id]
  if (typeof swTargetId === 'undefined') {
    _logger.error(`User id ${target.id} (${target.name}) doesn't match anyone`);
  }

  sw.getGroup((error, data, _) => {
    if (error) {
      _logger.error(`Splitwise API error ${JSON.stringify(error)}`);
      return;
    }

    let groupData = data.group;

    // let debits = group.original_debts
    //   .filter(entry => entry.from == swTargetId)
    //   .map(entry => `${util.formatDebtLabel(entry, config.defaultCurrencyCode)} to ${entry.to}`)

    let credits = group.original_debts
      .filter(entry => entry.to == swTargetId)
      .map(entry => `${util.formatDebtLabel(entry, config.defaultCurrencyCode)} from ${entry.from}`)

    let formattedOutput = [util.listFormat(debits), util.listFormat(credits)].join();

    cb(formattedOutput);
  });
};

let getBalance = (target, config, cb) => {
  sw.getGroup((error, data, _) => {
    if (error) {
      _logger.error(`Splitwise API error ${JSON.stringify(error)}`);
      return;
    }

    if (!config.swIdMap[target.id]) {
      cb(null, `I don't know who ${target.real_name} is on Splitwise!`);
    }

    let memberData = JSON.parse(data).group.members
      .find(member => member.id === config.swIdMap[target.id]);

    if (memberData) {
      let processBalance = (bal) => {
        return {
          amount: util.parseAmount(bal.amount),
          currency: bal.currency_code
        };
      };
      let processedBalances = memberData.balance.map(processBalance);

      let result = {
        owes: processedBalances.filter(bal => bal.amount < 0),
        owed: processedBalances.filter(bal => bal.amount > 0)
      };

      cb(result);
    }
  });
}

let graphDebts = (fullGraph, config, cb) => {
  sw.getGroup((error, data, _) => {
    if (error) {
      _logger.error(`Splitwise API error ${JSON.stringify(error)}`);
      return;
    }

    let groupData = JSON.parse(data).group;
    let groupMembers = {};
    let balanceGraph = graphviz.digraph('Debts');

    groupData.members.forEach(member => {
      member.fullName = util.formatName(member);
      groupMembers[member.id] = member.fullName;
      member.graphNode = balanceGraph.addNode(`${member.id}`, {
        label: member.fullName
      });
    });

    (fullGraph ? groupData.original_debts : groupData.simplified_debts)
      .forEach(debt => {
        let edge = balanceGraph.addEdge(`${debt.from}`, `${debt.to}`, {
          label: util.formatAmount(debt.amount, debt.currency_code,
            config.currencies)
        });
      });

    let imgPath = `/tmp/balanceGraph${Date.now()}.png`;
    balanceGraph.output('png', (rendered) => {
      fs.writeFile(imgPath, rendered, () => {
        imgur.uploadFile(imgPath)
          .then(res => cb(res.data.link))
          .catch(error => {
            _logger.error(`Error uploading splitwise graph to imgur: ${error.message}`);
          });
      });
    });
  });
};

let slackwise = (user, users, argv, config, logger, response, helpService) => {
  let cmdname = argv.shift();
  let subcmd = argv.shift();
  if (subcmd === 'graph') {
    let drawFullGraph = !!argv.some(item => item === '--full');
    graphDebts(drawFullGraph, config, imgUrl => response.end(imgUrl));
  }
  else if (subcmd === 'balance') {
    let arg = argv.shift();
    let target;

    try {
      target = users[arg ? util.parseUserTag(arg) : user.id];

      if (typeof target === 'undefined') {
        throw 'Don\'t know who this is lol';
      }
    }
    catch (e) {
      response.end(`I don't know who "${arg}" is!`);
      return;
    }

    getBalance(target, config, (balances, err) => {
      if (err) {
        response.end(err);
        return;
      }

      if (balances.owes.length) {
        response.write(`${target.real_name} owes:\n`);
        response.write(balances.owes.map(bal => {
          return `• ${util.formatAmount(-bal.amount, bal.currency,
            config.currencies)}`
        }).join('\n'));
        response.write('\n\n');
      }

      if (balances.owed.length) {
        response.write(`${target.real_name} is owed:\n`);
        response.write(balances.owed.map(bal => {
          return `• ${util.formatAmount(bal.amount, bal.currency,
            config.currencies)}`
        }).join('\n'));
      }

      response.end();
    });
  } else {
    let helpQuery = [cmdname].concat(argv);
    response.end(helpService.getHelpPage(helpQuery));
  }
};

slackwise.setup = (config, logger) => {
  sw = new Splitwise(config.swConsumerKey, config.swConsumerSecret,
    config.swAccessToken, config.swAccessTokenSecret, config.swGroupId);
  imgur.setClientId(config.imgurClientId);
  _logger = logger;
  swIdMap = config.swIdMap;
};

slackwise.metadata = require('./plugin');
module.exports = slackwise;
<|endoftext|>


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

foam.CLASS({
  package: 'foam.nanos.crunch.ruler',
  name: 'CheckUCJOwnershipOnPut',

  implements: [
    'foam.nanos.ruler.RuleAction'
  ],

  javaImports: [
    'foam.core.ContextAgent',
    'foam.core.X',
    'foam.nanos.auth.AuthorizationException',
    'foam.nanos.auth.AuthService',
    'foam.nanos.auth.Subject',
    'foam.nanos.auth.User',
    'foam.nanos.crunch.UserCapabilityJunction'
  ],

  methods: [
    {
      name: 'applyAction',
      javaCode: `
        agency.submit(x, new ContextAgent() {
          @Override
          public void execute(X x) {
            UserCapabilityJunction ucj = (UserCapabilityJunction) obj;

            Subject subject = (Subject) x.get("subject");
            User user = subject.getUser();
            User realUser = subject.getRealUser();

            boolean isOwner = ucj.getSourceId() == user.getId() || ucj.getSourceId() == realUser.getId();
            if ( isOwner ) return;

            // TODO: check permission for global UCJ updates instead?
            if ( user.isAdmin() ) return;

            throw new AuthorizationException("Cannot add UserCapabilityJunction. Not an admin or owner.");
          }
        }, "");
      `
    }
  ]
});
<commit_msg>With changes to ApprovalRequests which use createdFor, the subject user may only be an effective user (#4936)<commit_after>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.crunch.ruler',
  name: 'CheckUCJOwnershipOnPut',

  implements: [
    'foam.nanos.ruler.RuleAction'
  ],

  javaImports: [
    'foam.core.ContextAgent',
    'foam.core.X',
    'foam.nanos.auth.AuthorizationException',
    'foam.nanos.auth.AuthService',
    'foam.nanos.auth.Subject',
    'foam.nanos.auth.User',
    'foam.nanos.crunch.AgentCapabilityJunction',
    'foam.nanos.crunch.UserCapabilityJunction'
  ],

  methods: [
    {
      name: 'applyAction',
      javaCode: `
        agency.submit(x, new ContextAgent() {
          @Override
          public void execute(X x) {
            UserCapabilityJunction ucj = (UserCapabilityJunction) obj;

            Subject subject = (Subject) x.get("subject");
            User user = subject.getUser();
            User realUser = subject.getRealUser();

            // TODO: check permission for global UCJ updates instead?
            if ( user.isAdmin() ) return;

            boolean isOwner = ucj.getSourceId() == user.getId() || ucj.getSourceId() == realUser.getId();
            if ( isOwner ) return;

            if ( ucj instanceof AgentCapabilityJunction &&
                 ((AgentCapabilityJunction) ucj).getEffectiveUser() == user.getId() ) {
              return;
            }

            throw new AuthorizationException("Cannot add UserCapabilityJunction. Not an admin or owner.");
          }
        }, "");
      `
    }
  ]
});
<|endoftext|>


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

foam.CLASS({
  package: 'foam.nanos.notification.email',
  name: 'SMTPEmailService',

  implements: [
    'foam.nanos.notification.email.EmailService'
  ],

  documentation: 'Implementation of Email Service using SMTP',

  imports: [
    'threadPool?'
  ],

  javaImports: [
    'foam.core.ContextAgent',
    'foam.core.X',
    'foam.util.SafetyUtil',
    'java.nio.charset.StandardCharsets',
    'java.util.Date',
    'java.util.Properties',
    'java.util.List',
    'javax.mail.*',
    'javax.mail.internet.InternetAddress',
    'javax.mail.internet.MimeMessage',
    'org.apache.commons.lang3.StringUtils',
    'org.jtwig.JtwigTemplate',
    'org.jtwig.resource.loader.TypedResourceLoader',
    'foam.dao.ArraySink',
    'foam.dao.DAO',
    'foam.nanos.auth.User',
    'foam.nanos.auth.Group',
    'foam.nanos.logger.Logger',
    'foam.nanos.om.OMLogger',
    'static foam.mlang.MLang.EQ'
  ],

  axioms: [
    {
      name: 'javaExtras',
      buildJavaClass: function(cls) {
        cls.extras.push(foam.java.Code.create({
          data:
            `private class SMTPAuthenticator extends javax.mail.Authenticator {
              protected String username_;
              protected String password_;

              public SMTPAuthenticator(String username, String password) {
                this.username_ = username;
                this.password_ = password;
              }

              @Override
              protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(this.username_, this.password_);
              }
            }
            `
        }));
      }
    }
  ],

  properties: [
    {
      name: 'session_',
      javaType: 'Session',
      class: 'Object',
      javaFactory:
      `
        Properties props = new Properties();
        props.setProperty("mail.smtp.auth", getAuthenticate() ? "true" : "false");
        props.setProperty("mail.smtp.starttls.enable", getStarttls() ? "true" : "false");
        props.setProperty("mail.smtp.host", getHost());
        props.setProperty("mail.smtp.port", getPort());
        if ( getAuthenticate() ) {
          return Session.getInstance(props, new SMTPAuthenticator(getUsername(), getPassword()));
        }
        return Session.getInstance(props);
      `
    },
    {
      class: 'Object',
      javaType: 'Transport',
      name: 'transport_',
      javaFactory:
      `
        Logger logger = (Logger) getX().get("logger");
        OMLogger omLogger = (OMLogger) getX().get("OMLogger");
        Transport transport = null;
        try {
          omLogger.log(this.getClass().getSimpleName(), "transport", "connecting");
          transport = getSession_().getTransport("smtp");
          transport.connect(getUsername(), getPassword());
          logger.info("SMTPEmailService connected.");
          omLogger.log(this.getClass().getSimpleName(), "transport", "connected");
        } catch ( Exception e ) {
          logger.error("Transport failed to initialize: " + e);
        }
        return transport;
      `
    },
    {
      class: 'String',
      name: 'host',
      value: '127.0.0.1'
    },
    {
      class: 'String',
      name: 'port',
      value: '25'
    },
    {
      class: 'Boolean',
      name: 'authenticate',
      value: false
    },
    {
      class: 'Boolean',
      name: 'starttls',
      value: false
    },
    {
      class: 'String',
      name: 'username',
      value: null
    },
    {
      class: 'String',
      name: 'password',
      value: null
    }
  ],

  methods: [
    {
      name: 'createMimeMessage',
      javaType: 'MimeMessage',
      documentation: `Create a MimeMessage from the passed EmailMessage`,
      args: [
        {
          name: 'emailMessage',
          type: 'foam.nanos.notification.email.EmailMessage'
        }
      ],
      javaCode:
      `
        Logger logger = (Logger) getX().get("logger");
        try {
          MimeMessage message = new MimeMessage(getSession_());

          // the From Property is mainly to hide our smtp user credetials.
          if ( emailMessage.isPropertySet("from") ) {
            if ( emailMessage.isPropertySet("displayName") ) {
              message.setFrom( new InternetAddress(emailMessage.getFrom(), emailMessage.getDisplayName()) );
            } else {
              message.setFrom(new InternetAddress(emailMessage.getFrom()));
            }
          } else if ( emailMessage.isPropertySet("replyTo") ) {
              if ( emailMessage.isPropertySet("displayName") ) {
                message.setFrom( new InternetAddress(emailMessage.getReplyTo(), emailMessage.getDisplayName()) );
              } else {
                message.setFrom(new InternetAddress(emailMessage.getReplyTo()));
              }
          }

          if ( emailMessage.isPropertySet("replyTo") )
            message.setReplyTo(InternetAddress.parse(emailMessage.getReplyTo()));

          if ( emailMessage.isPropertySet("subject") )
            message.setSubject(emailMessage.getSubject());

          if ( emailMessage.isPropertySet("body") )
            message.setContent(emailMessage.getBody(), "text/html; charset=utf-8");

          if ( emailMessage.isPropertySet("to") ) {
            if ( emailMessage.getTo().length == 1 ) {
              message.setRecipient(Message.RecipientType.TO, new InternetAddress((emailMessage.getTo())[0], false));
            } else {
              message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(StringUtils.join(emailMessage.getTo(), ",")));
            }
          }

          if ( emailMessage.isPropertySet("cc") ) {
            if ( emailMessage.getCc().length == 1 ) {
              message.setRecipient(Message.RecipientType.CC, new InternetAddress((emailMessage.getCc())[0], false));
            } else {
              message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(StringUtils.join(emailMessage.getCc(), ",")));
            }
          }

          if ( emailMessage.isPropertySet("bcc") ) {
            if ( emailMessage.getBcc().length == 1 ) {
              message.setRecipient(Message.RecipientType.BCC, new InternetAddress((emailMessage.getBcc())[0], false));
            } else {
              message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(StringUtils.join(emailMessage.getBcc(), ",")));
            }
          }

          message.setSentDate(new Date());
          logger.info("SMTPEmailService Created MimeMessage.");
          return message;
        } catch (Throwable t) {
          logger.error("SMTPEmailService failed to created MimeMessage. " + t);
          return null;
        }
      `
    },
    {
      name: 'sendEmail',
      javaCode: `
        Logger logger = (Logger) getX().get("logger");
        OMLogger omLogger = (OMLogger) getX().get("OMLogger");

        emailMessage = (EmailMessage) emailMessage.fclone();
        MimeMessage message = createMimeMessage(emailMessage);
        if ( message == null ) {
          return emailMessage;
        }
        try {
          getTransport_().send(message);
          emailMessage.setStatus(Status.SENT);
          logger.debug("SMTPEmailService sent MimeMessage.");
          omLogger.log(this.getClass().getSimpleName(), "message", "sent");
        } catch ( SendFailedException e ) {
          emailMessage.setStatus(Status.FAILED);
          logger.error("SMTPEmailService sending MimeMessage failed. " + e);
        } catch ( MessagingException e ) {
          try {
            getTransport_().close();
          } catch ( Exception e2 ) {
            logger.error("Failed to close transport. " + e2);
          }
          clearTransport_();
          logger.error("SMTPEmailService sending MimeMessage failed. " + e);
        }
        return emailMessage;
      `
    }
  ]
});
<commit_msg>Do not send email already in FAILED state.<commit_after>/**
 * @license
 * Copyright 2017 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.notification.email',
  name: 'SMTPEmailService',

  implements: [
    'foam.nanos.notification.email.EmailService'
  ],

  documentation: 'Implementation of Email Service using SMTP',

  imports: [
    'threadPool?'
  ],

  javaImports: [
    'foam.core.ContextAgent',
    'foam.core.X',
    'foam.util.SafetyUtil',
    'java.nio.charset.StandardCharsets',
    'java.util.Date',
    'java.util.Properties',
    'java.util.List',
    'javax.mail.*',
    'javax.mail.internet.InternetAddress',
    'javax.mail.internet.MimeMessage',
    'org.apache.commons.lang3.StringUtils',
    'org.jtwig.JtwigTemplate',
    'org.jtwig.resource.loader.TypedResourceLoader',
    'foam.dao.ArraySink',
    'foam.dao.DAO',
    'foam.nanos.auth.User',
    'foam.nanos.auth.Group',
    'foam.nanos.logger.Logger',
    'foam.nanos.om.OMLogger',
    'static foam.mlang.MLang.EQ'
  ],

  axioms: [
    {
      name: 'javaExtras',
      buildJavaClass: function(cls) {
        cls.extras.push(foam.java.Code.create({
          data:
            `private class SMTPAuthenticator extends javax.mail.Authenticator {
              protected String username_;
              protected String password_;

              public SMTPAuthenticator(String username, String password) {
                this.username_ = username;
                this.password_ = password;
              }

              @Override
              protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(this.username_, this.password_);
              }
            }
            `
        }));
      }
    }
  ],

  properties: [
    {
      name: 'session_',
      javaType: 'Session',
      class: 'Object',
      javaFactory:
      `
        Properties props = new Properties();
        props.setProperty("mail.smtp.auth", getAuthenticate() ? "true" : "false");
        props.setProperty("mail.smtp.starttls.enable", getStarttls() ? "true" : "false");
        props.setProperty("mail.smtp.host", getHost());
        props.setProperty("mail.smtp.port", getPort());
        if ( getAuthenticate() ) {
          return Session.getInstance(props, new SMTPAuthenticator(getUsername(), getPassword()));
        }
        return Session.getInstance(props);
      `
    },
    {
      class: 'Object',
      javaType: 'Transport',
      name: 'transport_',
      javaFactory:
      `
        Logger logger = (Logger) getX().get("logger");
        OMLogger omLogger = (OMLogger) getX().get("OMLogger");
        Transport transport = null;
        try {
          omLogger.log(this.getClass().getSimpleName(), "transport", "connecting");
          transport = getSession_().getTransport("smtp");
          transport.connect(getUsername(), getPassword());
          logger.info("SMTPEmailService connected.");
          omLogger.log(this.getClass().getSimpleName(), "transport", "connected");
        } catch ( Exception e ) {
          logger.error("Transport failed to initialize: " + e);
        }
        return transport;
      `
    },
    {
      class: 'String',
      name: 'host',
      value: '127.0.0.1'
    },
    {
      class: 'String',
      name: 'port',
      value: '25'
    },
    {
      class: 'Boolean',
      name: 'authenticate',
      value: false
    },
    {
      class: 'Boolean',
      name: 'starttls',
      value: false
    },
    {
      class: 'String',
      name: 'username',
      value: null
    },
    {
      class: 'String',
      name: 'password',
      value: null
    }
  ],

  methods: [
    {
      name: 'createMimeMessage',
      javaType: 'MimeMessage',
      documentation: `Create a MimeMessage from the passed EmailMessage`,
      args: [
        {
          name: 'emailMessage',
          type: 'foam.nanos.notification.email.EmailMessage'
        }
      ],
      javaCode:
      `
        Logger logger = (Logger) getX().get("logger");
        try {
          MimeMessage message = new MimeMessage(getSession_());

          // the From Property is mainly to hide our smtp user credetials.
          if ( emailMessage.isPropertySet("from") ) {
            if ( emailMessage.isPropertySet("displayName") ) {
              message.setFrom( new InternetAddress(emailMessage.getFrom(), emailMessage.getDisplayName()) );
            } else {
              message.setFrom(new InternetAddress(emailMessage.getFrom()));
            }
          } else if ( emailMessage.isPropertySet("replyTo") ) {
              if ( emailMessage.isPropertySet("displayName") ) {
                message.setFrom( new InternetAddress(emailMessage.getReplyTo(), emailMessage.getDisplayName()) );
              } else {
                message.setFrom(new InternetAddress(emailMessage.getReplyTo()));
              }
          }

          if ( emailMessage.isPropertySet("replyTo") )
            message.setReplyTo(InternetAddress.parse(emailMessage.getReplyTo()));

          if ( emailMessage.isPropertySet("subject") )
            message.setSubject(emailMessage.getSubject());

          if ( emailMessage.isPropertySet("body") )
            message.setContent(emailMessage.getBody(), "text/html; charset=utf-8");

          if ( emailMessage.isPropertySet("to") ) {
            if ( emailMessage.getTo().length == 1 ) {
              message.setRecipient(Message.RecipientType.TO, new InternetAddress((emailMessage.getTo())[0], false));
            } else {
              message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(StringUtils.join(emailMessage.getTo(), ",")));
            }
          }

          if ( emailMessage.isPropertySet("cc") ) {
            if ( emailMessage.getCc().length == 1 ) {
              message.setRecipient(Message.RecipientType.CC, new InternetAddress((emailMessage.getCc())[0], false));
            } else {
              message.setRecipients(Message.RecipientType.CC, InternetAddress.parse(StringUtils.join(emailMessage.getCc(), ",")));
            }
          }

          if ( emailMessage.isPropertySet("bcc") ) {
            if ( emailMessage.getBcc().length == 1 ) {
              message.setRecipient(Message.RecipientType.BCC, new InternetAddress((emailMessage.getBcc())[0], false));
            } else {
              message.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(StringUtils.join(emailMessage.getBcc(), ",")));
            }
          }

          message.setSentDate(new Date());
          logger.info("SMTPEmailService Created MimeMessage.");
          return message;
        } catch (Throwable t) {
          logger.error("SMTPEmailService failed to created MimeMessage. " + t);
          return null;
        }
      `
    },
    {
      name: 'sendEmail',
      javaCode: `
        Logger logger = (Logger) getX().get("logger");
        OMLogger omLogger = (OMLogger) getX().get("OMLogger");

        emailMessage = (EmailMessage) emailMessage.fclone();
        if ( emailMessage.getStatus() == Status.FAILED ) {
          // ignore
          logger.debug("Email not sent, already FAILED.", emailMessage.getId());
          return emailMessage;
        }

        MimeMessage message = createMimeMessage(emailMessage);
        if ( message == null ) {
          return emailMessage;
        }
        try {
          getTransport_().send(message);
          emailMessage.setStatus(Status.SENT);
          logger.debug("SMTPEmailService sent MimeMessage.");
          omLogger.log(this.getClass().getSimpleName(), "message", "sent");
        } catch ( SendFailedException e ) {
          emailMessage.setStatus(Status.FAILED);
          logger.error("SMTPEmailService sending MimeMessage failed. " + e);
        } catch ( MessagingException e ) {
          try {
            getTransport_().close();
          } catch ( Exception e2 ) {
            logger.error("Failed to close transport. " + e2);
          }
          clearTransport_();
          logger.error("SMTPEmailService sending MimeMessage failed. " + e);
        }
        return emailMessage;
      `
    }
  ]
});
<|endoftext|>