diff --git a/modules/orionode.collab.atom/.gitignore b/modules/orionode.collab.atom/.gitignore new file mode 100644 index 0000000000..ade14b9196 --- /dev/null +++ b/modules/orionode.collab.atom/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +npm-debug.log +node_modules diff --git a/modules/orionode.collab.atom/CHANGELOG.md b/modules/orionode.collab.atom/CHANGELOG.md new file mode 100644 index 0000000000..76b1140445 --- /dev/null +++ b/modules/orionode.collab.atom/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + + + +## 0.0.1 - First Release +- Generated Atom package: Orion + diff --git a/modules/orionode.collab.atom/LICENSE.md b/modules/orionode.collab.atom/LICENSE.md new file mode 100644 index 0000000000..a0a98092e4 --- /dev/null +++ b/modules/orionode.collab.atom/LICENSE.md @@ -0,0 +1,5 @@ +Copyright (c) 2012, 2013, 2017 IBM Corporation and others. +All rights reserved. This program and the accompanying materials are made +available under the terms of the Eclipse Public License v1.0 +(http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution +License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). diff --git a/modules/orionode.collab.atom/README.md b/modules/orionode.collab.atom/README.md index ccef86da95..14247f402d 100644 --- a/modules/orionode.collab.atom/README.md +++ b/modules/orionode.collab.atom/README.md @@ -1 +1,165 @@ -Placeholder for atom editor code +# Orion - Atom Package + +Orion - Atom is a package that enables connections to Eclipse Orion for collaboration. + +## Getting Started + +These instructions will get you an instance of the Atom package up and running on your local machine for development and testing purposes. + +### Prerequisites + +Please ensure that you have the latest version of Atom installed: + +``` +https://atom.io/ +``` + +Also ensure that your Eclipse Orion repository is up to date. The repository can be found at: + +``` +https://github.com/eclipse/orion.client +``` + +### Installing the package + +1. Within your terminal, navigate to: + ``` + orion.client/modules/orionode.collab.atom + ``` +2. Within this directory, link the package to Atom using: + ``` + apm link + ``` + +## Running the package + +(*Refer to each command's specific section for configuration details before running the package.*) + +Running the Atom - Orion package consists of three consecutive commands: + +1. Start_Collab +2. Join_Session +3. Join_Document + +These three commands can be executed in Atom using the Command Palette + +`MAC: Command + Shift + P` + +`PC and Linux: Control + Shift + P` + +### Orion: Start_Collab + +Upon execution, function start_collab within *orion.js* is called. Start_collab handles the connections through socket.io, as well as message transfers. + +At the moment **there are two attributes that must be entered manually** in respect to their Orion values: + +- Orion Collab Hub IP + Port +- SessionID + +Example: + +``` +mySocket = io.connect('http://localhost:8082/' + "?sessionId=" + 'jnpO0CBn8n', { + path: "/socket.io/" +}); +``` +The SessionID can be found in *orion.client/modules/orionode.collab.hub/server.js* by printing to terminal: + +``` +var sessionID = sock.conn.request._query.sessionID; (~ Line 30) +console.log("The SessionID is: " + sessionID); +``` + +#### Socket.IO Channels + +Transferring of text from Orion to Atom is done within the 'message' channel as described below. + +'connect' : Calls join_session() + +'disconnect' : Disconnects user from current socket.io session + +'error' : Displays all socket.io server errors + +'message' : Has types 'init-document' and 'operation' + +##### ***If the message object has type 'operation', text is transferred and inserted into Atom.*** + +``` +if (msgObj.type === 'operation') { + atom.workspace.getActiveTextEditor().insertText(msgObj.operation[1]); +} +``` + +### Orion: Join_Session + +Upon execution, function join_session within *orion.js* is called. Join_session handles the connection of users to a specific Orion session. + +At the moment there are two attributes that must be entered manually in respect to their Orion values: +- ClientID `In the form of: username.12345` +- Token `In the form: username` + + +**Temporary Alert: *Both Authentication and Token usage were removed (commented out) to enable Atom connections to Orion. This avoids the need for JWT_SECRET within each user object.*** + +### Orion: Join_Document + +Upon execution, function join_document within *orion.js* is called. Join_document handles the connection of users to a specific Orion document. + +At the moment there are two attributes that must be entered manually in respect to their Orion values: +- Document path +- The same ClientID from Orion: Join_Session + +``` +mySocket.emit('message', JSON.stringify({ + type: 'join-document', + doc: '/arshi-OrionContent/Test/test.txt', + clientId: 'arshi3.12345' +})); +``` + +## To Do + +List of things to do towards the completion of the Atom - Orion package. + +**Real-time Editing** + +- Sending messages/text from Orion to Atom + - *Text Insertion location needs to be same on both editors (In progress)* +- Sending messages/text from Atom to Orion + +**Authentication** + +- Restore the security layer for Atom users such that the Atom user needs to be authenticated to view/edit files and directories. + - *Authentication had been temporarily removed for development purposes.* + +**Invitation/Revoke Access Control** + +- Show Atom user a list of workspaces that he has access to. + +**Workspaces** + +- Let the user choose the workspace he wishes to work on. +- Get a representation of the tree (files and directories) once they are in a workspace. + - Render the tree + - Starter goal: List View + - End goal: GUI within Atom +- Synchronized file manipulation, such as Create, Rename, Delete. +- Let Atom user select a file (in the chosen workspace) so that they can view/edit the document. +- Get file content for the chosen file and render it on Atom when Atom user joins a document. + - *Content is currently fetched and inserted into the Atom editor* + +**Text, Audio, and Video Communication** + +- Implementation of a text chat with individual chatroom scopes for: Workspace, Project Folder, and File. + +## Contributing + +Please read [CONTRIBUTING.md](https://github.com/eclipse/orion.client/blob/master/CONTRIBUTING.md) for details on our code of conduct, and developer resources. + +## Versioning + +We use GitHub for versioning. For the versions available, see the commits on [this repository](https://github.com/eclipse/orion.client). + +## License + +Dual-licensed under the [Eclipse Public License v1.0](http://www.eclipse.org/legal/epl-v10.html) and the [Eclipse Distribution License v1.0](http://www.eclipse.org/org/documents/edl-v10.html). diff --git a/modules/orionode.collab.atom/keymaps/orion.json b/modules/orionode.collab.atom/keymaps/orion.json new file mode 100644 index 0000000000..de015031fe --- /dev/null +++ b/modules/orionode.collab.atom/keymaps/orion.json @@ -0,0 +1,5 @@ +{ + "atom-workspace": { + "ctrl-alt-o": "orion:toggle" + } +} diff --git a/modules/orionode.collab.atom/lib/orion-view.js b/modules/orionode.collab.atom/lib/orion-view.js new file mode 100644 index 0000000000..c15284584f --- /dev/null +++ b/modules/orionode.collab.atom/lib/orion-view.js @@ -0,0 +1,29 @@ +'use babel'; + +export default class OrionView { + + constructor(serializedState) { + // Create root element + this.element = document.createElement('div'); + this.element.classList.add('orion'); + + // Create message element + const message = document.createElement('div'); + message.textContent = 'The Orion package is Alive! It\'s ALIVE!'; + message.classList.add('message'); + this.element.appendChild(message); + } + + // Returns an object that can be retrieved when package is activated + serialize() {} + + // Tear down any state and detach + destroy() { + this.element.remove(); + } + + getElement() { + return this.element; + } + +} diff --git a/modules/orionode.collab.atom/lib/orion.js b/modules/orionode.collab.atom/lib/orion.js new file mode 100644 index 0000000000..3dd7957273 --- /dev/null +++ b/modules/orionode.collab.atom/lib/orion.js @@ -0,0 +1,241 @@ +'use babel'; + +import OrionView from './orion-view'; +import { + CompositeDisposable +} from 'atom'; + +export default { + + orionView: null, + modalPanel: null, + subscriptions: null, + + activate(state) { + this.orionView = new OrionView(state.orionViewState); + this.modalPanel = atom.workspace.addModalPanel({ + item: this.orionView.getElement(), + visible: false + }); + + // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable + this.subscriptions = new CompositeDisposable(); + + // Register command that toggles this view + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'orion:toggle': () => this.toggle() + })); + + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'orion:start_collab': () => this.start_collab() + })); + + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'orion:join_session': () => this.join_session() + })); + + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'orion:join_document': () => this.join_document() + })); + }, + + deactivate() { + this.modalPanel.destroy(); + this.subscriptions.dispose(); + this.orionView.destroy(); + }, + + serialize() { + return { + orionViewState: this.orionView.serialize() + }; + }, + + toggle() { + console.log('Orion was toggled!'); + return ( + this.modalPanel.isVisible() ? + this.modalPanel.hide() : + this.modalPanel.show() + ); + }, + + start_collab() { + + const io = require('socket.io-client'); + + // HACK hardcoded value: sessionId + mySocket = io.connect('http://localhost:8082/' + "?sessionId=" + 'jnpO0CBn8n', { + path: "/socket.io/" + }); + + setTimeout(function() { + if (!mySocket.connected) { + console.error('Socket connection failed'); + /* Check: + * - if servers are running + * - `sessionId` above (attined from Collab Hub server) is correct + * - Port number for the Collab Hub server above is correct + */ + } + }, 2000); + + mySocket.on('connect', function() { + console.info('Socket connection successful'); + }); + + mySocket.on('disconnect', function() { + console.info('Socket disconnected'); + }); + + mySocket.on('error', function(e) { + console.info('Server sent an error message'); + }); + + mySocket.on('message', function(data) { + + var msgObj = JSON.parse(data); + console.log(msgObj); + + if (msgObj.type === 'init-document') { + console.info('Successfully joined document') + } + + if (msgObj.type === 'operation') { + + let editor = atom.workspace.getActiveTextEditor(); + + let operation = msgObj.operation; + let startPosition = 0; + let text = ''; + + // First char in empty doc + if (operation.length === 1) { + startPosition = 0; + text = operation[0]; + } + + // First char in non-empty doc + if (operation.length === 2 && Number.isInteger(operation[1])) { + startPosition = 0; + text = operation[0]; + } + + // Char in the middle + if (operation.length === 3) { + startPosition = operation[0]; + text = operation[1]; + } + + // Char at the end + if (operation.length === 2 && Number.isInteger(operation[0])) { + startPosition = operation[0]; + text = operation[1]; + } + + // ############################################################### demo + + let _demo_basic = true; + let _demo_insert_at_position = false; + + // + + /* + * This code will insert text where the cursor is on Atom + * regardless of where the Orion user is typing + */ + if (_demo_basic) { + if (Number.isInteger(text) && text < 0) { + let backspaceCount = -text; + for (let count = 0; count < backspaceCount; count++) { + editor.delete(); + // TODO handle backspace separately + // editor.backspace(); + } + } else { + // Insert received text from Orion + editor.insertText(text); + } + } // end of '_demo_basic' + + // + + /* + * This code will insert text at a given position in Atom + * regardless of where the cursor is + * + * Currently inserting text on row: 2 and col: 2 for demo. + * TODO: Send row-col value from Orion and use that data here + */ + if (_demo_insert_at_position) { + if (Number.isInteger(text) && text < 0) { + let backspaceCount = -text; + for (let count = 0; count < backspaceCount; count++) { + // Simulating backspace: + // Replacing from [2,2] to [2,3] (one char) with empty string + // HACK hardcoded value: range array + editor.setTextInBufferRange([ + [2, 2], + [2, 3] + ], ''); + } + } else { + // Insert received text from Orion + // HACK hardcoded value: range array + editor.setTextInBufferRange([ + [2, 2], + [2, 2] + ], text); + } + } // end of '_demo_insert_at_position' + + // ################################################### end of demo code + + } + + }); + + }, + + join_session() { + mySocket.emit('message', JSON.stringify({ + 'clientId': 'arshi3.12345', + 'token_bypass': 'arshi3' + // HACK hardcoded values: 'clientId' and 'token_bypass' + })); + }, + + join_document() { + mySocket.emit('message', JSON.stringify({ + type: 'join-document', + doc: '/arshi-OrionContent/Test/demo.txt', + clientId: 'arshi3.12345' + // HACK hardcoded values: 'doc' and 'clientId' + })); + + var request = require('request'); + // Requesting content of 'demo.txt' file + request('http://localhost:8081/file/arshi-OrionContent/Test/demo.txt', function(error, response, body) { + // TODO add authentication with this request + // HACK hardcoded value: URL + // + // The authentication to fetch content of the file + // on 'modules/orionode/index.js' ~ line 90 + // is disabled + + if (error) { + console.log('error:', error); + console.log('statusCode:', response && response.statusCode); + } + + let editor = atom.workspace.getActiveTextEditor(); + // Clear all content of the open Atom file + editor.selectAll(); + editor.backspace(); + // Insert received content into the open Atom file + editor.insertText(body); + + }); + }, + +}; diff --git a/modules/orionode.collab.atom/menus/orion.json b/modules/orionode.collab.atom/menus/orion.json new file mode 100644 index 0000000000..f0e28b8ee3 --- /dev/null +++ b/modules/orionode.collab.atom/menus/orion.json @@ -0,0 +1,26 @@ +{ + "context-menu": { + "atom-text-editor": [ + { + "label": "Toggle orion", + "command": "orion:toggle" + } + ] + }, + "menu": [ + { + "label": "Packages", + "submenu": [ + { + "label": "Orion", + "submenu": [ + { + "label": "Toggle", + "command": "orion:toggle" + } + ] + } + ] + } + ] +} diff --git a/modules/orionode.collab.atom/package.json b/modules/orionode.collab.atom/package.json new file mode 100644 index 0000000000..2e44a3d2eb --- /dev/null +++ b/modules/orionode.collab.atom/package.json @@ -0,0 +1,18 @@ +{ + "name": "orion", + "main": "./lib/orion", + "version": "0.0.1", + "description": "An Atom plugin to connect to Eclipse Orion and collaborate", + "keywords": [ + "eclipse", "orion", "collaboration" + ], + "repository": "https://github.com/eclipse/orion.client", + "license": "EPL-1.0", + "engines": { + "atom": ">=1.0.0 <2.0.0" + }, + "dependencies": { + "socket.io": "^2.0.0", + "request": "^2.81.0" + } +} diff --git a/modules/orionode.collab.atom/spec/orion-spec.js b/modules/orionode.collab.atom/spec/orion-spec.js new file mode 100644 index 0000000000..a1a85d17b7 --- /dev/null +++ b/modules/orionode.collab.atom/spec/orion-spec.js @@ -0,0 +1,73 @@ +'use babel'; + +import Orion from '../lib/orion'; + +// Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. +// +// To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` +// or `fdescribe`). Remove the `f` to unfocus the block. + +describe('Orion', () => { + let workspaceElement, activationPromise; + + beforeEach(() => { + workspaceElement = atom.views.getView(atom.workspace); + activationPromise = atom.packages.activatePackage('orion'); + }); + + describe('when the orion:toggle event is triggered', () => { + it('hides and shows the modal panel', () => { + // Before the activation event the view is not on the DOM, and no panel + // has been created + expect(workspaceElement.querySelector('.orion')).not.toExist(); + + // This is an activation event, triggering it will cause the package to be + // activated. + atom.commands.dispatch(workspaceElement, 'orion:toggle'); + + waitsForPromise(() => { + return activationPromise; + }); + + runs(() => { + expect(workspaceElement.querySelector('.orion')).toExist(); + + let orionElement = workspaceElement.querySelector('.orion'); + expect(orionElement).toExist(); + + let orionPanel = atom.workspace.panelForItem(orionElement); + expect(orionPanel.isVisible()).toBe(true); + atom.commands.dispatch(workspaceElement, 'orion:toggle'); + expect(orionPanel.isVisible()).toBe(false); + }); + }); + + it('hides and shows the view', () => { + // This test shows you an integration test testing at the view level. + + // Attaching the workspaceElement to the DOM is required to allow the + // `toBeVisible()` matchers to work. Anything testing visibility or focus + // requires that the workspaceElement is on the DOM. Tests that attach the + // workspaceElement to the DOM are generally slower than those off DOM. + jasmine.attachToDOM(workspaceElement); + + expect(workspaceElement.querySelector('.orion')).not.toExist(); + + // This is an activation event, triggering it causes the package to be + // activated. + atom.commands.dispatch(workspaceElement, 'orion:toggle'); + + waitsForPromise(() => { + return activationPromise; + }); + + runs(() => { + // Now we can test for view visibility + let orionElement = workspaceElement.querySelector('.orion'); + expect(orionElement).toBeVisible(); + atom.commands.dispatch(workspaceElement, 'orion:toggle'); + expect(orionElement).not.toBeVisible(); + }); + }); + }); +}); diff --git a/modules/orionode.collab.atom/spec/orion-view-spec.js b/modules/orionode.collab.atom/spec/orion-view-spec.js new file mode 100644 index 0000000000..2245dccb4e --- /dev/null +++ b/modules/orionode.collab.atom/spec/orion-view-spec.js @@ -0,0 +1,9 @@ +'use babel'; + +import OrionView from '../lib/orion-view'; + +describe('OrionView', () => { + it('has one valid test', () => { + expect('life').toBe('easy'); + }); +}); diff --git a/modules/orionode.collab.atom/styles/orion.less b/modules/orionode.collab.atom/styles/orion.less new file mode 100644 index 0000000000..d8b2459d03 --- /dev/null +++ b/modules/orionode.collab.atom/styles/orion.less @@ -0,0 +1,8 @@ +// The ui-variables file is provided by base themes provided by Atom. +// +// See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less +// for a full listing of what's available. +@import "ui-variables"; + +.orion { +} diff --git a/modules/orionode.collab.hub/server.js b/modules/orionode.collab.hub/server.js index c6c4b234c9..7f1dbb3616 100644 --- a/modules/orionode.collab.hub/server.js +++ b/modules/orionode.collab.hub/server.js @@ -28,6 +28,7 @@ var sessions = new SessionManager(); io.on('connection', function(sock) { // Get session ID var sessionId = sock.conn.request._query.sessionId; + console.log('[Info] sessionId: ' + sessionId); /** * Handle the initial message (authentication) @@ -35,16 +36,42 @@ io.on('connection', function(sock) { */ sock.on('message', function initMsgHandler(msg) { try { + var msgObj = JSON.parse(msg); - if (msgObj.type !== 'authenticate') { - throw new Error('Not authenticated.'); - } - // Authenticate + // Commenting out the following code + // to skip authentication + // + // TODO: + // - Restore the following code + // when Atom is capable of authentication + // + // + // // Original Code: + // + // if (msgObj.type !== 'authenticate') { + // throw new Error('Not authenticated.'); + // } + // + // if (!msgObj.token) { + // throw new Error('No token is specified.'); + // } + // + // // End of Original Code + // + // + // // New code to skip auth //////////////////////////////////////// + console.log('[Info] Skipping authentication for atom plugin dev.') + if (!msgObj.token) { - throw new Error('No token is specified.'); + var user = { + 'username': msgObj.token_bypass + } + } else { + var user = jwt.verify(msgObj.token, JWT_SECRET); } - var user = jwt.verify(msgObj.token, JWT_SECRET); + // // End of new code ////////////////////////////////////////////// + // // Give the control to a session sessions.addConnection(sessionId, sock, msgObj.clientId, user.username).then(function() { @@ -53,7 +80,11 @@ io.on('connection', function(sock) { }).catch(function(err) { sock.send(JSON.stringify({ type: 'error', error: err })); }); + } catch (ex) { + + console.log(ex); + sock.send(JSON.stringify({ type: 'error', message: ex.message diff --git a/modules/orionode/index.js b/modules/orionode/index.js index 4786aefd1d..c4190240a5 100755 --- a/modules/orionode/index.js +++ b/modules/orionode/index.js @@ -85,7 +85,20 @@ function startServer(options) { app.use('/site', checkAuthenticated, require('./lib/sites')(options)); app.use('/task', checkAuthenticated, require('./lib/tasks').router({ taskRoot: contextPath + '/task', options: options})); app.use('/filesearch', checkAuthenticated, require('./lib/search')(options)); - app.use('/file*', checkAuthenticated, require('./lib/file')({ workspaceRoot: contextPath + '/workspace', fileRoot: contextPath + '/file', options: options })); + + // + // Bypassing authentication for Atom plugin development + // + // TODO restore authentication when Atom plugin is capable of auth + // + // Old code: + // app.use('/file*', checkAuthenticated, require('./lib/file')({ workspaceRoot: contextPath + '/workspace', fileRoot: contextPath + '/file', options: options })); + // + // New Code: + app.use('/file*', require('./lib/file')({ workspaceRoot: contextPath + '/workspace', fileRoot: contextPath + '/file', options: options })); + // End of auth bypass + // + app.use('/workspace*', checkAuthenticated, require('./lib/workspace')({ workspaceRoot: contextPath + '/workspace', fileRoot: contextPath + '/file', gitRoot: contextPath + '/gitapi', options: options })); /* Note that the file and workspace root for the git middleware should not include the context path to match java implementation */ app.use('/gitapi', checkAuthenticated, require('./lib/git')({ gitRoot: contextPath + '/gitapi', fileRoot: /*contextPath + */'/file', workspaceRoot: /*contextPath + */'/workspace', options: options}));