diff --git a/README.rst b/README.rst index 4f292767..e8a9c9d2 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Matrix Client SDK for Python :target: https://pypi.python.org/pypi/matrix-client/ :alt: Latest Version -This is a Matrix client-server SDK for Python 2.x. +This is a Matrix client-server SDK for Python 2.x and 3.x. Usage ===== @@ -42,10 +42,15 @@ The SDK is split into two modules: ``api`` and ``client``. API --- -This contains the raw HTTP API calls and has minimal business logic. You can -set the access token (``token``) to use for requests as well as set a custom +This contains the raw HTTP API calls and has minimal business logic. You can +set the access token (``token``) to use for requests as well as set a custom transaction ID (``txn_id``) which will be incremented for each request. Client ------ -This encapsulates the API module and provides object models such as ``Room``. +This encapsulates the API module and provides object models such as ``Room``. + +Samples +======= +A collection of samples are included, written in Python 3. +You do not need to install matrix_client to run the samples, they will automatically include the files. diff --git a/matrix_client/api.py b/matrix_client/api.py index 7593662e..6bc4b2af 100644 --- a/matrix_client/api.py +++ b/matrix_client/api.py @@ -16,16 +16,24 @@ import json import re import requests -import urllib + try: import urlparse + from urllib import quote except ImportError: + from urllib.parse import quote import urllib.parse as urlparse # For python 3 class MatrixError(Exception): """A generic Matrix error. Specific errors will subclass this.""" pass +class MatrixUnexpectedResponse(MatrixError): + """The home server gave an unexpected response. """ + def __init__(self,content=""): + super(MatrixRequestError, self).__init__(content) + self.content = content + class MatrixRequestError(MatrixError): """ The home server returned an error response. """ @@ -54,10 +62,7 @@ def __init__(self, base_url, token=None): base_url(str): The home server URL e.g. 'http://localhost:8008' token(str): Optional. The client's access token. """ - if not base_url.endswith("/_matrix/client/api/v1"): - self.url = urlparse.urljoin(base_url, "/_matrix/client/api/v1") - else: - self.url = base_url + self.base_url = base_url self.token = token self.txn_id = 0 self.validate_cert = True @@ -130,7 +135,7 @@ def join_room(self, room_id_or_alias): if not room_id_or_alias: raise MatrixError("No alias or room ID to join.") - path = "/join/%s" % urllib.quote(room_id_or_alias) + path = "/join/%s" % quote(room_id_or_alias) return self._send("POST", path) @@ -159,10 +164,10 @@ def send_state_event(self, room_id, event_type, content, state_key=""): state_key(str): Optional. The state key for the event. """ path = ("/rooms/%s/state/%s" % - (urllib.quote(room_id), urllib.quote(event_type)) + (urlparse.quote(room_id), urlparse.quote(event_type)) ) if state_key: - path += "/%s" % (urllib.quote(state_key)) + path += "/%s" % (quote(state_key)) return self._send("PUT", path, content) def send_message_event(self, room_id, event_type, content, txn_id=None): @@ -180,11 +185,24 @@ def send_message_event(self, room_id, event_type, content, txn_id=None): self.txn_id = self.txn_id + 1 path = ("/rooms/%s/send/%s/%s" % - (urllib.quote(room_id), urllib.quote(event_type), - urllib.quote(unicode(txn_id))) + (quote(room_id), quote(event_type), quote(str(txn_id))) ) return self._send("PUT", path, content) + # content_type can be a image,audio or video + # extra information should be supplied, see https://matrix.org/docs/spec/r0.0.1/client_server.html + def send_content(self, room_id, item_url, item_name, msg_type, extra_information=None): + if extra_information == None: + extra_information = {} + + content_pack = { + "url":item_url, + "msgtype":msg_type, + "body":item_name, + "info":extra_information + } + return self.send_message_event(room_id, "m.room.message", content_pack) + def send_message(self, room_id, text_content, msgtype="m.text"): """Perform /rooms/$room_id/send/m.room.message @@ -302,18 +320,23 @@ def get_emote_body(self, text): "body": text } - def _send(self, method, path, content=None, query_params={}, headers={}): + def _send(self, method, path, content=None, query_params={}, headers={}, api_path="/_matrix/client/api/v1"): method = method.upper() if method not in ["GET", "PUT", "DELETE", "POST"]: raise MatrixError("Unsupported HTTP method: %s" % method) - headers["Content-Type"] = "application/json" + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json" + query_params["access_token"] = self.token - endpoint = self.url + path + endpoint = self.base_url + api_path + path + + if headers["Content-Type"] == "application/json": + content = json.dumps(content) response = requests.request( method, endpoint, params=query_params, - data=json.dumps(content), headers=headers + data=content, headers=headers , verify=self.validate_cert #if you want to use SSL without verifying the Cert ) @@ -323,3 +346,6 @@ def _send(self, method, path, content=None, query_params={}, headers={}): ) return response.json() + + def media_upload(self, content, content_type): + return _send("PUT","",content=content,headers={"Content-Type":content_type},apipath="/_matrix/media/r0/upload") diff --git a/matrix_client/client.py b/matrix_client/client.py index ea89dc6c..021c50eb 100644 --- a/matrix_client/client.py +++ b/matrix_client/client.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from .api import MatrixHttpApi, MatrixRequestError +from .api import MatrixHttpApi, MatrixRequestError, MatrixUnexpectedResponse from threading import Thread import sys # TODO: Finish implementing this. @@ -127,6 +127,16 @@ def start_listener_thread(self, timeout=30000): e = sys.exc_info()[0] print("Error: unable to start thread. " + str(e)) + def upload(self,content,content_type): + try: + response = self.api.media_upload(content,content_type) + return response["content_uri"] + except MatrixRequestError as e: + raise MatrixRequestError(code=e.code, content="Upload failed: %s" % e) + except KeyError: + raise MatrixUnexpectedResponse(content="The upload was successful, but content_uri was found in response.") + + def _mkroom(self, room_id): self.rooms[room_id] = Room(self, room_id) return self.rooms[room_id] @@ -171,6 +181,10 @@ def send_text(self, text): def send_emote(self, text): return self.client.api.send_emote(self.room_id, text) + #See http://matrix.org/docs/spec/r0.0.1/client_server.html#m-image for the imageinfo args. + def send_image(self, url, name, **imageinfo): + return self.client.api.send_content(self.room_id, url, name, "image", imageinfo) + def add_listener(self, callback): self.listeners.append(callback) @@ -256,4 +270,3 @@ def update_aliases(self): return False except MatrixRequestError: return False - diff --git a/samples/SimpleChatClient.py b/samples/SimpleChatClient.py new file mode 100755 index 00000000..e75eee55 --- /dev/null +++ b/samples/SimpleChatClient.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# A simple chat client for matrix. +# This sample will allow you to connect to a room, and send/recieve messages. +# Args: host:port username room +# Error Codes: +# 1 - Unknown problem has occured +# 2 - Could not find the server. +# 3 - Bad URL Format. +# 4 - Bad username/password. +# 11 - Wrong room format. +# 12 - Couldn't find room. + +import sys +sys.path.insert(0, "../") # add ../ to PYTHONPATH + +from matrix_client.client import MatrixClient +from matrix_client.api import MatrixRequestError +from requests.exceptions import MissingSchema + +from getpass import getpass + +# Called when a message is recieved. +def on_message(event): + if event['type'] == "m.room.member": + if event['membership'] == "join": + print("{0} joined".format(event['content']['displayname'])) + elif event['type'] == "m.room.message": + if event['content']['msgtype'] == "m.text": + print("{0}: {1}".format(event['sender'],event['content']['body'])) + else: + print(event['type']) + +host = "" +username = "" +room = "" + +if len(sys.argv) > 1: + host = sys.argv[1] +else: + host = input("Host (ex: http://localhost:8008 ): ") + +client = MatrixClient(host) + +if len(sys.argv) > 2: + username = sys.argv[2] +else: + username = input("Username: ") + +password = getpass() #Hide user input + +try: + client.login_with_password(username,password) +except MatrixRequestError as e: + print(e) + if e.code == 403: + print("Bad username or password.") + sys.exit(4) + else: + print("Check your sever details are correct.") + sys.exit(3) + +except MissingSchema as e: + print("Bad URL format.") + print(e) + sys.exit(2) + + +room = None + +if len(sys.argv) > 3: + room = sys.argv[3] +else: + room = input("Room ID/Alias: ") + +try: + room = client.join_room(room) +except MatrixRequestError as e: + print(e) + if e.code == 400: + print("Room ID/Alias in the wrong format") + sys.exit(11) + else: + print("Couldn't find room.") + sys.exit(12) + +room.add_listener(on_message) +client.start_listener_thread() + +while True: + msg = input() + if msg == "/quit": + break + else: + room.send_text(msg)