<commit_before>/// <reference path='../../../../third_party/typings/es6-promise/es6-promise.d.ts' />
/// <reference path='../../../../third_party/typings/freedom/freedom-module-env.d.ts' />
/// <reference path='../../../../third_party/typings/node/node.d.ts' />
/// <reference path='../../../../third_party/typings/ssh2/ssh2.d.ts' />

import arraybuffers = require('../../arraybuffers/arraybuffers');
import logging = require('../../logging/logging');

// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
import * as ssh2 from 'ssh2';
var Client = require('ssh2').Client;

var log: logging.Log = new logging.Log('cloud social');

const SSH_SERVER_PORT = 5000;

const ZORK_HOST = 'zork';
const ZORK_PORT = 9000;

// Key under which our contacts are saved in storage.
const STORAGE_KEY = 'cloud-social-contacts';

// Credentials for accessing a cloud instance.
// The serialised, base64 form is distributed amongst users.
// TODO: add (private) keys, for key-based auth
interface Invite {
  // Hostname or IP of the cloud instance.
  // This is the host on which sshd is running, so it should
  // be directly accessible from the client.
  host?: string;
  // Username.
  user?: string;
  // Password.
  pass?: string;
  // Private key, base64-encoded.
  key?: string;
}

// Type of the object placed, in serialised form, in storage
// under STORAGE_KEY.
interface SavedContacts {
  // TODO: remove this, invites are now embedded in contacts.
  invites?: Invite[];
  contacts?: SavedContact[];
}

// A contact as saved to storage, consisting of the invite
// plus any data fetched from the server on login (effectively,
// this on-demand data is cached here).
interface SavedContact {
  invite?: Invite;
  description?: string;
}

// Returns a VersionedPeerMessage, as defined in interfaces/social.ts
// in the uProxy repo.
//
// type is a PeerMessageType value (also defined in that file) and
// payload is an arbitrary payload, e.g. an instance message.
// TODO: use typings from the uProxy repo
function makeVersionedPeerMessage(
    type:number,
    payload:Object) : any {
  return {
    type: type,
    data: payload,
    // TODO: remote-instance assumes client versions >= 5 can do PGP, yuck
    version: 4
  };
}

// Returns an InstanceHandshake, as defined in interfaces/social.ts
// in the uProxy repo.
//
// publicKey is deliberately omitted so that uProxy will consider us
// an older client without PGP support and will avoid encrypting
// signalling messages (unnecessary because we are forwarding all the
// messages over an SSH tunnel).
// TODO: use typings from the uProxy repo
function makeInstanceMessage(address:string, description?:string): any {
  return {
    instanceId: address,
    // Shown in the contacts list while the user's list item is expanded
    description: description,
    consent: {
      isRequesting: false,
      isOffering: true
    },
    name: address,
    userId: address
  };
}

// To see how these fields are handled, see
// generic_core/social.ts#handleClient in the uProxy repo.
function makeClientState(address: string): freedom.Social.ClientState {
  return {
    userId: address,
    clientId: address,
    // https://github.com/freedomjs/freedom/blob/master/interface/social.json
    status: 'ONLINE',
    timestamp: Date.now()
  };
}

// To see how these fields are handled, see
// generic_core/social.ts#handleUserProfile in the uProxy repo. We omit
// the status field since remote-user.ts#update will use FRIEND as a default.
function makeUserProfile(address: string): freedom.Social.UserProfile {
  return {
    userId: address,
    name: address
  };
}

