├── bin
└── run.js
├── termgram.png
├── .npmignore
├── README.md
├── .gitignore
├── i18n
├── mark-down.js
└── en-US.js
├── package.json
├── LICENSE
├── lib
├── user-data.js
├── updates.js
├── use-case
│ ├── chat.js
│ ├── select-chat.js
│ ├── sign-in.js
│ └── sign-up.js
├── user-interface.js
└── client-proxy.js
└── termgram.js
/bin/run.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | require('../termgram');
--------------------------------------------------------------------------------
/termgram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enricostara/termgram/HEAD/termgram.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Logs
23 | log/*
24 |
25 | # Dev-Config
26 | *.yml
27 | .gitignore
28 | .npmignore
29 | gulpfile.js
30 |
31 | # Users Environment Variables
32 | .lock-wscript
33 |
34 | # IDE Project files
35 | *.iws
36 | *.iml
37 | *.ipr
38 | .idea
39 | .classpath
40 | .project
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
2 |
3 | ##TERMGRAM
4 |
5 | ###A terminal client to connect with [Telegram](http://www.telegram.org).
6 |
7 | The main purpose of the project is to provide a working **example** of
8 | how to use the [**telegram.link**](http://telegram.link) library in order to build a Telegram client application
9 |
10 | ## Install & Run
11 |
12 | ```bash
13 | $ npm install -g termgram
14 | $ termgram
15 | ```
16 |
17 | ## Node.js 0.12.x
18 | In order to take advantage of some new Javascript features, the **Node.js version required is the 0.12.x**
19 |
20 | **Note:** [**telegram.link**](http://telegram.link) just requires the version **0.10.X**
21 |
22 | ### License
23 |
24 | The project is released under the [MIT license](./LICENSE)
25 |
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Logs
26 | log/*
27 |
28 | # Dependency directory
29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
30 | node_modules
31 |
32 | # IDE Project files
33 | *.iws
34 | *.iml
35 | *.ipr
36 | .idea
37 | .classpath
38 | .project
--------------------------------------------------------------------------------
/i18n/mark-down.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('colors');
8 | var boldEx = /\*\*(.+?)\*\*/g;
9 | var emEx = /_(.+?)_/g;
10 |
11 | module.exports = function (i18n) {
12 | return convert(i18n);
13 | };
14 |
15 | function convert(obj) {
16 | for (var propertyName in obj) {
17 | var value = obj[propertyName];
18 | //console.log(propertyName + ': ' + value);
19 | switch (typeof value) {
20 | case 'string':
21 | obj[propertyName] = value.replace(boldEx, '$1'.bold).replace(emEx, '$1'.underline);
22 | break;
23 | default:
24 | obj[propertyName] = convert(value);
25 | }
26 | }
27 | return obj;
28 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "termgram",
3 | "version": "0.2.1",
4 | "description": "Termgram App",
5 | "keywords": [
6 | "telegram",
7 | "terminal",
8 | "client",
9 | "telegram.link"
10 | ],
11 | "author": "Enrico Stara ",
12 | "homepage": "http://termgram.me",
13 | "repository": {
14 | "type": "git",
15 | "url": "git://github.com/enricostara/termgram.git"
16 | },
17 | "preferGlobal": true,
18 | "bugs": {
19 | "url": "https://github.com/enricostara/termgram/issues"
20 | },
21 | "dependencies": {
22 | "telegram.link": "latest",
23 | "colors": "latest",
24 | "mute-stream": "latest",
25 | "requirish": "latest",
26 | "get-log": "latest"
27 | },
28 | "license": "MIT",
29 | "main": "./termgram",
30 | "engines": {
31 | "node": "0.12.x"
32 | },
33 | "bin": {
34 | "termgram": "./bin/run.js"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Enrico Stara
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/lib/user-data.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | var fs = require('fs');
8 | var util = require('util');
9 | var fileResolver = /^\.(\w+)\.user$/;
10 | var baseFolder = '.';
11 |
12 |
13 | function UserData(data) {
14 | util._extend(this, data);
15 | }
16 |
17 | UserData.prototype.setDataCenter = function (dataCenter) {
18 | this.dataCenter = dataCenter;
19 | };
20 |
21 | UserData.prototype.getDataCenter = function () {
22 | return this.dataCenter;
23 | };
24 |
25 | UserData.prototype.setAuthKey = function (authKeyBuffer) {
26 | this.authKey = authKeyBuffer.toString('base64');
27 | };
28 |
29 | UserData.prototype.getAuthKey = function () {
30 | return this.authKey ? new Buffer(this.authKey, 'base64') : null;
31 | };
32 |
33 | UserData.prototype.save = function () {
34 | var filePath = baseFolder + '/.' + this.name + '.user';
35 | var ws = fs.createWriteStream(filePath);
36 | ws.write(JSON.stringify(this));
37 | ws.end();
38 | };
39 |
40 | function retrieveUsernameList() {
41 | var list = fs.readdirSync(baseFolder);
42 | list = list.map(function (value) {
43 | var match = value.match(fileResolver);
44 | return (match ? match[1] : null);
45 | }).filter(function (value) {
46 | return value
47 | });
48 | return list;
49 | }
50 |
51 | function loadUser(username) {
52 | var filePath = baseFolder + '/.' + username + '.user';
53 | return new UserData(JSON.parse(fs.readFileSync(filePath)));
54 | }
55 |
56 | function setBaseFolder(folder) {
57 | baseFolder = folder;
58 | }
59 |
60 | module.exports = exports = UserData;
61 | exports.retrieveUsernameList = retrieveUsernameList;
62 | exports.loadUser = loadUser;
63 | exports.setBaseFolder = setBaseFolder;
64 |
65 |
--------------------------------------------------------------------------------
/lib/updates.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('requirish')._(module);
8 | require('colors');
9 |
10 | var clientProxy = require('lib/client-proxy');
11 | var getLogger = require('get-log');
12 | var logger = getLogger('update-emitter');
13 |
14 | var UPDATE_INTERVAL = 1000;
15 |
16 | function Updates() {
17 | }
18 |
19 | // Start the emitter.
20 | Updates.prototype.start = function () {
21 | var self = this;
22 | logger.info('start updates');
23 | console.log('start updates');
24 | return new Promise(function (fulfill, reject) {
25 | try {
26 | clientProxy.getClient().account.updateStatus(false).then(function () {
27 | clientProxy.getClient().registerOnUpdates(function (update) {
28 | console.log('update', update.toPrintable());
29 | });
30 | try {
31 | clientProxy.getClient().updates.getState().then(function (state) {
32 | try {
33 | setState.call(self, state);
34 | clientProxy.getClient().on('error', function (error) {
35 | console.log('client error', error.stack);
36 | });
37 | clientProxy.getClient().httpPoll();
38 | setTimeout(fulfill, 100);
39 | //fulfill();
40 | } catch (e) {
41 | reject(e)
42 | }
43 | }, reject);
44 | } catch (e) {
45 | reject(e)
46 | }
47 | });
48 | } catch (e) {
49 | reject(e)
50 | }
51 | });
52 | };
53 |
54 | function setState(state) {
55 | this.pts = state.pts;
56 | this.date = state.date;
57 | this.qts = state.qts;
58 | this.unreadCount = state.unread_count;
59 | logger.info('set state', state.toPrintable());
60 | console.log('set state', state.toPrintable());
61 | }
62 |
63 |
64 | // Stop the emitter.
65 | Updates.prototype.stop = function () {
66 | logger.info('stop updates');
67 | console.log('stop updates');
68 | clientProxy.getClient().stopHttpPollLoop();
69 | clientProxy.getClient().account.updateStatus(true);
70 | };
71 |
72 | var instance;
73 | Updates.getInstance = function () {
74 | return instance = instance || new Updates();
75 | };
76 |
77 | // export the services
78 | module.exports = exports = Updates;
--------------------------------------------------------------------------------
/lib/use-case/chat.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('requirish')._(module);
8 | require('colors');
9 |
10 | var clientProxy = require('lib/client-proxy');
11 | var ui = require('lib/user-interface');
12 | var i18n = require('i18n/en-US');
13 | var getLogger = require('get-log');
14 | var logger = getLogger('use-case.select-chat');
15 |
16 | var OPEN_TOTAL_MESSAGES = 10;
17 | var UPDATE_INTERVAL = 5000;
18 |
19 | function chat(peer) {
20 | return new Promise(function (fulfill, reject) {
21 | open(peer).then(function () {
22 | //fulfill();
23 | }, reject);
24 | });
25 | }
26 |
27 | function open(peer) {
28 | return new Promise(function (fulfill, reject) {
29 | ui.spinner();
30 | try {
31 | clientProxy.getClient().messages.getHistory(peer, 0, 0, OPEN_TOTAL_MESSAGES).then(function (container) {
32 | try {
33 | ui.spinner();
34 | // title
35 | var title;
36 | if (peer.instanceOf('api.type.InputPeerChat')) {
37 | title = container.chats.getById(peer.chat_id).title;
38 | } else {
39 | var user = container.users.getById(peer.user_id);
40 | title = user.first_name + ' ' + user.last_name;
41 | }
42 | ui.hRule();
43 | console.log('\t' + title.bold);
44 | // messages
45 | var total = container && container.messages && container.messages.list ? container.messages.list.length : 0;
46 | logger.info('Message list retrieved, total messages ', total);
47 | if (total > 0) {
48 | var lastUsername = '';
49 | for (var index = total - 1; index >= 0; index--) {
50 | lastUsername = renderMessage(container, index, lastUsername);
51 | }
52 | }
53 | // set history as read
54 | clientProxy.getClient().messages.readHistory(peer, 0, 0, true).
55 | then(fulfill, reject);
56 | } catch (e) {
57 | reject(e)
58 | }
59 | }, reject);
60 | } catch (e) {
61 | reject(e)
62 | }
63 | });
64 | }
65 |
66 | function renderMessage(container, index, lastUserName) {
67 | // message
68 | var message = container.messages.list[index];
69 | // check if a pure message
70 | if (!message.instanceOf('api.type.Message')) {
71 | ui.spacer();
72 | console.log(message.toPrintable());
73 | // todo: manage the MessageService type
74 | return lastUserName;
75 | }
76 | var msgText = message.media.instanceOf('api.type.MessageMediaEmpty') ? message.message : '[image]';
77 | // user
78 | var userName = container.users.getById(message.from_id).first_name;
79 | // render
80 | if (userName !== lastUserName) {
81 | ui.spacer();
82 | console.log(' ' + userName.bold.cyan);
83 | }
84 | console.log(' ' + msgText);
85 | return userName;
86 | }
87 |
88 | // export the services
89 | module.exports = exports = chat;
--------------------------------------------------------------------------------
/i18n/en-US.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | module.exports = require('./mark-down')
7 | ({
8 | "welcome": "Welcome to **TERMGRAM**, a terminal client to connect with _Telegram_.",
9 | "signUp": {
10 | "info": "To protect your Telegram identity you must create a local user/password,\n" +
11 | "**Termgram** will use your password to encrypt your authorization data saved locally.\n" +
12 | "Choose your **username**, it will be _local_ and it will _not be used_ by Telegram!",
13 | "choose_username": "Username",
14 | "choose_username_error": "The username must be between 3 and 12 characters long!",
15 | "choose_username_alreadyIn": "The username '**%s**' is already registered!",
16 | "choose_username_hello": "Hi **%s** !",
17 | "choose_passwordType_answer": "Which type of password fits your needs?",
18 | "choose_passwordType_option_simple": "**Numeric**, only 4 digits for fast typing",
19 | "choose_passwordType_option_simple_case": "Case: you are on your PC already protected by a secure password..",
20 | "choose_passwordType_option_strong": "**PassPhrase**, at least 10 characters long and " +
21 | "should contain lowercase/uppercase letters, numbers and symbols",
22 | "choose_passwordType_option_strong_case": "Case: you are connected remotely to a departmental server or to a nuclear plant or whatever..",
23 | "choose_passwordType": "Select the password type number",
24 | "choose_passwordType_chose": "You chose:",
25 | "choose_password_simple": "Password (4 digits)",
26 | "choose_password_strong": "Password (10+ chars)",
27 | "choose_password_retype": "Retype the password for confirmation",
28 | "choose_password_retype_error": "Hey, the _two passwords didn't match_!",
29 | "auth_inProgress": "Authorization on going, wait few seconds..",
30 | "ask_phoneNumber_info": "Provide your phone number in order to receive the verification code from Telegram",
31 | "ask_phoneNumber": "Phone number, must have the international code",
32 | "ask_authorizationCode": "Authorization code received by SMS",
33 | "phone_unregistered": "Your phone number is not registered, give some info to complete a new registration:",
34 | "choose_firstName": "First name",
35 | "choose_lastName": "Last name",
36 | "error_PHONE_NUMBER_INVALID": "Phone number **%s** is invalid! try again..",
37 | "error_PHONE_CODE_INVALID": "Authorization code **%s** is invalid! try again..",
38 | "error_PHONE_CODE_EXPIRED": "Authorization code is expired!",
39 | "error_FIRSTNAME_INVALID": "First name is invalid! try again..",
40 | "error_LASTNAME_INVALID": "Last name is invalid! try again.."
41 | },
42 | "signIn": {
43 | "info": "Termgram Login",
44 | "welcome": "Welcome back **%s**!",
45 | "ask_username": "Username",
46 | "ask_password": "Password",
47 | "ask_signUp": "The username provided is new, do you want to create a new local account?"
48 | },
49 | "selectChat": {
50 | "list": "Chats:",
51 | "noChatAvailable": "No chat available!",
52 | "userSelfName": "You",
53 | "choose_chat": "Choose a chat **number**"
54 | },
55 | "ui": {
56 | "askConfirmation": "y/n",
57 | "askConfirmation_defaultYes": "_Y_/n",
58 | "askConfirmation_defaultNo": "y/_N_",
59 | "error": {
60 | "askWord": "Hey, you must enter only alphanumeric chars!",
61 | "askNumeric": "Hey, you must enter only numeric chars!",
62 | "askPhone": "Hey, this is not an international phone number!",
63 | "askPassword": "Hey, the password didn't match the requirements!"
64 | }
65 | },
66 | "exit": "Are you sure you want to exit?"
67 | });
--------------------------------------------------------------------------------
/termgram.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // clear the term
7 | clearTerminal();
8 |
9 | // setup the fs
10 | var fs = require('fs');
11 | var userHome = (process.env.HOME || process.env.USERPROFILE) + '/.termgram';
12 | var logFolder = userHome + '/log';
13 | try {
14 | fs.mkdirSync(userHome, '0770');
15 | fs.mkdirSync(logFolder, '0770');
16 | } catch (e) {
17 | }
18 |
19 | // setup the logger
20 | process.env.LOGGER_FILE = logFolder + '/termgram';
21 | var getLogger = require('get-log');
22 | getLogger.PROJECT_NAME = 'termgram';
23 | var logger = getLogger('main');
24 |
25 | // import other dependencies
26 | require('colors');
27 | require('telegram.link')(getSignature());
28 | var ui = require('./lib/user-interface');
29 | var i18n = require('./i18n/en-US');
30 | var signUp = require('./lib/use-case/sign-up');
31 | var signIn = require('./lib/use-case/sign-in');
32 | var Updates = require('./lib/updates');
33 | var selectChat = require('./lib/use-case/select-chat');
34 | var chat = require('./lib/use-case/chat');
35 | var userData = require('./lib/user-data');
36 | userData.setBaseFolder(userHome);
37 |
38 |
39 | // begin
40 | function main() {
41 | var users = userData.retrieveUsernameList();
42 | console.log(i18n.welcome);
43 | ui.spacer();
44 |
45 | function doSignUp() {
46 | signUp(users).then(function (res) {
47 | logger.info('signUp res: %s', res);
48 | home();
49 | }, function (error) {
50 | console.log('signUp error: ', error.stack);
51 | shutdown();
52 | });
53 | }
54 |
55 | // if no users
56 | if (users.length == 0) {
57 | logger.info('User list is empty, sign up a new user.');
58 | doSignUp();
59 | } else {
60 | signIn(users).then(function (res) {
61 | logger.info('signIn res:', res);
62 | home();
63 | }, function (error) {
64 | if (error) {
65 | console.log('signIn error: ', error.stack);
66 | shutdown();
67 | } else {
68 | doSignUp();
69 | }
70 | });
71 | }
72 | ui.events.on(ui.EVENT.EXIT, function () {
73 | ui.askConfirmationInput(i18n.exit, true).then(shutdown, function () {
74 | console.log('nothing to do, again...')
75 | });
76 | });
77 | }
78 |
79 | // userHome page
80 | function home() {
81 | var updates = Updates.getInstance();
82 | updates.start().then(function() {
83 | ui.spacer();
84 | selectChat().then(function (peer) {
85 | if (peer) {
86 | ui.spacer();
87 | chat(peer).then(function () {
88 | console.log('nothing to do, now...');
89 | shutdown();
90 | }, function (error) {
91 | console.log('chat error: ', error.stack);
92 | shutdown();
93 | });
94 | } else {
95 | console.log('No chat selected');
96 | console.log('nothing to do, now...');
97 | shutdown();
98 | }
99 | }, function (error) {
100 | console.log('selectChat error: ', error.stack);
101 | shutdown();
102 | });
103 |
104 | }, function (error) {
105 | console.log('update-emitter error: ', error.stack);
106 | shutdown();
107 | });
108 | }
109 |
110 | // end
111 | function shutdown() {
112 | ui.close();
113 | Updates.getInstance().stop();
114 | }
115 |
116 | // clear the term
117 | function clearTerminal() {
118 | process.stdout.write('\033c');
119 | }
120 |
121 | // get the application signature
122 | function getSignature() {
123 | return (' T E R M G R A M '.bold + ' ' + require('./package.json').version ).cyan;
124 | }
125 |
126 | // run
127 | main();
--------------------------------------------------------------------------------
/lib/use-case/select-chat.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('requirish')._(module);
8 | require('colors');
9 |
10 | var api = require('telegram.link')();
11 | var clientProxy = require('lib/client-proxy');
12 | var ui = require('lib/user-interface');
13 | var i18n = require('i18n/en-US');
14 | var getLogger = require('get-log');
15 | var logger = getLogger('use-case.select-chat');
16 |
17 | function selectChat() {
18 | return new Promise(function (fulfill, reject) {
19 | ui.spinner();
20 | try {
21 | clientProxy.getClient().messages.getDialogs(0, 0, 0).then(function (container) {
22 | try {
23 | ui.spinner();
24 | var total = container && container.dialogs && container.dialogs.list ? container.dialogs.list.length : 0;
25 | logger.info('Chat list retrieved, total chats ', total);
26 | if (total > 0) {
27 | ui.hRule();
28 | console.log('\t' + i18n.selectChat.list);
29 | ui.spacer();
30 | var peers = {};
31 | for (var index = total - 1; index >= 0; index--) {
32 | peers[index] = renderChat(container, index);
33 | ui.spacer();
34 | }
35 | ui.askNumericInput(i18n.selectChat.choose_chat, '1').then(function (index) {
36 | logger.info('Selected chat:', index);
37 | fulfill(peers[index - 1]);
38 | });
39 | } else {
40 | console.log(i18n.selectChat.noChatAvailable);
41 | fulfill(null);
42 | }
43 | } catch (e) {
44 | reject(e)
45 | }
46 | }, reject);
47 | } catch (e) {
48 | reject(e)
49 | }
50 | });
51 | }
52 |
53 | function renderChat(container, index) {
54 | var dialog = container.dialogs.list[index];
55 | var unreadCount = dialog.unread_count > 0 ? ('(' + dialog.unread_count + ')').bold : '';
56 | var message = container.messages.getById(dialog.top_message);
57 | var msgText = message.media.instanceOf('api.type.MessageMediaEmpty') ? message.message : '[image]';
58 | var maxLength = 50;
59 | msgText = msgText.length > maxLength ? msgText.slice(0, maxLength - 3) + '...' : msgText;
60 |
61 | var user;
62 | var title;
63 | var peer = dialog.peer;
64 | var inputPeer;
65 | if (peer.instanceOf('api.type.PeerChat')) {
66 | var chat = container.chats.getById(peer.chat_id);
67 | title = chat.title;
68 | user = container.users.getById(message.from_id);
69 | inputPeer = new api.type.InputPeerChat();
70 | inputPeer.chat_id = peer.chat_id;
71 | } else if (peer.instanceOf('api.type.PeerUser')) {
72 | user = container.users.getById(peer.user_id);
73 | inputPeer = retrieveInputPeer(user);
74 | } else {
75 | throw new Error('Unknown peer type ' + peer.getTypeName());
76 | }
77 |
78 | var userName = user.instanceOf('api.type.UserSelf') ?
79 | i18n.selectChat.userSelfName :
80 | (user.first_name + ' ' + user.last_name);
81 | title = title ? title.bold + ' - ' + userName : userName.bold;
82 |
83 | ui.option(index + 1, title + ' ' + unreadCount);
84 | console.log('\t\t' + msgText);
85 | return inputPeer;
86 | }
87 |
88 | function retrieveInputPeer(user) {
89 | var inputPeer;
90 | if (user.instanceOf('api.type.UserContact')) {
91 | inputPeer = new api.type.InputPeerContact();
92 | inputPeer.user_id = user.id;
93 | } else if (user.instanceOf('api.type.UserRequest') || user.instanceOf('api.type.UserForeign')) {
94 | inputPeer = new api.type.InputPeerForeign();
95 | inputPeer.user_id = user.id;
96 | inputPeer.access_hash = user.access_hash;
97 | } else {
98 | throw new Error('Unknown user type ' + user.getTypeName());
99 | }
100 | return inputPeer;
101 | }
102 |
103 | // export the services
104 | module.exports = exports = selectChat;
--------------------------------------------------------------------------------
/lib/use-case/sign-in.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('requirish')._(module);
8 | require('colors');
9 |
10 | var clientProxy = require('lib/client-proxy');
11 | var ui = require('lib/user-interface');
12 | var UserData = require('lib/user-data');
13 | var i18n = require('i18n/en-US');
14 | var getLogger = require('get-log');
15 | var logger = getLogger('use-case.sign-in');
16 | var userData;
17 |
18 | function signIn(users) {
19 | return new Promise(function (fulfill, reject) {
20 | console.log(i18n.signIn.info);
21 | askUsername(users).then(function (username) {
22 | userData = UserData.loadUser(username);
23 | logger.info('User %s data loaded', username, userData);
24 | askPassword(username).then(function () {
25 | ui.spacer();
26 | console.log(i18n.signIn.welcome, username);
27 | clientProxy.createClientForUser(userData.getDataCenter()).then(fulfill, reject);
28 | /*
29 | var clientPromise = clientProxy.createClientForUser(userData.getDataCenter());
30 | clientPromise.then(function () {
31 | try{
32 | //var peer = new api.type.InputPeerContact({props: {user_id: ....}});
33 | //var msg = 'Test';
34 | //
35 | //clientProxy.getClient().messages.sendMessage(peer, msg, 9876543211).then(function(sentMsg) {
36 | // console.log('sentMsg:', sentMsg.toPrintable());
37 | // fulfill();
38 | //}, reject);
39 | //clientProxy.getClient().messages.getHistory(peer, 0, 0, 100).then(function(messages) {
40 | // console.log('messages:', messages.toPrintable());
41 | // fulfill();
42 | //}, reject);
43 |
44 | //clientProxy.getClient().messages.getDialogs(0, 0, 1).then(function(dialogs) {
45 | // console.log('dialogs:', dialogs.toPrintable());
46 | // fulfill();
47 | //}, reject);
48 |
49 | //clientProxy.getClient().contacts.getContacts('').then(function(contacts) {
50 | // console.log('contacts:', contacts.toPrintable());
51 | // fulfill();
52 | //}, reject);
53 |
54 | //clientProxy.getClient().updates.getState().then(function(state) {
55 | // console.log('state:', state.toPrintable());
56 | // fulfill();
57 | //}, reject);
58 |
59 | console.log('client created');
60 | } catch (e) {
61 | reject(e)
62 | }
63 | });
64 | */
65 | }, reject)
66 | }, function () {
67 | ui.askConfirmationInput(i18n.signIn.ask_signUp, true).then(
68 | function () {
69 | ui.spacer();
70 | reject();
71 | },
72 | function () {
73 | ui.spacer();
74 | signIn(users);
75 | });
76 | });
77 | });
78 | }
79 |
80 |
81 | function askUsername(usernameList) {
82 | return new Promise(function (fulfill, reject) {
83 | ui.askWordInput(i18n.signIn.ask_username, process.env.USER).then(function (username) {
84 | if (usernameList.indexOf(username) < 0) {
85 | reject();
86 | } else {
87 | fulfill(username);
88 | }
89 | });
90 | })
91 | }
92 |
93 | var attempts = 3;
94 | function askPassword() {
95 | return new Promise(function (fulfill, reject) {
96 | var authKeyBuffer = userData.getAuthKey();
97 | (function ask() {
98 | ui.askPasswordInput(i18n.signIn.ask_password).then(function (password) {
99 | var authKey = clientProxy.setAuthKey(authKeyBuffer, password);
100 | if (!authKey) {
101 | if (--attempts > 0) {
102 | ask();
103 | } else {
104 | reject(new Error('User failed to authenticate after 3 attempts.'));
105 | }
106 | } else {
107 | fulfill()
108 | }
109 | });
110 | })();
111 | })
112 | }
113 |
114 |
115 | // export the services
116 | module.exports = exports = signIn;
--------------------------------------------------------------------------------
/lib/user-interface.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('requirish')._(module);
8 | var outStream = new (require('mute-stream'))({replace: '*'});
9 | outStream.pipe(process.stdout);
10 | require('colors');
11 | var i18n = require('i18n/en-US');
12 | var rl = require('readline').createInterface(process.stdin, outStream);
13 | var events = new (require('events').EventEmitter);
14 |
15 |
16 | function askWordInput(question, defaultAnswer) {
17 | return new Promise(function (fulfill) {
18 | ask(question, defaultAnswer, /^[\w\+]+$/, i18n.ui.error.askWord, function (answer) {
19 | fulfill(answer);
20 | })
21 | });
22 | }
23 |
24 | function askPasswordInput(question, regEx) {
25 | return new Promise(function (fulfill, reject) {
26 | rl.setPrompt(question + ': ');
27 | rl.prompt();
28 | outStream.mute();
29 | rl.once('line', function (password) {
30 | outStream.unmute();
31 | if (!regEx) {
32 | fulfill(password);
33 | } else if (password.match(regEx)) {
34 | fulfill(password);
35 | } else {
36 | console.log(i18n.ui.error.askPassword);
37 | reject();
38 | }
39 | });
40 | });
41 | }
42 |
43 | function askNumericInput(question, defaultAnswer) {
44 | return new Promise(function (fulfill) {
45 | ask(question, defaultAnswer, /^\d+$/, i18n.ui.error.askNumeric, function (answer) {
46 | fulfill(answer);
47 | })
48 | });
49 | }
50 |
51 | function askPhoneInput(question, defaultAnswer) {
52 | return new Promise(function (fulfill) {
53 | ask(question, defaultAnswer, /^(\+|00)\d{5,}$/, i18n.ui.error.askPhone, function (answer) {
54 | fulfill(answer);
55 | })
56 | });
57 | }
58 |
59 | function ask(question, defaultAnswer, regex, errorMsg, callback) {
60 | var postfix = defaultAnswer ? ' (' + defaultAnswer.underline + ')' : '';
61 | rl.question(question + postfix + ': ', function (answer) {
62 | if (defaultAnswer && answer.length === 0) {
63 | callback(defaultAnswer);
64 | } else if (answer.match(regex)) {
65 | callback(answer);
66 | } else {
67 | console.log(errorMsg);
68 | ask(question, defaultAnswer, regex, errorMsg, callback);
69 | }
70 | });
71 | }
72 |
73 | function askConfirmationInput(question, defaultYes) {
74 | return new Promise(function (fulfill, reject) {
75 | var postfix = defaultYes === undefined ? i18n.ui.askConfirmation :
76 | (defaultYes === false ? i18n.ui.askConfirmation_defaultNo : i18n.ui.askConfirmation_defaultYes);
77 | rl.question(question + ' (' + postfix + '): ', function (answer) {
78 | if (defaultYes !== undefined && answer.length === 0) {
79 | if (defaultYes) {
80 | fulfill();
81 | } else {
82 | reject();
83 | }
84 | } else if (answer.match(/^y(es)?$/i)) {
85 | fulfill();
86 | } else {
87 | reject();
88 | }
89 | });
90 | });
91 | }
92 |
93 | function spacer(rows) {
94 | var spacer = '';
95 | if (rows) {
96 | for (var i = 1; i < rows; i++) {
97 | spacer += '\n';
98 | }
99 | }
100 | console.log(spacer);
101 | }
102 |
103 | function hRule() {
104 | console.log('________________________________________\n');
105 | }
106 |
107 | var interval;
108 | function spinner() {
109 | if(interval) {
110 | clearInterval(interval);
111 | outStream.write(' \b');
112 | rl.resume();
113 | return;
114 | }
115 | var chars = ['\\', '|', '/', '-'];
116 | var i = 0;
117 | interval = setInterval(function () {
118 | outStream.write(chars[i++ % 4]);
119 | outStream.write('\b');
120 | }, 200);
121 | rl.pause();
122 | }
123 |
124 | function option(key, msg) {
125 | console.log('\t[ ' + ('' + key).bold.cyan + ' ] ' + msg);
126 | }
127 |
128 |
129 | rl.on('SIGINT', function () {
130 | events.emit(exports.EVENT.EXIT);
131 | });
132 |
133 | function close() {
134 | rl.close();
135 | }
136 |
137 | exports.askWordInput = askWordInput;
138 | exports.askPasswordInput = askPasswordInput;
139 | exports.askNumericInput = askNumericInput;
140 | exports.askPhoneInput = askPhoneInput;
141 | exports.askConfirmationInput = askConfirmationInput;
142 | exports.spacer = spacer;
143 | exports.hRule = hRule;
144 | exports.spinner = spinner;
145 | exports.option = option;
146 | exports.close = close;
147 | exports.events = events;
148 | exports.EVENT = {
149 | EXIT: 'exit'
150 | };
151 |
--------------------------------------------------------------------------------
/lib/client-proxy.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | var telegramLink = require('telegram.link')();
8 | var getLogger = require('get-log');
9 | var logger = getLogger('client-factory');
10 | var os = require('os');
11 |
12 | // set the environment
13 | var app = {
14 | // NOTE: if you FORK the project you MUST use your APP ID.
15 | // Otherwise YOUR APPLICATION WILL BE BLOCKED BY TELEGRAM
16 | // You can obtain your own APP ID for your application here: https://my.telegram.org
17 | id: 19896,
18 | hash: 'b43316c048960d6a599d4fe3497c3610',
19 | version: require('../package.json').version,
20 | lang: 'en',
21 | deviceModel: os.type().replace('Darwin', 'OS_X'),
22 | systemVersion: os.platform() + '/' + os.release()
23 | //,connectionType: 'TCP'
24 | };
25 |
26 | //var primaryDC = telegramLink.TEST_PRIMARY_DC;
27 | var primaryDC = telegramLink.PROD_PRIMARY_DC;
28 |
29 | var client;
30 | var dataCenters;
31 | var authKey;
32 |
33 | function createClientForUser(dataCenter) {
34 | if (client) {
35 | throw new Error('Authorization key already created.')
36 | }
37 | return new Promise(function (fulfill, reject) {
38 | client = createClient(dataCenter || primaryDC, function () {
39 | client.once('error', reject);
40 | if(!dataCenter) {
41 | logger.info('Look for the nearest data centers..');
42 | client.getDataCenters(function (dcs) {
43 | client.removeListener('error', reject);
44 | dataCenters = dcs;
45 | logger.info('Data centers: ', dataCenters.toPrintable());
46 | //check if the current data-center is also the nearest one
47 | if (dataCenters.current != dataCenters.nearest) {
48 | var newDc = dataCenters[dataCenters.nearest];
49 | logger.info('Change the current data-center with the nearest one %s = %s:%s', dataCenters.nearest, newDc.host, newDc.port);
50 | authKey = null;
51 | client = createClient(newDc, fulfill, reject);
52 | } else {
53 | fulfill(dataCenters[dataCenters.current]);
54 | }
55 | });
56 | } else {
57 | fulfill(dataCenter);
58 | }
59 | }, reject);
60 | });
61 | }
62 |
63 | function sendCodeToPhone(phoneNumber) {
64 | return new Promise(function (fulfill, reject) {
65 | client.once('error', reject);
66 | client.auth.sendCode(phoneNumber, 5, 'en', function (result) {
67 | if (result.instanceOf('mtproto.type.Rpc_error')) {
68 | switch (result.error_code) {
69 | // check if the phone number requires a different data-center (PHONE_MIGRATE_X)
70 | case 303:
71 | var requiredDCName = 'DC_' + result.error_message.slice(-1);
72 | logger.info('The phone number %s requires the data-center %s', phoneNumber, requiredDCName);
73 | var requiredDC = dataCenters[requiredDCName];
74 | client = createClient(requiredDC, function () {
75 | client.sendCodeToPhone(phoneNumber, fulfill);
76 | }, reject);
77 | break;
78 | default :
79 | reject(new Error(result.error_message));
80 | }
81 | } else {
82 | fulfill(result);
83 | }
84 | });
85 | });
86 | }
87 |
88 | function signIn(phoneNumber, codeHash, code) {
89 | return new Promise(function (fulfill, reject) {
90 | client.once('error', reject);
91 | client.auth.signIn(phoneNumber, codeHash, code, function (result) {
92 | if (result.instanceOf('mtproto.type.Rpc_error')) {
93 | if (result.error_message == 'PHONE_NUMBER_UNOCCUPIED') {
94 | fulfill(result);
95 | } else {
96 | reject(new Error(result.error_message));
97 | }
98 | } else {
99 | fulfill(result);
100 | }
101 | });
102 | });
103 | }
104 |
105 |
106 | function getClient() {
107 | return client;
108 | }
109 |
110 | function createClient(dataCenter, fulfill, reject) {
111 | if (client) {
112 | client.end();
113 | client = null;
114 | }
115 | logger.info('Start to create the client connecting to DC %s:%s..', dataCenter.host, dataCenter.port);
116 | var newClient = telegramLink.createClient(app, dataCenter, function () {
117 | if (!authKey) {
118 | logger.info('Start to create the auth-key..');
119 | newClient.createAuthKey(function (auth) {
120 | logger.info('..the auth-key [%s] was created, the client is now ready.', auth.key.id.toString('hex'));
121 | authKey = auth.key;
122 | fulfill(dataCenter);
123 | });
124 | } else {
125 | logger.info('auth-key already set');
126 | fulfill(dataCenter);
127 | }
128 |
129 | });
130 | newClient.once('error', reject);
131 | logger.info('..client created.');
132 | return newClient;
133 | }
134 |
135 | function getAuthKey() {
136 | return authKey;
137 | }
138 |
139 | function setAuthKey(authKeyBuffer, password) {
140 | authKey = telegramLink.retrieveAuthKey(authKeyBuffer, password);
141 | app.authKey = authKey;
142 | return authKey;
143 | }
144 |
145 | // export the services
146 | exports.createClientForUser = createClientForUser;
147 | exports.sendCodeToPhone = sendCodeToPhone;
148 | exports.signIn = signIn;
149 | exports.getAuthKey = getAuthKey;
150 | exports.setAuthKey = setAuthKey;
151 | exports.getClient = getClient;
152 |
--------------------------------------------------------------------------------
/lib/use-case/sign-up.js:
--------------------------------------------------------------------------------
1 | // Termgram
2 | // Copyright 2015 Enrico Stara 'enrico.stara@gmail.com'
3 | // Released under the MIT License
4 | // http://termgram.me
5 |
6 | // import the dependencies
7 | require('requirish')._(module);
8 | require('colors');
9 | var clientProxy = require('lib/client-proxy');
10 | var ui = require('lib/user-interface');
11 | var UserData = require('lib/user-data');
12 | var i18n = require('i18n/en-US');
13 | var getLogger = require('get-log');
14 | var logger = getLogger('use-case.sign-up');
15 |
16 | // constants
17 | var PASSWORD_TYPE = {
18 | SIMPLE: '1',
19 | STRONG: '2'
20 | };
21 |
22 | function signUp(users) {
23 | return new Promise(function (fulfill, reject) {
24 | console.log(i18n.signUp.info);
25 | ui.spacer();
26 | askUsername(users, function (username) {
27 | logger.info('New user name: %s', username);
28 | var userData = new UserData({name: username});
29 | ui.spacer();
30 | console.log(i18n.signUp.choose_username_hello, username);
31 | var clientPromise = clientProxy.createClientForUser();
32 | askPassword(function (password) {
33 | console.log(i18n.signUp.auth_inProgress);
34 | ui.spinner();
35 | clientPromise.then(function (dataCenter) {
36 | userData.setDataCenter(dataCenter);
37 | ui.spinner();
38 | ui.spacer();
39 | askPhoneNumberToSendCode().then(function (res) {
40 | logger.info('Send code to phone number response: %s', res.sendCodeRes.toPrintable());
41 | // ask the received code
42 | askAuthCode(res.phoneNumber, res.sendCodeRes.phone_code_hash).then(function (authCodeRes) {
43 | function saveUser(signUpRes) {
44 | try {
45 | var authKeyBuffer = clientProxy.getAuthKey().encrypt(password);
46 | userData.setAuthKey(authKeyBuffer);
47 | userData.save();
48 | logger.info('User data saved');
49 | fulfill(signUpRes);
50 | } catch (e) {
51 | reject(e);
52 | }
53 | }
54 | logger.info('Auth code response: %s', authCodeRes.res.toPrintable());
55 | if (res.sendCodeRes.phone_registered) {
56 | saveUser(authCodeRes);
57 | } else {
58 | askFirstLastName(res.phoneNumber, res.sendCodeRes.phone_code_hash, authCodeRes.code).then(function(firstLastNamesRes) {
59 | saveUser(firstLastNamesRes);
60 | }, reject)
61 | }
62 | }, reject);
63 | });
64 | }, reject);
65 | });
66 | }, reject);
67 | });
68 | }
69 |
70 |
71 | function askUsername(users, callback) {
72 | ui.askWordInput(i18n.signUp.choose_username, process.env.USER).then(function (user) {
73 | if (user.length < 3 || user.length > 12) {
74 | console.log(i18n.signUp.choose_username_error);
75 | ui.spacer();
76 | askUsername(users, callback);
77 | } else if (users.indexOf(user) > -1) {
78 | console.log(i18n.signUp.choose_username_alreadyIn, user);
79 | ui.spacer();
80 | askUsername(users, callback);
81 | } else {
82 | callback(user);
83 | }
84 | });
85 | }
86 |
87 | function askPassword(callback) {
88 | console.log(i18n.signUp.choose_passwordType_answer);
89 | ui.spacer();
90 | ui.option(PASSWORD_TYPE.SIMPLE,
91 | i18n.signUp.choose_passwordType_option_simple + '\n (' + i18n.signUp.choose_passwordType_option_simple_case + ')');
92 | ui.spacer();
93 | ui.option(PASSWORD_TYPE.STRONG,
94 | i18n.signUp.choose_passwordType_option_strong + '\n (' + i18n.signUp.choose_passwordType_option_strong_case + ')');
95 | ui.spacer();
96 | (function askType() {
97 | ui.askNumericInput(i18n.signUp.choose_passwordType, '1').then(function (passwordType) {
98 | switch (passwordType) {
99 | case PASSWORD_TYPE.SIMPLE:
100 | ui.spacer();
101 | console.log('You chose: ' + i18n.signUp.choose_passwordType_option_simple);
102 | (function askSimple() {
103 | askTwice(i18n.signUp.choose_password_simple,
104 | i18n.signUp.choose_password_retype, /^\d{4}$/).then(callback, askSimple);
105 | })();
106 | break;
107 | case PASSWORD_TYPE.STRONG:
108 | ui.spacer();
109 | console.log(i18n.signUp.choose_passwordType_chose + ' ' + i18n.signUp.choose_passwordType_option_strong);
110 | (function askStrong() {
111 | askTwice(i18n.signUp.choose_password_strong,
112 | i18n.signUp.choose_password_retype, /^.{10,}$/).then(callback, askStrong);
113 | })();
114 | break;
115 | default:
116 | askType();
117 | }
118 | });
119 | })();
120 | function askTwice(question, retypeQuestion, regEx) {
121 | return new Promise(function (fulfill, reject) {
122 | ui.askPasswordInput(question, regEx).then(function (password) {
123 | ui.askPasswordInput(retypeQuestion, regEx).then(function (password2) {
124 | if (password == password2) {
125 | fulfill(password);
126 | } else {
127 | console.log(i18n.signUp.choose_password_retype_error);
128 | ui.spacer();
129 | reject();
130 | }
131 | }, function () {
132 | ui.spacer();
133 | reject();
134 | });
135 | }, function () {
136 | ui.spacer();
137 | reject();
138 | });
139 | });
140 | }
141 | }
142 |
143 | function askPhoneNumberToSendCode() {
144 | return new Promise(function (fulfill, reject) {
145 | console.log(i18n.signUp.ask_phoneNumber_info);
146 | (function askPhone() {
147 | ui.askPhoneInput(i18n.signUp.ask_phoneNumber).then(function (phoneNumber) {
148 | logger.info('Phone number: %s', phoneNumber);
149 | ui.spinner();
150 | clientProxy.sendCodeToPhone(phoneNumber).then(function (res) {
151 | ui.spinner();
152 | fulfill({
153 | phoneNumber: phoneNumber,
154 | sendCodeRes: res
155 | });
156 | }, function (error) {
157 | console.log(i18n.signUp['error_' + error.message], phoneNumber);
158 | if (error.message == 'PHONE_NUMBER_INVALID') {
159 | askPhone();
160 | } else {
161 | reject(error);
162 | }
163 | })
164 | });
165 | })();
166 | });
167 | }
168 |
169 | function askAuthCode(phoneNumber, code_hash) {
170 | return new Promise(function (fulfill, reject) {
171 | (function askCode() {
172 | ui.askNumericInput(i18n.signUp.ask_authorizationCode).then(function (code) {
173 | ui.spinner();
174 | // check if the authorization is valid
175 | clientProxy.signIn(
176 | phoneNumber,
177 | code_hash,
178 | code).then(function (res) {
179 | ui.spinner();
180 | fulfill({
181 | code: code,
182 | res: res
183 | });
184 | }, function (error) {
185 | console.log(i18n.signUp['error_' + error.message], code);
186 | if (error.message == 'PHONE_CODE_INVALID') {
187 | askCode();
188 | } else {
189 | reject(error);
190 | }
191 | });
192 | });
193 | })();
194 | });
195 | }
196 |
197 | function askFirstLastName(phoneNumber, code_hash, code) {
198 | return new Promise(function (fulfill, reject) {
199 | console.log(i18n.signUp.phone_unregistered);
200 | (function askNames() {
201 | ui.askWordInput(i18n.signUp.choose_firstName).then(function (firstName) {
202 | ui.askWordInput(i18n.signUp.choose_lastName).then(function (lastName) {
203 | ui.spinner();
204 | clientProxy.getClient().auth.signUp(
205 | phoneNumber,
206 | code_hash,
207 | code,
208 | firstName,
209 | lastName
210 | ).then(function (res) {
211 | ui.spinner();
212 | fulfill(res);
213 | }, function (error) {
214 | console.log(i18n.signUp['error_' + error.message], code);
215 | if (error.message == 'FIRSTNAME_INVALID' || error.message == 'LASTNAME_INVALID') {
216 | askNames();
217 | } else {
218 | reject(error);
219 | }
220 | });
221 | });
222 | });
223 | })();
224 | });
225 | }
226 |
227 | // export the services
228 | module.exports = exports = signUp;
--------------------------------------------------------------------------------