Skip to content

Commit

Permalink
Merge pull request #9 from Half-Shot/python3
Browse files Browse the repository at this point in the history
Python 3 support

This will update the existing code to work for python 3 where libraries have been moved and syntax has changed. 
The newish media API has been implemented in the API and client.
In addition, a sample Python 3 program has been included.

Signed-off-by: Will Hunt <half-shot@molrams.com>
  • Loading branch information
kegsay committed Mar 19, 2016
2 parents af72b56 + e150667 commit e43db1d
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 20 deletions.
13 changes: 9 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=====
Expand Down Expand Up @@ -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.
54 changes: 40 additions & 14 deletions matrix_client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. """
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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
)

Expand All @@ -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")
17 changes: 15 additions & 2 deletions matrix_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -256,4 +270,3 @@ def update_aliases(self):
return False
except MatrixRequestError:
return False

95 changes: 95 additions & 0 deletions samples/SimpleChatClient.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit e43db1d

Please sign in to comment.