party_iparty.js

const debug = require('debug')('dataparty.iparty')
const dataparty_crypto = require('@dataparty/crypto')

const ROSLIB = require('roslib')

const Query = require('./query.js')
const IDocument = require('./idocument')
const DocumentFactory = require('./document-factory')
const LokiCache = require('./loki-cache.js') // insert | populate cache


class IParty {
/**
 * @class module:Party.IParty
 * @link module.Party
 *
 * @param {module:Config.IConfig}   options.config
 * @param {module:Party.LokiCache}  options.cache
 * @param {boolean}                 options.noCache
 * @param {module:Comms.ISocketComms} options.comms
 * @param {Object}                  options.model
 * @param {Object}                  options.factories
 * @param {module:Party.IDocument}  options.documentClass
 * @param {module:Party.Qb}         options.qb
 */
  constructor({config, cache, noCache=false, comms, model, factories, documentClass, qb=null}){
    this.config = config
    this.qb = qb

    if(noCache){ this.cache = null }
    else{ this.cache = cache || new LokiCache() }
    
    this.comms = comms
    
    this._actor = {id: undefined, type: undefined}
    this._actors = []
    this._identity = undefined
    this.started = false

    /**
     * @member module:Party.IParty.factory
     * @type {DocumentFactory} */
    this.factory = new DocumentFactory({party: this, model, factories, documentClass})
  }

  /**
   * @async
   *  @method module:Party.IParty.start */
  async start(){
    
    if(this.started){ return }

    debug('start')
    if(this.config){
      await this.config.start()
    }

    if(this.cache){
      await this.cache.start()
    }

    await Promise.all([
      this.loadIdentity(),
      this.loadActor(),
    ])

    this.started = true

    debug('\tDocument Validators', this.factory.getValidators())
    debug('\tDocument Classes', this.factory.getTypes())
  }

  async stop(){
    this.comms.close()
  }

  /**
   * @async
   * @method module:Party.IParty.createDocument
   */
  async createDocument(type, data, id){
    let Type = this.factory.getFactory(type)


    return await Type.create(this, {data, type, id})
  }

  /**
   * @async
   * @method module:Party.IParty.create
   */
  async create (type, ...msgs) {
    return await this.qb.create(type, msgs)
  }


  /**
   * @async
   * @method module:Party.IParty.remove
   */
  async remove (...msgs) {
    return this.qb.modify(msgs, 'remove')
  }

  // takes modified json msgs & writes to backend, resolves to new stamps
  // requires type & id
  /**
   * @async
   * @method module:Party.IParty.update
   */
  async update (...msgs) {
    return this.qb.modify(msgs, 'update')
  }

  /**
   * Starts a query
   * @method module:Party.IParty.find
   * @returns {module:Party.Query}
   */
  find () {
    return new Query(this.qb, this.factory)
  }

 

  /**
   * @async
   * @method module:Party.IParty.call
   */
  async call(msg){
    throw new Error('Not Implemented')
  }

  /**
   * @method
   */
  async socket(reuse){
    throw new Error('Not Implemented')
  }

  

  /**
   * @member module:Party.IParty.ROSLIB
   *  @type {ROSLIB} */
  get ROSLIB(){
    return ROSLIB
  }


  /**
   * @member module:Party.IParty.Document
   *  @type {IDocument} */
  get Document(){
    return this.factory.Document
  }

  /**
   * @member module:Party.IParty.types
   */
  get types(){
    return this.factory.getFactories()
  }

  /**
   * @member module:Party.IParty.identity
   * @type {module:dataparty/Types.Identity}
   */
  get identity(){
    if (!this.hasIdentity()){ return undefined }
    return dataparty_crypto.Identity.fromString(this._identity.toString())
  }

  /**
   * @member module:Party.IParty.privateIdentity
   * @type {module:dataparty/Types.Identity}
   */
  get privateIdentity(){
    if (!this.hasIdentity()){ return undefined }
    return this._identity
  }

  /**
   *  @member module:Party.IParty.actor 
   * @type {IdObj} */
  get actor(){
    if (this.actors && this.actors[0]){

      return this.actors[0]

    } else if (this._actor.id && this._actor.type){

      return this._actor

    }

    return undefined
  }

  /**
   *  @member module:Party.IParty.actors 
   * @type {IdObj[]} */
  get actors(){
    return this._actors
  }

  set actors(value){
    this._actors = value

    const primaryActor = this.actor

    if (!primaryActor){
     return
    }

    this._actor.id = primaryActor.id
    this._actor.type = primaryActor.type

    const path = 'actor'

    //! @hack & @todo - this needs to be `await` so this accessor probably should be removed
    this.config.write(path, this._actor)
  }

  /**
   * @method module:Party.IParty.hasIdentity
   */
  hasIdentity(){
    return this._identity != undefined
  }

  /**
   * @method module:Party.IParty.hasActor
   */
  hasActor(){
    return this.actor != undefined
  }



  /**
   * @async
   * @method module:Party.IParty.loadIdentity
   */
  async loadIdentity(){
    const path = 'identity'
    const cfgIdenStr = await this.config.read(path)

    if (!cfgIdenStr){
      debug('generated new identity')
      
      await this.resetIdentity()
    } else {
      debug('loaded identity')
      this._identity = dataparty_crypto.Identity.fromString(JSON.stringify(cfgIdenStr))
    }
  }

  /**
   * @async
   * @method module:Party.IParty.resetIdentity
   */
  async resetIdentity(){
    const path = 'identity'
    await this.config.write(path, null)

    this._identity = new dataparty_crypto.Identity({id: 'primary'})
    await this.config.write(path, this._identity.toJSON(true))

    await this.loadIdentity()
  }

  /**
   * @async
   * @method module:Party.IParty.loadActor
   */
  async loadActor(){
    const path = 'actor'
    const localActorObj = await this.config.read(path)

    if (!localActorObj){ return }

    this._actor.id = localActorObj.id
    this._actor.type = localActorObj.type

    debug('loaded actor', this._actor)
  }

    /**
   * @async
   * @method module:Party.IParty.encrypt
   * @param {any} data
   * @param {dataparty_crypto.Identity} to
   * @returns {dataparty_crypto.Message}
   */
  async encrypt(data, to){
    const msg = new dataparty_crypto.Message({msg: data})
    await msg.encrypt(this._identity, to.key)

    return msg
  }

  /**
   * @async
   * @method module:Party.IParty.decrypt
   * @param {dataparty_crypto.Message} reply
   * @param {dataparty_crypto.Identity} expectedSender
   * @param {boolean} expectClearTextReply
   */
  async decrypt(reply, expectedSender, expectClearTextReply = false){
    // if reply has ciphertext & sig attempt to decrypt
    if (reply.enc && reply.sig) {
      const msg = new dataparty_crypto.Message(reply)

      const replyContent = await msg.decrypt(this._identity)

      const publicKeys = dataparty_crypto.Routines.extractPublicKeys(msg.enc)

      debug(`publicKeys.sign - ${publicKeys.sign}`)

      if (publicKeys.sign !== expectedSender.key.public.sign ||
          publicKeys.box !== expectedSender.key.public.box) {
        throw new Error('TrustFail: reply is not from service')
      }

      debug('decrypted reply ->', JSON.stringify(replyContent))

      if (replyContent.error) {
        debug('call failed ->', replyContent.error)
        throw replyContent.error
      }

      return replyContent

    } else if (expectClearTextReply && !reply.error) {

      return reply

    }

    if (reply.error) {
      debug('call failed ->', reply.error)
      throw reply.error
    }

    throw new Error('TrustFail: reply is not encrypted')
  }
}

module.exports = IParty