// Exposes Zork instances as friends.
//
// Intended for use with the run_cloud.sh script in the uproxy-docker
// repo, which will spin up the expected configuration:
//  - an SSH server running on port 5000, with an account named
//    "giver" having the password "<PASSWORD>"
//  - a Zork instance, accessible from the SSH server at the
//    hostname "zork"
//
// Note that since there's no actual uProxy instance running on the
// cloud instance, we are forced to re-implement some of uProxy's
// social code here, namely:
//  - we must send a "fake" INSTANCE message in order for uProxy
//    to consider the cloud instance online
//  - we have to inspect, wrap, and unwrap messages coming back and
//    forth since Zork has its own protocol
//
// Additionally, contacts are saved to storage so that we can contact
// them on login and emit a fake instance message for them, making
// them appear online.
//
// TODO: key-based authentication
// TODO: move the social message parsing stuff into Zork
export class CloudSocialProvider {
  private storage_: freedom.Storage.Storage = freedom['core.storage']();

  // Saved contacts, keyed by host.
  private savedContacts_: { [host: string]: SavedContact } = {};

  // SSH connections, keyed by host.
  private clients_: { [host: string]: Promise<Connection> } = {};

  constructor(private dispatchEvent_: (name: string, args: Object) => void) { }

  // Emits the messages necessary to make the user appear online 
  // in the contacts list.
  private notifyOfUser_ = (address: string, description?: string) => {
    this.dispatchEvent_('onUserProfile', makeUserProfile(address));

    var clientState = makeClientState(address);
    this.dispatchEvent_('onClientState', clientState);

    // Pretend that we received a message from a remote uProxy client.
    this.dispatchEvent_('onMessage', {
      from: clientState,
      // INSTANCE
      message: JSON.stringify(makeVersionedPeerMessage(
        3000, makeInstanceMessage(address, description)))
    });
  }

  // Establishes an SSH connection to a server, first shutting down
  // any that previously exists. Also emits an instance message,
  // allowing fields such as the description be updated on every
  // reconnect, and saves the contact to storage.
  private reconnect_ = (invite: Invite): Promise<Connection> => {
    log.debug('reconnecting to %1', invite.host);
    if (invite.host in this.clients_) {
      log.debug('closing old connection to %1', invite.host);
      this.clients_[invite.host].then((connection: Connection) => {
        connection.close();
      });
    }

    var connection = new Connection(invite, (message: Object) => {
      this.dispatchEvent_('onMessage', {
        from: makeClientState(invite.host),
        // SIGNAL_FROM_SERVER_PEER,
        message: JSON.stringify(makeVersionedPeerMessage(3002, message))
      });
    });

    this.clients_[invite.host] = connection.connect().then(() => {
      log.info('connected to zork on %1', invite.host);

      // Fetch the banner, if available, then emit an instance message.
      connection.getBanner().then((banner: string) => {
        if (banner.length < 1) {
          log.debug('empty banner, leaving blank');
        }
        return banner;
      }, (e: Error) => {
        log.warn('failed to fetch banner: %1', e);
        return '';
      }).then((banner: string) => {
        this.notifyOfUser_(invite.host, banner);
        this.savedContacts_[invite.host] = {
          invite: invite,
          description: banner
        };
        this.saveContacts_();
      });

      return connection;
    });

    return this.clients_[invite.host];
  }

