1#!/usr/bin/env python 2 3# Copyright 2014-2016 OpenMarket Ltd 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" Starts a synapse client console. """ 18import argparse 19import cmd 20import getpass 21import json 22import shlex 23import sys 24import time 25import urllib 26from http import TwistedHttpClient 27from typing import Optional 28 29import nacl.encoding 30import nacl.signing 31import urlparse 32from signedjson.sign import SignatureVerifyException, verify_signed_json 33 34from twisted.internet import defer, reactor, threads 35 36CONFIG_JSON = "cmdclient_config.json" 37 38# TODO: The concept of trusted identity servers has been deprecated. This option and checks 39# should be removed 40TRUSTED_ID_SERVERS = ["localhost:8001"] 41 42 43class SynapseCmd(cmd.Cmd): 44 45 """Basic synapse command-line processor. 46 47 This processes commands from the user and calls the relevant HTTP methods. 48 """ 49 50 def __init__(self, http_client, server_url, identity_server_url, username, token): 51 cmd.Cmd.__init__(self) 52 self.http_client = http_client 53 self.http_client.verbose = True 54 self.config = { 55 "url": server_url, 56 "identityServerUrl": identity_server_url, 57 "user": username, 58 "token": token, 59 "verbose": "on", 60 "complete_usernames": "on", 61 "send_delivery_receipts": "on", 62 } 63 self.path_prefix = "/_matrix/client/api/v1" 64 self.event_stream_token = "END" 65 self.prompt = ">>> " 66 67 def do_EOF(self, line): # allows CTRL+D quitting 68 return True 69 70 def emptyline(self): 71 pass # else it repeats the previous command 72 73 def _usr(self): 74 return self.config["user"] 75 76 def _tok(self): 77 return self.config["token"] 78 79 def _url(self): 80 return self.config["url"] + self.path_prefix 81 82 def _identityServerUrl(self): 83 return self.config["identityServerUrl"] 84 85 def _is_on(self, config_name): 86 if config_name in self.config: 87 return self.config[config_name] == "on" 88 return False 89 90 def _domain(self): 91 if "user" not in self.config or not self.config["user"]: 92 return None 93 return self.config["user"].split(":")[1] 94 95 def do_config(self, line): 96 """Show the config for this client: "config" 97 Edit a key value mapping: "config key value" e.g. "config token 1234" 98 Config variables: 99 user: The username to auth with. 100 token: The access token to auth with. 101 url: The url of the server. 102 verbose: [on|off] The verbosity of requests/responses. 103 complete_usernames: [on|off] Auto complete partial usernames by 104 assuming they are on the same homeserver as you. 105 E.g. name >> @name:yourhost 106 send_delivery_receipts: [on|off] Automatically send receipts to 107 messages when performing a 'stream' command. 108 Additional key/values can be added and can be substituted into requests 109 by using $. E.g. 'config roomid room1' then 'raw get /rooms/$roomid'. 110 """ 111 if len(line) == 0: 112 print(json.dumps(self.config, indent=4)) 113 return 114 115 try: 116 args = self._parse(line, ["key", "val"], force_keys=True) 117 118 # make sure restricted config values are checked 119 config_rules = [ # key, valid_values 120 ("verbose", ["on", "off"]), 121 ("complete_usernames", ["on", "off"]), 122 ("send_delivery_receipts", ["on", "off"]), 123 ] 124 for key, valid_vals in config_rules: 125 if key == args["key"] and args["val"] not in valid_vals: 126 print("%s value must be one of %s" % (args["key"], valid_vals)) 127 return 128 129 # toggle the http client verbosity 130 if args["key"] == "verbose": 131 self.http_client.verbose = "on" == args["val"] 132 133 # assign the new config 134 self.config[args["key"]] = args["val"] 135 print(json.dumps(self.config, indent=4)) 136 137 save_config(self.config) 138 except Exception as e: 139 print(e) 140 141 def do_register(self, line): 142 """Registers for a new account: "register <userid> <noupdate>" 143 <userid> : The desired user ID 144 <noupdate> : Do not automatically clobber config values. 145 """ 146 args = self._parse(line, ["userid", "noupdate"]) 147 148 password = None 149 pwd = None 150 pwd2 = "_" 151 while pwd != pwd2: 152 pwd = getpass.getpass("Type a password for this user: ") 153 pwd2 = getpass.getpass("Retype the password: ") 154 if pwd != pwd2 or len(pwd) == 0: 155 print("Password mismatch.") 156 pwd = None 157 else: 158 password = pwd 159 160 body = {"type": "m.login.password"} 161 if "userid" in args: 162 body["user"] = args["userid"] 163 if password: 164 body["password"] = password 165 166 reactor.callFromThread(self._do_register, body, "noupdate" not in args) 167 168 @defer.inlineCallbacks 169 def _do_register(self, data, update_config): 170 # check the registration flows 171 url = self._url() + "/register" 172 json_res = yield self.http_client.do_request("GET", url) 173 print(json.dumps(json_res, indent=4)) 174 175 passwordFlow = None 176 for flow in json_res["flows"]: 177 if flow["type"] == "m.login.recaptcha" or ( 178 "stages" in flow and "m.login.recaptcha" in flow["stages"] 179 ): 180 print("Unable to register: Home server requires captcha.") 181 return 182 if flow["type"] == "m.login.password" and "stages" not in flow: 183 passwordFlow = flow 184 break 185 186 if not passwordFlow: 187 return 188 189 json_res = yield self.http_client.do_request("POST", url, data=data) 190 print(json.dumps(json_res, indent=4)) 191 if update_config and "user_id" in json_res: 192 self.config["user"] = json_res["user_id"] 193 self.config["token"] = json_res["access_token"] 194 save_config(self.config) 195 196 def do_login(self, line): 197 """Login as a specific user: "login @bob:localhost" 198 You MAY be prompted for a password, or instructed to visit a URL. 199 """ 200 try: 201 args = self._parse(line, ["user_id"], force_keys=True) 202 can_login = threads.blockingCallFromThread(reactor, self._check_can_login) 203 if can_login: 204 p = getpass.getpass("Enter your password: ") 205 user = args["user_id"] 206 if self._is_on("complete_usernames") and not user.startswith("@"): 207 domain = self._domain() 208 if domain: 209 user = "@" + user + ":" + domain 210 211 reactor.callFromThread(self._do_login, user, p) 212 # print " got %s " % p 213 except Exception as e: 214 print(e) 215 216 @defer.inlineCallbacks 217 def _do_login(self, user, password): 218 path = "/login" 219 data = {"user": user, "password": password, "type": "m.login.password"} 220 url = self._url() + path 221 json_res = yield self.http_client.do_request("POST", url, data=data) 222 print(json_res) 223 224 if "access_token" in json_res: 225 self.config["user"] = user 226 self.config["token"] = json_res["access_token"] 227 save_config(self.config) 228 print("Login successful.") 229 230 @defer.inlineCallbacks 231 def _check_can_login(self): 232 path = "/login" 233 # ALWAYS check that the home server can handle the login request before 234 # submitting! 235 url = self._url() + path 236 json_res = yield self.http_client.do_request("GET", url) 237 print(json_res) 238 239 if "flows" not in json_res: 240 print("Failed to find any login flows.") 241 defer.returnValue(False) 242 243 flow = json_res["flows"][0] # assume first is the one we want. 244 if "type" not in flow or "m.login.password" != flow["type"] or "stages" in flow: 245 fallback_url = self._url() + "/login/fallback" 246 print( 247 "Unable to login via the command line client. Please visit " 248 "%s to login." % fallback_url 249 ) 250 defer.returnValue(False) 251 defer.returnValue(True) 252 253 def do_emailrequest(self, line): 254 """Requests the association of a third party identifier 255 <address> The email address) 256 <clientSecret> A string of characters generated when requesting an email that you'll supply in subsequent calls to identify yourself 257 <sendAttempt> The number of times the user has requested an email. Leave this the same between requests to retry the request at the transport level. Increment it to request that the email be sent again. 258 """ 259 args = self._parse(line, ["address", "clientSecret", "sendAttempt"]) 260 261 postArgs = { 262 "email": args["address"], 263 "clientSecret": args["clientSecret"], 264 "sendAttempt": args["sendAttempt"], 265 } 266 267 reactor.callFromThread(self._do_emailrequest, postArgs) 268 269 @defer.inlineCallbacks 270 def _do_emailrequest(self, args): 271 # TODO: Update to use v2 Identity Service API endpoint 272 url = ( 273 self._identityServerUrl() 274 + "/_matrix/identity/api/v1/validate/email/requestToken" 275 ) 276 277 json_res = yield self.http_client.do_request( 278 "POST", 279 url, 280 data=urllib.urlencode(args), 281 jsonreq=False, 282 headers={"Content-Type": ["application/x-www-form-urlencoded"]}, 283 ) 284 print(json_res) 285 if "sid" in json_res: 286 print("Token sent. Your session ID is %s" % (json_res["sid"])) 287 288 def do_emailvalidate(self, line): 289 """Validate and associate a third party ID 290 <sid> The session ID (sid) given to you in the response to requestToken 291 <token> The token sent to your third party identifier address 292 <clientSecret> The same clientSecret you supplied in requestToken 293 """ 294 args = self._parse(line, ["sid", "token", "clientSecret"]) 295 296 postArgs = { 297 "sid": args["sid"], 298 "token": args["token"], 299 "clientSecret": args["clientSecret"], 300 } 301 302 reactor.callFromThread(self._do_emailvalidate, postArgs) 303 304 @defer.inlineCallbacks 305 def _do_emailvalidate(self, args): 306 # TODO: Update to use v2 Identity Service API endpoint 307 url = ( 308 self._identityServerUrl() 309 + "/_matrix/identity/api/v1/validate/email/submitToken" 310 ) 311 312 json_res = yield self.http_client.do_request( 313 "POST", 314 url, 315 data=urllib.urlencode(args), 316 jsonreq=False, 317 headers={"Content-Type": ["application/x-www-form-urlencoded"]}, 318 ) 319 print(json_res) 320 321 def do_3pidbind(self, line): 322 """Validate and associate a third party ID 323 <sid> The session ID (sid) given to you in the response to requestToken 324 <clientSecret> The same clientSecret you supplied in requestToken 325 """ 326 args = self._parse(line, ["sid", "clientSecret"]) 327 328 postArgs = {"sid": args["sid"], "clientSecret": args["clientSecret"]} 329 postArgs["mxid"] = self.config["user"] 330 331 reactor.callFromThread(self._do_3pidbind, postArgs) 332 333 @defer.inlineCallbacks 334 def _do_3pidbind(self, args): 335 # TODO: Update to use v2 Identity Service API endpoint 336 url = self._identityServerUrl() + "/_matrix/identity/api/v1/3pid/bind" 337 338 json_res = yield self.http_client.do_request( 339 "POST", 340 url, 341 data=urllib.urlencode(args), 342 jsonreq=False, 343 headers={"Content-Type": ["application/x-www-form-urlencoded"]}, 344 ) 345 print(json_res) 346 347 def do_join(self, line): 348 """Joins a room: "join <roomid>" """ 349 try: 350 args = self._parse(line, ["roomid"], force_keys=True) 351 self._do_membership_change(args["roomid"], "join", self._usr()) 352 except Exception as e: 353 print(e) 354 355 def do_joinalias(self, line): 356 try: 357 args = self._parse(line, ["roomname"], force_keys=True) 358 path = "/join/%s" % urllib.quote(args["roomname"]) 359 reactor.callFromThread(self._run_and_pprint, "POST", path, {}) 360 except Exception as e: 361 print(e) 362 363 def do_topic(self, line): 364 """ "topic [set|get] <roomid> [<newtopic>]" 365 Set the topic for a room: topic set <roomid> <newtopic> 366 Get the topic for a room: topic get <roomid> 367 """ 368 try: 369 args = self._parse(line, ["action", "roomid", "topic"]) 370 if "action" not in args or "roomid" not in args: 371 print("Must specify set|get and a room ID.") 372 return 373 if args["action"].lower() not in ["set", "get"]: 374 print("Must specify set|get, not %s" % args["action"]) 375 return 376 377 path = "/rooms/%s/topic" % urllib.quote(args["roomid"]) 378 379 if args["action"].lower() == "set": 380 if "topic" not in args: 381 print("Must specify a new topic.") 382 return 383 body = {"topic": args["topic"]} 384 reactor.callFromThread(self._run_and_pprint, "PUT", path, body) 385 elif args["action"].lower() == "get": 386 reactor.callFromThread(self._run_and_pprint, "GET", path) 387 except Exception as e: 388 print(e) 389 390 def do_invite(self, line): 391 """Invite a user to a room: "invite <userid> <roomid>" """ 392 try: 393 args = self._parse(line, ["userid", "roomid"], force_keys=True) 394 395 user_id = args["userid"] 396 397 reactor.callFromThread(self._do_invite, args["roomid"], user_id) 398 except Exception as e: 399 print(e) 400 401 @defer.inlineCallbacks 402 def _do_invite(self, roomid, userstring): 403 if not userstring.startswith("@") and self._is_on("complete_usernames"): 404 # TODO: Update to use v2 Identity Service API endpoint 405 url = self._identityServerUrl() + "/_matrix/identity/api/v1/lookup" 406 407 json_res = yield self.http_client.do_request( 408 "GET", url, qparams={"medium": "email", "address": userstring} 409 ) 410 411 mxid = None 412 413 if "mxid" in json_res and "signatures" in json_res: 414 # TODO: Update to use v2 Identity Service API endpoint 415 url = ( 416 self._identityServerUrl() 417 + "/_matrix/identity/api/v1/pubkey/ed25519" 418 ) 419 420 pubKey = None 421 pubKeyObj = yield self.http_client.do_request("GET", url) 422 if "public_key" in pubKeyObj: 423 pubKey = nacl.signing.VerifyKey( 424 pubKeyObj["public_key"], encoder=nacl.encoding.HexEncoder 425 ) 426 else: 427 print("No public key found in pubkey response!") 428 429 sigValid = False 430 431 if pubKey: 432 for signame in json_res["signatures"]: 433 if signame not in TRUSTED_ID_SERVERS: 434 print( 435 "Ignoring signature from untrusted server %s" 436 % (signame) 437 ) 438 else: 439 try: 440 verify_signed_json(json_res, signame, pubKey) 441 sigValid = True 442 print( 443 "Mapping %s -> %s correctly signed by %s" 444 % (userstring, json_res["mxid"], signame) 445 ) 446 break 447 except SignatureVerifyException as e: 448 print("Invalid signature from %s" % (signame)) 449 print(e) 450 451 if sigValid: 452 print("Resolved 3pid %s to %s" % (userstring, json_res["mxid"])) 453 mxid = json_res["mxid"] 454 else: 455 print( 456 "Got association for %s but couldn't verify signature" 457 % (userstring) 458 ) 459 460 if not mxid: 461 mxid = "@" + userstring + ":" + self._domain() 462 463 self._do_membership_change(roomid, "invite", mxid) 464 465 def do_leave(self, line): 466 """Leaves a room: "leave <roomid>" """ 467 try: 468 args = self._parse(line, ["roomid"], force_keys=True) 469 self._do_membership_change(args["roomid"], "leave", self._usr()) 470 except Exception as e: 471 print(e) 472 473 def do_send(self, line): 474 """Sends a message. "send <roomid> <body>" """ 475 args = self._parse(line, ["roomid", "body"]) 476 txn_id = "txn%s" % int(time.time()) 477 path = "/rooms/%s/send/m.room.message/%s" % ( 478 urllib.quote(args["roomid"]), 479 txn_id, 480 ) 481 body_json = {"msgtype": "m.text", "body": args["body"]} 482 reactor.callFromThread(self._run_and_pprint, "PUT", path, body_json) 483 484 def do_list(self, line): 485 """List data about a room. 486 "list members <roomid> [query]" - List all the members in this room. 487 "list messages <roomid> [query]" - List all the messages in this room. 488 489 Where [query] will be directly applied as query parameters, allowing 490 you to use the pagination API. E.g. the last 3 messages in this room: 491 "list messages <roomid> from=END&to=START&limit=3" 492 """ 493 args = self._parse(line, ["type", "roomid", "qp"]) 494 if "type" not in args or "roomid" not in args: 495 print("Must specify type and room ID.") 496 return 497 if args["type"] not in ["members", "messages"]: 498 print("Unrecognised type: %s" % args["type"]) 499 return 500 room_id = args["roomid"] 501 path = "/rooms/%s/%s" % (urllib.quote(room_id), args["type"]) 502 503 qp = {"access_token": self._tok()} 504 if "qp" in args: 505 for key_value_str in args["qp"].split("&"): 506 try: 507 key_value = key_value_str.split("=") 508 qp[key_value[0]] = key_value[1] 509 except Exception: 510 print("Bad query param: %s" % key_value) 511 return 512 513 reactor.callFromThread(self._run_and_pprint, "GET", path, query_params=qp) 514 515 def do_create(self, line): 516 """Creates a room. 517 "create [public|private] <roomname>" - Create a room <roomname> with the 518 specified visibility. 519 "create <roomname>" - Create a room <roomname> with default visibility. 520 "create [public|private]" - Create a room with specified visibility. 521 "create" - Create a room with default visibility. 522 """ 523 args = self._parse(line, ["vis", "roomname"]) 524 # fixup args depending on which were set 525 body = {} 526 if "vis" in args and args["vis"] in ["public", "private"]: 527 body["visibility"] = args["vis"] 528 529 if "roomname" in args: 530 room_name = args["roomname"] 531 body["room_alias_name"] = room_name 532 elif "vis" in args and args["vis"] not in ["public", "private"]: 533 room_name = args["vis"] 534 body["room_alias_name"] = room_name 535 536 reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body) 537 538 def do_raw(self, line): 539 """Directly send a JSON object: "raw <method> <path> <data> <notoken>" 540 <method>: Required. One of "PUT", "GET", "POST", "xPUT", "xGET", 541 "xPOST". Methods with 'x' prefixed will not automatically append the 542 access token. 543 <path>: Required. E.g. "/events" 544 <data>: Optional. E.g. "{ "msgtype":"custom.text", "body":"abc123"}" 545 """ 546 args = self._parse(line, ["method", "path", "data"]) 547 # sanity check 548 if "method" not in args or "path" not in args: 549 print("Must specify path and method.") 550 return 551 552 args["method"] = args["method"].upper() 553 valid_methods = [ 554 "PUT", 555 "GET", 556 "POST", 557 "DELETE", 558 "XPUT", 559 "XGET", 560 "XPOST", 561 "XDELETE", 562 ] 563 if args["method"] not in valid_methods: 564 print("Unsupported method: %s" % args["method"]) 565 return 566 567 if "data" not in args: 568 args["data"] = None 569 else: 570 try: 571 args["data"] = json.loads(args["data"]) 572 except Exception as e: 573 print("Data is not valid JSON. %s" % e) 574 return 575 576 qp = {"access_token": self._tok()} 577 if args["method"].startswith("X"): 578 qp = {} # remove access token 579 args["method"] = args["method"][1:] # snip the X 580 else: 581 # append any query params the user has set 582 try: 583 parsed_url = urlparse.urlparse(args["path"]) 584 qp.update(urlparse.parse_qs(parsed_url.query)) 585 args["path"] = parsed_url.path 586 except Exception: 587 pass 588 589 reactor.callFromThread( 590 self._run_and_pprint, 591 args["method"], 592 args["path"], 593 args["data"], 594 query_params=qp, 595 ) 596 597 def do_stream(self, line): 598 """Stream data from the server: "stream <longpoll timeout ms>" """ 599 args = self._parse(line, ["timeout"]) 600 timeout = 5000 601 if "timeout" in args: 602 try: 603 timeout = int(args["timeout"]) 604 except ValueError: 605 print("Timeout must be in milliseconds.") 606 return 607 reactor.callFromThread(self._do_event_stream, timeout) 608 609 @defer.inlineCallbacks 610 def _do_event_stream(self, timeout): 611 res = yield defer.ensureDeferred( 612 self.http_client.get_json( 613 self._url() + "/events", 614 { 615 "access_token": self._tok(), 616 "timeout": str(timeout), 617 "from": self.event_stream_token, 618 }, 619 ) 620 ) 621 print(json.dumps(res, indent=4)) 622 623 if "chunk" in res: 624 for event in res["chunk"]: 625 if ( 626 event["type"] == "m.room.message" 627 and self._is_on("send_delivery_receipts") 628 and event["user_id"] != self._usr() 629 ): # not sent by us 630 self._send_receipt(event, "d") 631 632 # update the position in the stram 633 if "end" in res: 634 self.event_stream_token = res["end"] 635 636 def _send_receipt(self, event, feedback_type): 637 path = "/rooms/%s/messages/%s/%s/feedback/%s/%s" % ( 638 urllib.quote(event["room_id"]), 639 event["user_id"], 640 event["msg_id"], 641 self._usr(), 642 feedback_type, 643 ) 644 data = {} 645 reactor.callFromThread( 646 self._run_and_pprint, 647 "PUT", 648 path, 649 data=data, 650 alt_text="Sent receipt for %s" % event["msg_id"], 651 ) 652 653 def _do_membership_change(self, roomid, membership, userid): 654 path = "/rooms/%s/state/m.room.member/%s" % ( 655 urllib.quote(roomid), 656 urllib.quote(userid), 657 ) 658 data = {"membership": membership} 659 reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) 660 661 def do_displayname(self, line): 662 """Get or set my displayname: "displayname [new_name]" """ 663 args = self._parse(line, ["name"]) 664 path = "/profile/%s/displayname" % (self.config["user"]) 665 666 if "name" in args: 667 data = {"displayname": args["name"]} 668 reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) 669 else: 670 reactor.callFromThread(self._run_and_pprint, "GET", path) 671 672 def _do_presence_state(self, state, line): 673 args = self._parse(line, ["msgstring"]) 674 path = "/presence/%s/status" % (self.config["user"]) 675 data = {"state": state} 676 if "msgstring" in args: 677 data["status_msg"] = args["msgstring"] 678 679 reactor.callFromThread(self._run_and_pprint, "PUT", path, data=data) 680 681 def do_offline(self, line): 682 """Set my presence state to OFFLINE""" 683 self._do_presence_state(0, line) 684 685 def do_away(self, line): 686 """Set my presence state to AWAY""" 687 self._do_presence_state(1, line) 688 689 def do_online(self, line): 690 """Set my presence state to ONLINE""" 691 self._do_presence_state(2, line) 692 693 def _parse(self, line, keys, force_keys=False): 694 """Parses the given line. 695 696 Args: 697 line : The line to parse 698 keys : A list of keys to map onto the args 699 force_keys : True to enforce that the line has a value for every key 700 Returns: 701 A dict of key:arg 702 """ 703 line_args = shlex.split(line) 704 if force_keys and len(line_args) != len(keys): 705 raise IndexError("Must specify all args: %s" % keys) 706 707 # do $ substitutions 708 for i, arg in enumerate(line_args): 709 for config_key in self.config: 710 if ("$" + config_key) in arg: 711 arg = arg.replace("$" + config_key, self.config[config_key]) 712 line_args[i] = arg 713 714 return dict(zip(keys, line_args)) 715 716 @defer.inlineCallbacks 717 def _run_and_pprint( 718 self, 719 method, 720 path, 721 data=None, 722 query_params: Optional[dict] = None, 723 alt_text=None, 724 ): 725 """Runs an HTTP request and pretty prints the output. 726 727 Args: 728 method: HTTP method 729 path: Relative path 730 data: Raw JSON data if any 731 query_params: dict of query parameters to add to the url 732 """ 733 query_params = query_params or {"access_token": None} 734 735 url = self._url() + path 736 if "access_token" in query_params: 737 query_params["access_token"] = self._tok() 738 739 json_res = yield self.http_client.do_request( 740 method, url, data=data, qparams=query_params 741 ) 742 if alt_text: 743 print(alt_text) 744 else: 745 print(json.dumps(json_res, indent=4)) 746 747 748def save_config(config): 749 with open(CONFIG_JSON, "w") as out: 750 json.dump(config, out) 751 752 753def main(server_url, identity_server_url, username, token, config_path): 754 print("Synapse command line client") 755 print("===========================") 756 print("Server: %s" % server_url) 757 print("Type 'help' to get started.") 758 print("Close this console with CTRL+C then CTRL+D.") 759 if not username or not token: 760 print("- 'register <username>' - Register an account") 761 print("- 'stream' - Connect to the event stream") 762 print("- 'create <roomid>' - Create a room") 763 print("- 'send <roomid> <message>' - Send a message") 764 http_client = TwistedHttpClient() 765 766 # the command line client 767 syn_cmd = SynapseCmd(http_client, server_url, identity_server_url, username, token) 768 769 # load synapse.json config from a previous session 770 global CONFIG_JSON 771 CONFIG_JSON = config_path # bit cheeky, but just overwrite the global 772 try: 773 with open(config_path, "r") as config: 774 syn_cmd.config = json.load(config) 775 try: 776 http_client.verbose = "on" == syn_cmd.config["verbose"] 777 except Exception: 778 pass 779 print("Loaded config from %s" % config_path) 780 except Exception: 781 pass 782 783 # Twisted-specific: Runs the command processor in Twisted's event loop 784 # to maintain a single thread for both commands and event processing. 785 # If using another HTTP client, just call syn_cmd.cmdloop() 786 reactor.callInThread(syn_cmd.cmdloop) 787 reactor.run() 788 789 790if __name__ == "__main__": 791 parser = argparse.ArgumentParser("Starts a synapse client.") 792 parser.add_argument( 793 "-s", 794 "--server", 795 dest="server", 796 default="http://localhost:8008", 797 help="The URL of the home server to talk to.", 798 ) 799 parser.add_argument( 800 "-i", 801 "--identity-server", 802 dest="identityserver", 803 default="http://localhost:8090", 804 help="The URL of the identity server to talk to.", 805 ) 806 parser.add_argument( 807 "-u", "--username", dest="username", help="Your username on the server." 808 ) 809 parser.add_argument("-t", "--token", dest="token", help="Your access token.") 810 parser.add_argument( 811 "-c", 812 "--config", 813 dest="config", 814 default=CONFIG_JSON, 815 help="The location of the config.json file to read from.", 816 ) 817 args = parser.parse_args() 818 819 if not args.server: 820 print("You must supply a server URL to communicate with.") 821 parser.print_help() 822 sys.exit(1) 823 824 server = args.server 825 if not server.startswith("http://"): 826 server = "http://" + args.server 827 828 main(server, args.identityserver, args.username, args.token, args.config) 829