├── .gitignore ├── README.md ├── client.js ├── config.sample.json ├── hangups_client.py ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | persist 3 | config.json 4 | node_modules 5 | *.yaml 6 | /env 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # matrix-puppet-hangouts [![#matrix-puppet-bridge:matrix.org](https://img.shields.io/matrix/matrix-puppet-bridge:matrix.org.svg?label=%23matrix-puppet-bridge%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#matrix-puppet-bridge:matrix.org) 2 | 3 | This is a Matrix bridge for Google Hangouts. 4 | It logs in as (aka "puppets") both your matrix user and your hangouts user to 5 | establish the bridge. For more information, see: 6 | https://github.com/matrix-hacks/matrix-puppet-bridge 7 | 8 | To interact with the google hangouts servers, this bridge uses a python hangouts client library called hangups: 9 | https://github.com/tdryer/hangups 10 | 11 | ## Requirements 12 | 13 | ### For hangups, and our own python, python3 (Python 3.5+ for async) is required: 14 | sudo apt install python3 python3-dev 15 | (Or similar for your package manager) 16 | 17 | Make sure your python3 is version 3.5+ by running `python3 --version` 18 | 19 | ### Install hangups system-wide: 20 | `sudo pip3 install hangups` 21 | 22 | ## Installation 23 | 24 | Clone this repo or download a release of the puppet 25 | 26 | cd into the directory 27 | 28 | run `npm install` 29 | 30 | ## Configure 31 | 32 | Copy `config.sample.json` to `config.json` and update it to match your setup. 33 | 34 | You should really only have to edit the `homeserverUrl` and `domain` values. 35 | If you are running the puppet on the same server as your homeserver, then you should probably 36 | set the `homeserverUrl` value to be `http://127.0.0.1:8008`. 37 | 38 | 39 | ### Login to hangouts to save your authentication token. 40 | 41 | Run the following 42 | 43 | ``` 44 | mkdir -p ~/.cache/hangups 45 | python3 hangups_client.py --login-and-save-token 46 | ``` 47 | 48 | This saves an authentication token into the default hangups token path (`~/.cache/hangups/` as of this writing). 49 | 50 | #### Troubleshooting Login 51 | 52 | ##### Refresh token not found 53 | 54 | If you get an error like this 55 | 56 | ``` 57 | Traceback (most recent call last): 58 | File "/home/keyvan/code/matrix-puppet-hangouts/env/lib/python3.6/site-packages/hangups/auth.py", line 158, in get_auth 59 | raise GoogleAuthError("Refresh token not found") 60 | hangups.auth.GoogleAuthError: Refresh token not found 61 | ``` 62 | 63 | Then try logging in through a real browser first (lynx should work) to get past the SMS verification. 64 | 65 | If you are still having this issue, make sure you turn off "Login with my phone" and "2-step verification". You may be able to turn these back on AFTER you've successfully logged in, but this is unconfirmed. 66 | 67 | You can control these settings here: https://myaccount.google.com/security 68 | 69 | ##### Authorization code cookie not found 70 | 71 | If you get an error that says something along the lines of: 72 | 73 | `Login failed (Authorization code cookie not found)` 74 | 75 | You may get your refresh token manually using hangups itself: 76 | 77 | ``` 78 | hangups --manual 79 | ``` 80 | (follow the instructions on screen in order to get the refresh token) 81 | 82 | While this will take you into a hangups TUI once everything is done, you can exit it with Control+E. 83 | 84 | 85 | ### Register the app service 86 | 87 | Note: `your-bridge-server` is simply the IP address or hostname that your puppet will be running on, as it utilizes its own HTTP server for functionality. 88 | If you are running Synapse and the puppet on the same system, feel free to use `localhost` or `127.0.0.1`. 89 | 90 | Generate an `hangouts-registration.yaml` file with `node index.js -r -u "http://your-bridge-server:8090"` 91 | 92 | This command above will ask you to sign in as a user. You should sign in as a user that is an administrator, otherwise you might experience problems. 93 | 94 | Note: The 'registration' setting in the config.json needs to set to the path of this file. By default, it already is. 95 | 96 | Copy this `hangouts-registration.yaml` file to your home server (or to the homeserver's configuration directory if the puppet and homeserver are the same system), 97 | then edit it, setting its url to point to your bridge server. e.g. `url: 'http://your-bridge-server.example.org:8090'`. 98 | 99 | Edit your homeserver.yaml file and update the `app_service_config_files` with the path to the `hangouts-registration.yaml` file. 100 | 101 | Launch the bridge with ```npm start```. 102 | 103 | Restart your HS. 104 | 105 | A handy way to control your puppet is through `systemd`. Here is a `systemd` service, that can be called `hangouts-puppet` and placed at `/etc/systemd/system`. 106 | ``` 107 | [Unit] 108 | Description=Hangouts Matrix Puppet 109 | 110 | [Service] 111 | Type=simple 112 | Environment=DEBUG=verbose:matrix-puppet:* 113 | User=my-user-who-is-running-puppet-here 114 | Group=my-group-who-is-running-puppet-here 115 | WorkingDirectory=/my/path/to/downloaded/puppet/here 116 | ExecStart=/usr/bin/npm start 117 | Restart=always 118 | RestartSec=3 119 | 120 | [Install] 121 | WantedBy=multi-user.target 122 | ``` 123 | This service will auto-restart when the puppet fails. If you want that to not be the case, remove the lines: 124 | ``` 125 | Restart=always 126 | RestartSec=3 127 | ``` 128 | 129 | ## Discussion, Help and Support 130 | 131 | Join us in the [![Matrix Puppet Bridge](https://user-images.githubusercontent.com/13843293/52007839-4b2f6580-24c7-11e9-9a6c-14d8fc0d0737.png)](https://matrix.to/#/#matrix-puppet-bridge:matrix.org) room 132 | 133 | ## Using this puppet to bridge two group chats 134 | This is a simple hack in order for this puppet to be usable to bridge two group chats, one on Matrix and one on Hangouts. 135 | 136 | 1. Have a Google account that will be just for this puppet and create your hangups refresh token with this account. 137 | 2. Run `register_new_matrix_user -c homeserver.yaml http://localhost:8008` on your Synapse homeserver. Make sure that this user is an administrator. 138 | 3. Now run `node index.js -r -u "http://your-bridge-server:8090"`, but use the admin user you created before this in the registration. 139 | 4. Follow other steps as detailed to get the puppet running. 140 | 5. When the puppet starts successfully with `npm start`, invite the puppet's Google account to the group chat on Hangouts. 141 | 6. By default, the sender's name is not shown, which makes it very difficult to bridge two group chats, because you wouldn't know who is who on the Matrix side in Hangouts. 142 | However, if you replace this block of code in `index.js` 143 | 144 | ```js 145 | sendMessageAsPuppetToThirdPartyRoomWithId(id, text) { 146 | return this.thirdPartyClient.send(id, text); 147 | } 148 | sendImageMessageAsPuppetToThirdPartyRoomWithId(id, data) { 149 | return this.thirdPartyClient.sendImage(id, data); 150 | } 151 | ``` 152 | with: 153 | ```js 154 | sendMessageAsPuppetToThirdPartyRoomWithId(id, text, event) { 155 | return this.thirdPartyClient.send(id, "**" + event.sender.split(":")[0].substring(1) + "**\n" + text); 156 | } 157 | sendImageMessageAsPuppetToThirdPartyRoomWithId(id, data, event) { 158 | this.sendMessageAsPuppetToThirdPartyRoomWithId(id, "**" + event.sender.split(':')[0].substring(1) + "**", event); 159 | return this.thirdPartyClient.sendImage(id, data); 160 | } 161 | ``` 162 | you should be able to see the sender's name from Matrix in the Hangouts chat. 163 | 164 | All this does is the puppet adds a line before every message stating the sender in bold. 165 | While this only shows the localpart (username without homeserver), if you replace `event.sender.split(":")[0].substring(1)` with just `event.sender`, you will be able to see the username with the homeserver at the end, like `@username:homeserver.here`. 166 | 167 | At the end, you should be able to just log in to Riot or your favorite client as that admin user that you created, and invite your real account to the "bridged" room. 168 | ## TODO 169 | * Be able to start a brand new hangouts conversation fully from within a matrix client by choosing participants from your google contacts list. Currently, to start a new conversation this way, you'll have to start the conversation with an official hangouts client where your full contact list is available and once you send a message a bridged room will automatically be created for you. After this, you can carry on the rest of the conversation using your matrix client. Naturally, any incoming message will also automatically create a bridged room for you, so this limitation only applies when creating brand new rooms yourself. See this comment by tfreedman for a proposed solution to this problem - https://github.com/kfatehi/matrix-puppet-facebook/issues/2#issuecomment-274170696 and if you have any better ideas, please let us know! 170 | * Add a bot to each hangouts bridge room and use the bot's presence to indicate whether the bridge is running. This is an easy way to check on the status of the bridge. 171 | * Read receipt support. 172 | * Typing notification support. 173 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug'); 2 | const debugVerbose = debug('verbose:matrix-puppet:hangouts:client'); 3 | const EventEmitter = require('events').EventEmitter; 4 | const Promise = require('bluebird'); 5 | 6 | const resolveData = ({data:{response}}) => { 7 | return Promise.resolve(response); 8 | }; 9 | 10 | class Client extends EventEmitter { 11 | constructor() { 12 | super(); 13 | this.hangupsProc = null; 14 | } 15 | connect() { 16 | this.hangupsProc = require("child_process").spawn('python3', ['-u', 'hangups_client.py']); 17 | 18 | this.hangupsProc.stderr.on("data", (str) => { 19 | this.emit('status', str.toString()); 20 | debugVerbose("status message from hangups process:", str.toString()); 21 | }); 22 | 23 | this.hangupsProc.stdout.on("data", (str) => { 24 | debugVerbose("got message from hangups before JSON.parse():", str.toString()); 25 | // XXX:NOTE: See https://github.com/matrix-hacks/matrix-puppet-hangouts/pull/24 for rationale 26 | try { 27 | var data = JSON.parse(str); 28 | debugVerbose("emitting message", data); 29 | this.emit('message', data); 30 | } catch(error) { 31 | debugVerbose("ERROR: incorrect JSON format: ", str.toString()); 32 | } 33 | }); 34 | 35 | console.log('started hangups child'); 36 | } 37 | 38 | send(id, msg) { 39 | let themsg = { 'cmd': "sendmessage", 'conversation_id':id, 'msgbody': msg }; 40 | debugVerbose('sending message to hangouts subprocess: ', JSON.stringify(themsg)); 41 | this.hangupsProc.stdin.write(JSON.stringify(themsg) + "\n"); 42 | return Promise.resolve(); 43 | } 44 | 45 | sendImage(id, data) { 46 | let themsg = { 'cmd': "sendimage", 'conversation_id':id, 'msgbody': data.text, 'url': data.url, 'mimetype': data.mimetype }; 47 | debugVerbose('sending message to hangouts subprocess: ', JSON.stringify(themsg)); 48 | this.hangupsProc.stdin.write(JSON.stringify(themsg) + "\n"); 49 | return Promise.resolve(); 50 | } 51 | } 52 | 53 | module.exports = Client; 54 | -------------------------------------------------------------------------------- /config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "registrationPath": "hangouts-registration.yaml", 3 | "port": 8090, 4 | "bridge": { 5 | "homeserverUrl": "https://your-home-server.example.org", 6 | "domain": "example.org", 7 | "registration": "hangouts-registration.yaml" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /hangups_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Mostly adopted from the hangups example files. 4 | 5 | from inspect import getmembers 6 | from pprint import pprint 7 | import hangups 8 | from hangups.ui.utils import get_conv_name 9 | 10 | import sys 11 | import json 12 | import queue 13 | import time 14 | 15 | from asyncio.streams import StreamWriter, FlowControlMixin 16 | import argparse 17 | import asyncio 18 | import logging 19 | import os 20 | import aiohttp 21 | from types import SimpleNamespace 22 | 23 | from hangups.auth import CredentialsPrompt, RefreshTokenCache, get_auth 24 | import appdirs 25 | 26 | MIME_EXT = { 27 | 'image/gif': '.gif', 28 | 'image/jpeg': '.jpg', 29 | 'image/png': '.png', 30 | 'image/svg+xml': '.svg', 31 | 'image/tiff': '.tiff', 32 | 'image/webp': '.webp', 33 | 'image/x-icon': '.ico', 34 | } 35 | 36 | global cachedStdout 37 | 38 | def print_jsonmsg(*args, **kwargs): 39 | global cachedStdout 40 | print(*args, file=cachedStdout, **kwargs) 41 | 42 | class NullCredentialsPrompt(object): 43 | @staticmethod 44 | def get_email(): 45 | return '' 46 | 47 | @staticmethod 48 | def get_password(): 49 | return '' 50 | 51 | @staticmethod 52 | def get_verification_code(): 53 | return '' 54 | 55 | def run_example(example_coroutine, *extra_args): 56 | """Run a hangups example coroutine. 57 | 58 | Args: 59 | example_coroutine (coroutine): Coroutine to run with a connected 60 | hangups client and arguments namespace as arguments. 61 | extra_args (str): Any extra command line arguments required by the 62 | example. 63 | """ 64 | args = _get_parser().parse_args() 65 | logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING) 66 | 67 | if args.login_and_save_token: 68 | cookies = hangups.auth.get_auth_stdin(args.token_path) 69 | #pprint(getmembers(args)) 70 | return 71 | else: 72 | # Obtain hangups authentication cookies, prompting for credentials from 73 | # standard input if necessary. 74 | refresh_token_cache = RefreshTokenCache(args.token_path) 75 | try: 76 | cookies = get_auth(NullCredentialsPrompt(), refresh_token_cache) 77 | except: 78 | print("Hangouts login failed. Either you didn't log in yet, or your refresh token expired.\nPlease log in with --login-and-save-token") 79 | return 80 | 81 | while 1: 82 | print("Attempting main loop...") 83 | client = hangups.Client(cookies, max_retries=float('inf'), retry_backoff_base=1.2) 84 | task = asyncio.ensure_future(_async_main(example_coroutine, client, args)) 85 | loop = asyncio.get_event_loop() 86 | 87 | try: 88 | loop.run_until_complete(task) 89 | except KeyboardInterrupt: 90 | task.cancel() 91 | loop.run_forever() 92 | except: 93 | pass 94 | finally: 95 | time.sleep(5) 96 | 97 | try: 98 | loop.run_until_complete(task) 99 | except KeyboardInterrupt: 100 | task.cancel() 101 | loop.run_forever() 102 | finally: 103 | loop.close() 104 | 105 | 106 | def _get_parser(): 107 | """Return ArgumentParser with any extra arguments.""" 108 | parser = argparse.ArgumentParser( 109 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 110 | ) 111 | dirs = appdirs.AppDirs('hangups', 'hangups') 112 | default_token_path = os.path.join(dirs.user_cache_dir, 'refresh_token.txt') 113 | parser.add_argument( 114 | '--token-path', default=default_token_path, 115 | help='path used to store OAuth refresh token' 116 | ) 117 | parser.add_argument( 118 | '--login-and-save-token', action='store_true', 119 | help='Perform login and save refresh token' 120 | ) 121 | parser.add_argument( 122 | '-d', '--debug', action='store_true', 123 | help='log detailed debugging messages' 124 | ) 125 | return parser 126 | 127 | @asyncio.coroutine 128 | def _async_main(example_coroutine, client, args): 129 | """Run the example coroutine.""" 130 | # Spawn a task for hangups to run in parallel with the example coroutine. 131 | task = asyncio.ensure_future(client.connect()) 132 | 133 | # Wait for hangups to either finish connecting or raise an exception. 134 | on_connect = asyncio.Future() 135 | client.on_connect.add_observer(lambda: on_connect.set_result(None)) 136 | done, _ = yield from asyncio.wait( 137 | (on_connect, task), return_when=asyncio.FIRST_COMPLETED 138 | ) 139 | yield from asyncio.gather(*done) 140 | 141 | # Run the example coroutine. Afterwards, disconnect hangups gracefully and 142 | # yield the hangups task to handle any exceptions. 143 | try: 144 | yield from example_coroutine(client, args) 145 | finally: 146 | yield from client.disconnect() 147 | yield from task 148 | 149 | reader, writer = None, None 150 | 151 | @asyncio.coroutine 152 | def stdio(loop=None): 153 | if loop is None: 154 | loop = asyncio.get_event_loop() 155 | 156 | reader = asyncio.StreamReader() 157 | reader_protocol = asyncio.StreamReaderProtocol(reader) 158 | 159 | writer_transport, writer_protocol = yield from loop.connect_write_pipe(FlowControlMixin, os.fdopen(0, 'wb')) 160 | writer = StreamWriter(writer_transport, writer_protocol, None, loop) 161 | 162 | yield from loop.connect_read_pipe(lambda: reader_protocol, sys.stdin) 163 | 164 | return reader, writer 165 | 166 | @asyncio.coroutine 167 | def async_input(): 168 | global reader, writer 169 | if (reader, writer) == (None, None): 170 | reader, writer = yield from stdio() 171 | 172 | line = yield from reader.readline() 173 | return line.decode('utf8').strip() 174 | 175 | global event_queue 176 | event_queue = queue.Queue() 177 | global user_list, conv_list 178 | 179 | def on_event(conv_event): 180 | global conv_list, event_queue 181 | #pprint(getmembers(conv_event)) 182 | if isinstance(conv_event, hangups.ChatMessageEvent): 183 | conv = conv_list.get(conv_event.conversation_id) 184 | user = conv.get_user(conv_event.user_id) 185 | 186 | try: 187 | msgJson = json.dumps({ 188 | 'status':'success', 189 | 'type':'message', 190 | 'content':conv_event.text, 191 | 'attachments': conv_event.attachments, 192 | 'conversation_id': conv.id_, 193 | 'conversation_name':get_conv_name(conv), 194 | 'photo_url':user.photo_url, 195 | 'user':user.full_name, 196 | 'self_user_id':user_list._self_user.id_.chat_id, 197 | 'user_id':{'chat_id':conv_event.user_id.chat_id, 'gaia_id':conv_event.user_id.gaia_id} 198 | }) 199 | print_jsonmsg(msgJson) 200 | 201 | except Exception as error: 202 | print(repr(error)) 203 | 204 | 205 | #event_queue.put(msgJson) 206 | #else: 207 | #print(conv_event.user_id) 208 | 209 | def _on_message_sent(future): 210 | """Handle showing an error if a message fails to send.""" 211 | try: 212 | future.result() 213 | except Exception as e: 214 | # TODO: Properly notify the bridge that a message send failure has 215 | # occurred, so it can react in some way (e.g. alert the owner with a 216 | # message). 217 | print("Message send failure! Exception: %s" % str(e)) 218 | # TODO: Properly notify the bridge that a message has successfully sent so 219 | # it can react in some way (e.g. set a read receipt on the message showing 220 | # that the bot read the message) 221 | #print_jsonmsg(json.dumps({"status":"success"})) 222 | 223 | async def download_image(url): 224 | async with aiohttp.ClientSession() as session: 225 | async with session.get(url) as resp: 226 | if resp.status == 200: 227 | data = await resp.read() 228 | return SimpleNamespace(name=url, read=lambda: data) 229 | 230 | @asyncio.coroutine 231 | def listen_events(client, _): 232 | global user_list, conv_list 233 | global event_queue 234 | 235 | user_list, conv_list = ( 236 | yield from hangups.build_user_conversation_list(client) 237 | ) 238 | #print("my user id"); 239 | #print(user_list._self_user.id_); 240 | conv_list.on_event.add_observer(on_event) 241 | 242 | #print(client._client_id); 243 | #print(client._email); 244 | #hangups.client. 245 | #print(json.dumps({"cmd":"status", "status":"ready"})) 246 | #print(hangups.user.) 247 | 248 | while 1: 249 | try: 250 | msgtxt = yield from async_input() 251 | msgtxt = msgtxt.strip() 252 | 253 | # TOOD: take conversation id from other side 254 | msgJson = json.loads(msgtxt) 255 | conv = conv_list.get(msgJson['conversation_id']) 256 | #print("ok, I got: " + msgtxt) 257 | 258 | if msgJson['cmd'] == 'sendmessage': 259 | segments = hangups.ChatMessageSegment.from_str(msgJson['msgbody']) 260 | asyncio.ensure_future( 261 | conv.send_message(segments) 262 | ).add_done_callback(_on_message_sent) 263 | elif msgJson['cmd'] == 'sendimage': 264 | segments = [] 265 | image_file = yield from download_image(msgJson['url']) 266 | # deduplicate 267 | image_file.name += '_mx_' + MIME_EXT.get(msgJson['mimetype'], '.unk') 268 | if not image_file: 269 | raise Exception("Invalid image url") 270 | asyncio.ensure_future( 271 | conv.send_message(segments, image_file=image_file) 272 | ).add_done_callback(_on_message_sent) 273 | else: 274 | raise Exception("Invalid cmd specified!") 275 | 276 | except Exception as error: 277 | print(repr(error)) 278 | 279 | if __name__ == '__main__': 280 | cachedStdout = sys.stdout 281 | sys.stdout = sys.stderr 282 | run_example(listen_events) 283 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { 2 | MatrixAppServiceBridge: { 3 | Cli, AppServiceRegistration 4 | }, 5 | Puppet, 6 | MatrixPuppetBridgeBase 7 | } = require("matrix-puppet-bridge"); 8 | const HangoutsClient = require('./client'); 9 | const config = require('./config.json'); 10 | const path = require('path'); 11 | const puppet = new Puppet(path.join(__dirname, './config.json' )); 12 | const debug = require('debug'); 13 | const debugVerbose = debug('verbose:matrix-puppet:hangouts:index'); 14 | 15 | class App extends MatrixPuppetBridgeBase { 16 | getServicePrefix() { 17 | return "hangouts"; 18 | } 19 | getServiceName() { 20 | return "Hangouts"; 21 | } 22 | defaultDeduplicationTag() { 23 | return " \u200b"; // Unicode Character 'ZERO WIDTH SPACE' 24 | } 25 | defaultDeduplicationTagPattern() { 26 | return "\\u200b$"; // Unicode Character 'ZERO WIDTH SPACE' 27 | } 28 | initThirdPartyClient() { 29 | // We used to call this here, but can't do so anymore because the bridge isn't ready yet at this point. 30 | // Fixes error: "Cannot read property 'getClient' of null" 31 | //this.sendStatusMsg({fixedWidthOutput:false}, "Starting hangouts bridge!"); 32 | 33 | this.threadInfo = {}; 34 | this.userId = null; 35 | this.thirdPartyClient = new HangoutsClient(); 36 | this.thirdPartyClient.connect(); 37 | 38 | this.thirdPartyClient.on('status', (statusTxt)=> { 39 | this.sendStatusMsg({}, statusTxt); 40 | }); 41 | 42 | this.thirdPartyClient.on('message', (data)=> { 43 | if(data && data.type === 'message') 44 | { 45 | try { 46 | this.threadInfo[data.conversation_id] = { 47 | conversation_name: data.conversation_name, 48 | }; 49 | debugVerbose("incoming message data:", data); 50 | const isMe = data.user_id.chat_id === data.self_user_id; 51 | // normal message 52 | if (!data.attachments.length) { 53 | const payload = { 54 | roomId: data.conversation_id, 55 | senderName: data.user, 56 | senderId: isMe ? undefined : data.user_id.chat_id, 57 | text: data.content, 58 | avatarUrl: data.photo_url, 59 | }; 60 | return this.handleThirdPartyRoomMessage(payload).catch(err => { 61 | console.log("handleThirdPartyRoomMessage error", err); 62 | sendStatusMsg({}, "handleThirdPartyRoomMessage error", err); 63 | }); 64 | } 65 | // image message 66 | return Promise.all(data.attachments.map((attachment)=> { 67 | const payload = { 68 | roomId: data.conversation_id, 69 | senderName: data.user, 70 | senderId: isMe ? undefined : data.user_id.chat_id, 71 | text: data.content, 72 | url: attachment, 73 | avatarUrl: data.photo_url, 74 | }; 75 | return this.handleThirdPartyRoomImageMessage(payload).catch(err => { 76 | console.log("handleThirdPartyRoomMessage error", err); 77 | sendStatusMsg({}, "handleThirdPartyRoomMessage error", err); 78 | }); 79 | })); 80 | } catch(er) { 81 | console.log("incoming message handling error:", er); 82 | sendStatusMsg({}, "incoming message handling error:", err); 83 | } 84 | } 85 | 86 | // Message data format: 87 | /*{ user_id: 88 | { chat_id: '10xxxxxxxxxxxxxxxxxxx', 89 | gaia_id: '10xxxxxxxxxxxxxxxxxxx' }, 90 | conversation_id: 'Ugxxxxxxxxxxxxxxxxxxxxxxxx', 91 | conversation_name: '+1nnnnnnnnnn', 92 | user: 'John Doe', 93 | content: 'a message!', 94 | type: 'message', 95 | status: 'success' }*/ 96 | }); 97 | 98 | return this.thirdPartyClient; 99 | } 100 | getThirdPartyRoomDataById(id) { 101 | let roomData = { 102 | name: this.threadInfo[id].conversation_name, 103 | topic: "Hangouts Chat" 104 | }; 105 | return roomData; 106 | } 107 | sendReadReceiptAsPuppetToThirdPartyRoomWithId() { 108 | // not available for now 109 | } 110 | sendMessageAsPuppetToThirdPartyRoomWithId(id, text) { 111 | return this.thirdPartyClient.send(id, text); 112 | } 113 | sendImageMessageAsPuppetToThirdPartyRoomWithId(id, data) { 114 | return this.thirdPartyClient.sendImage(id, data); 115 | } 116 | } 117 | 118 | new Cli({ 119 | port: config.port, 120 | registrationPath: config.registrationPath, 121 | generateRegistration: function(reg, callback) { 122 | puppet.associate().then(()=>{ 123 | reg.setId(AppServiceRegistration.generateToken()); 124 | reg.setHomeserverToken(AppServiceRegistration.generateToken()); 125 | reg.setAppServiceToken(AppServiceRegistration.generateToken()); 126 | reg.setSenderLocalpart("hangoutsbot"); 127 | reg.addRegexPattern("users", "@hangouts_.*", true); 128 | reg.addRegexPattern("aliases", "#hangouts_.*", true); 129 | callback(reg); 130 | }).catch(err=>{ 131 | console.error(err.message); 132 | process.exit(-1); 133 | }); 134 | }, 135 | run: function(port) { 136 | const app = new App(config, puppet); 137 | return puppet.startClient().then(()=>{ 138 | return app.initThirdPartyClient(); 139 | }).then(() => { 140 | return app.bridge.run(port, config); 141 | }).then(()=>{ 142 | console.log('Matrix-side listening on port %s', port); 143 | }).catch(err=>{ 144 | console.error(err.message); 145 | process.exit(-1); 146 | }); 147 | } 148 | }).run(); 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrix-puppet-hangouts", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "DEBUG=*matrix-puppet:* node index.js", 8 | "test": "echo \"no tests at this time\"; exit 1" 9 | }, 10 | "keywords": [ 11 | "matrix", 12 | "hangouts" 13 | ], 14 | "contributors": [ 15 | "Keyvan Fatehi", 16 | "Andrew Johnson" 17 | ], 18 | "license": "ISC", 19 | "dependencies": { 20 | "bluebird": "^3.4.7", 21 | "matrix-puppet-bridge": "matrix-hacks/matrix-puppet-bridge#c16ac9f" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^3.12.2" 25 | }, 26 | "eslintConfig": { 27 | "env": { 28 | "es6": true, 29 | "node": true 30 | }, 31 | "extends": "eslint:recommended", 32 | "rules": { 33 | "no-console": 0, 34 | "indent": [ 35 | "error", 36 | 2, 37 | { 38 | "SwitchCase": 1 39 | } 40 | ], 41 | "linebreak-style": [ 42 | "error", 43 | "unix" 44 | ], 45 | "semi": [ 46 | "error", 47 | "always" 48 | ], 49 | "no-unused-vars": [ 50 | "error", 51 | { 52 | "argsIgnorePattern": "^_" 53 | } 54 | ] 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------