  // Loads contacts from storage and emits an instance message for each.
  // This makes all of the stored contacts appear online.
  private loadContacts_ = () => {
    log.debug('loadContacts');
    this.savedContacts_ = {};
    this.storage_.get(STORAGE_KEY).then((storedString: string) => {
      if (!storedString) {
        log.debug('no saved contacts');
        return;
      }
      log.debug('loaded contacts: %1', storedString);
      try {
        var savedContacts: SavedContacts = JSON.parse(storedString);
        for (let contact of savedContacts.contacts) {
          this.savedContacts_[contact.invite.host] = contact;
          this.notifyOfUser_(contact.invite.host, contact.description);
        }
      } catch (e) {
        log.error('could not parse saved contacts: %1', e.message);
      }<commit_msg>fix undefined variable exception when no saved contacts<commit_after>/// <reference path='../../../../third_party/typings/es6-promise/es6-promise.d.ts' />
/// <reference path='../../../../third_party/typings/freedom/freedom-module-env.d.ts' />
/// <reference path='../../../../third_party/typings/node/node.d.ts' />
/// <reference path='../../../../third_party/typings/ssh2/ssh2.d.ts' />

import arraybuffers = require('../../arraybuffers/arraybuffers');
import logging = require('../../logging/logging');

// https://github.com/borisyankov/DefinitelyTyped/blob/master/ssh2/ssh2-tests.ts
import * as ssh2 from 'ssh2';
var Client = require('ssh2').Client;

var log: logging.Log = new logging.Log('cloud social');

const SSH_SERVER_PORT = 5000;

const ZORK_HOST = 'zork';
const ZORK_PORT = 9000;

// Key under which our contacts are saved in storage.
const STORAGE_KEY = 'cloud-social-contacts';

// Credentials for accessing a cloud instance.
// The serialised, base64 form is distributed amongst users.
// TODO: add (private) keys, for key-based auth
interface Invite {
  // Hostname or IP of the cloud instance.
  // This is the host on which sshd is running, so it should
  // be directly accessible from the client.
  host?: string;
  // Username.
  user?: string;
  // Password.
  pass?: string;
  // Private key, base64-encoded.
  key?: string;
}

// Type of the object placed, in serialised form, in storage
// under STORAGE_KEY.
interface SavedContacts {
  // TODO: remove this, invites are now embedded in contacts.
  invites?: Invite[];
  contacts?: SavedContact[];
}

// A contact as saved to storage, consisting of the invite
// plus any data fetched from the server on login (effectively,
// this on-demand data is cached here).
interface SavedContact {
  invite?: Invite;
  description?: string;
}

// Returns a VersionedPeerMessage, as defined in interfaces/social.ts
// in the uProxy repo.
//
// type is a PeerMessageType value (also defined in that file) and
// payload is an arbitrary payload, e.g. an instance message.
// TODO: use typings from the uProxy repo
function makeVersionedPeerMessage(
    type:number,
    payload:Object) : any {
  return {
    type: type,
    data: payload,
    // TODO: remote-instance assumes client versions >= 5 can do PGP, yuck
    version: 4
  };
}

// Returns an InstanceHandshake, as defined in interfaces/social.ts
// in the uProxy repo.
//
// publicKey is deliberately omitted so that uProxy will consider us
// an older client without PGP support and will avoid encrypting
// signalling messages (unnecessary because we are forwarding all the
// messages over an SSH tunnel).
// TODO: use typings from the uProxy repo
function makeInstanceMessage(address:string, description?:string): any {
  return {
    instanceId: address,
    // Shown in the contacts list while the user's list item is expanded
    description: description,
    consent: {
      isRequesting: false,
      isOffering: true
    },
    name: address,
    userId: address
  };
}

// To see how these fields are handled, see
// generic_core/social.ts#handleClient in the uProxy repo.
function makeClientState(address: string): freedom.Social.ClientState {
  return {
    userId: address,
    clientId: address,
    // https://github.com/freedomjs/freedom/blob/master/interface/social.json
    status: 'ONLINE',
    timestamp: Date.now()
  };
}

// To see how these fields are handled, see
// generic_core/social.ts#handleUserProfile in the uProxy repo. We omit
// the status field since remote-user.ts#update will use FRIEND as a default.
function makeUserProfile(address: string): freedom.Social.UserProfile {
  return {
    userId: address,
    name: address
  };
}

// Exposes Zork instances as friends.
//
// Intended for use with the run_cloud.sh script in the uproxy-docker
// repo, which will spin up the expected configuration:
//  - an SSH server running on port 5000, with an account named
//    "giver" having the password "<PASSWORD>"
//  - a Zork instance, accessible from the SSH server at the
//    hostname "zork"
//
// Note that since there's no actual uProxy instance running on the
// cloud instance, we are forced to re-implement some of uProxy's
// social code here, namely:
//  - we must send a "fake" INSTANCE message in order for uProxy
//    to consider the cloud instance online
//  - we have to inspect, wrap, and unwrap messages coming back and
//    forth since Zork has its own protocol
//
// Additionally, contacts are saved to storage so that we can contact
// them on login and emit a fake instance message for them, making
// them appear online.
//
// TODO: key-based authentication
// TODO: move the social message parsing stuff into Zork
export class CloudSocialProvider {
  private storage_: freedom.Storage.Storage = freedom['core.storage']();

  // Saved contacts, keyed by host.
  private savedContacts_: { [host: string]: SavedContact } = {};

  // SSH connections, keyed by host.
  private clients_: { [host: string]: Promise<Connection> } = {};

  constructor(private dispatchEvent_: (name: string, args: Object) => void) { }

  // Emits the messages necessary to make the user appear online 
  // in the contacts list.
  private notifyOfUser_ = (address: string, description?: string) => {
    this.dispatchEvent_('onUserProfile', makeUserProfile(address));

    var clientState = makeClientState(address);
    this.dispatchEvent_('onClientState', clientState);

    // Pretend that we received a message from a remote uProxy client.
    this.dispatchEvent_('onMessage', {
      from: clientState,
      // INSTANCE
      message: JSON.stringify(makeVersionedPeerMessage(
        3000, makeInstanceMessage(address, description)))
    });
  }

  // Establishes an SSH connection to a server, first shutting down
  // any that previously exists. Also emits an instance message,
  // allowing fields such as the description be updated on every
  // reconnect, and saves the contact to storage.
  private reconnect_ = (invite: Invite): Promise<Connection> => {
    log.debug('reconnecting to %1', invite.host);
    if (invite.host in this.clients_) {
      log.debug('closing old connection to %1', invite.host);
      this.clients_[invite.host].then((connection: Connection) => {
        connection.close();
      });
    }

    var connection = new Connection(invite, (message: Object) => {
      this.dispatchEvent_('onMessage', {
        from: makeClientState(invite.host),
        // SIGNAL_FROM_SERVER_PEER,
        message: JSON.stringify(makeVersionedPeerMessage(3002, message))
      });
    });

    this.clients_[invite.host] = connection.connect().then(() => {
      log.info('connected to zork on %1', invite.host);

      // Fetch the banner, if available, then emit an instance message.
      connection.getBanner().then((banner: string) => {
        if (banner.length < 1) {
          log.debug('empty banner, leaving blank');
        }
        return banner;
      }, (e: Error) => {
        log.warn('failed to fetch banner: %1', e);
        return '';
      }).then((banner: string) => {
        this.notifyOfUser_(invite.host, banner);
        this.savedContacts_[invite.host] = {
          invite: invite,
          description: banner
        };
        this.saveContacts_();
      });

      return connection;
    });

    return this.clients_[invite.host];
  }

  // Loads contacts from storage and emits an instance message for each.
  // This makes all of the stored contacts appear online.
  private loadContacts_ = () => {
    log.debug('loadContacts');
    this.savedContacts_ = {};
    this.storage_.get(STORAGE_KEY).then((storedString: string) => {
      if (!storedString) {
        log.debug('no saved contacts');
        return;
      }
      log.debug('loaded contacts: %1', storedString);
      try {
        var savedContacts: SavedContacts = JSON.parse(storedString);
        if (savedContacts.contacts) {
          for (let contact of savedContacts.contacts) {
            this.savedContacts_[contact.invite.host] = contact;
            this.notifyOfUser_(contact.invite.host, contact.description);
          }
        }
      } catch (e) {
        log.error('could not parse saved contacts: %1', e.message);
      }
{'fraction_non_alphanumeric': 0.11911170928667564, 'fraction_numerical': 0.005138871895264897, 'mean_word_length': 3.051301115241636, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 41, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7523349', 'n_tokens_mistral': 4769, 'n_tokens_neox': 4623, 'n_words': 1583}