2016-11-30 11:46:33 +00:00
// Package gomatrix implements the Matrix Client-Server API.
//
// Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html
2016-11-30 17:41:14 +00:00
//
// Example usage of this library: (blocking version)
2016-11-30 17:51:03 +00:00
// cli, _ := gomatrix.NewClient("https://matrix.org", "@example:matrix.org", "MDAefhiuwehfuiwe")
// syncer := cli.Syncer.(*gomatrix.DefaultSyncer)
// syncer.OnEventType("m.room.message", func(ev *gomatrix.Event) {
// fmt.Println("Message: ", ev)
// })
// if err := cli.Sync(); err != nil {
// fmt.Println("Sync() returned ", err)
// }
2016-11-30 17:41:14 +00:00
//
// To make the example non-blocking, call Sync() in a goroutine.
2016-11-29 17:03:42 +00:00
package gomatrix
import (
2016-11-30 17:24:46 +00:00
"bytes"
"encoding/json"
"fmt"
2016-12-01 12:19:25 +00:00
"io"
2016-11-30 17:24:46 +00:00
"io/ioutil"
2016-11-29 17:03:42 +00:00
"net/http"
"net/url"
2016-11-30 11:46:33 +00:00
"path"
2016-11-30 17:24:46 +00:00
"strconv"
2016-11-29 17:03:42 +00:00
"sync"
2016-11-30 17:24:46 +00:00
"time"
2016-11-29 17:03:42 +00:00
)
// Client represents a Matrix client.
type Client struct {
2016-11-30 17:24:46 +00:00
HomeserverURL * url . URL // The base homeserver URL
Prefix string // The API prefix eg '/_matrix/client/r0'
UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID.
AccessToken string // The access_token for the client.
syncingMutex sync . Mutex // protects syncingID
syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
Client * http . Client // The underlying HTTP client which will be used to make HTTP requests.
Syncer Syncer // The thing which can process /sync responses
2016-12-01 11:33:12 +00:00
Store Storer // The thing which can store rooms/tokens/ids
2016-11-29 17:03:42 +00:00
}
2016-11-30 17:24:46 +00:00
// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
type HTTPError struct {
WrappedError error
Message string
Code int
}
func ( e HTTPError ) Error ( ) string {
var wrappedErrMsg string
if e . WrappedError != nil {
wrappedErrMsg = e . WrappedError . Error ( )
}
2016-12-01 11:36:41 +00:00
return fmt . Sprintf ( "msg=%s code=%d wrapped=%s" , e . Message , e . Code , wrappedErrMsg )
2016-11-30 17:24:46 +00:00
}
2016-11-30 11:46:33 +00:00
// BuildURL builds a URL with the Client's homserver/prefix/access_token set already.
func ( cli * Client ) BuildURL ( urlPath ... string ) string {
ps := [ ] string { cli . Prefix }
for _ , p := range urlPath {
ps = append ( ps , p )
}
return cli . BuildBaseURL ( ps ... )
}
// BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must
// supply the prefix in the path.
func ( cli * Client ) BuildBaseURL ( urlPath ... string ) string {
// copy the URL. Purposefully ignore error as the input is from a valid URL already
hsURL , _ := url . Parse ( cli . HomeserverURL . String ( ) )
parts := [ ] string { hsURL . Path }
parts = append ( parts , urlPath ... )
hsURL . Path = path . Join ( parts ... )
query := hsURL . Query ( )
2016-12-02 17:02:25 +00:00
if cli . AccessToken != "" {
query . Set ( "access_token" , cli . AccessToken )
}
2016-11-30 11:46:33 +00:00
hsURL . RawQuery = query . Encode ( )
return hsURL . String ( )
}
// BuildURLWithQuery builds a URL with query paramters in addition to the Client's homeserver/prefix/access_token set already.
func ( cli * Client ) BuildURLWithQuery ( urlPath [ ] string , urlQuery map [ string ] string ) string {
u , _ := url . Parse ( cli . BuildURL ( urlPath ... ) )
q := u . Query ( )
for k , v := range urlQuery {
q . Set ( k , v )
}
u . RawQuery = q . Encode ( )
return u . String ( )
}
2016-11-30 17:24:46 +00:00
// Sync starts syncing with the provided Homeserver. This function will block until a fatal /sync error occurs, so should
// almost always be started as a new goroutine. If Sync() is called twice then the first sync will be stopped.
func ( cli * Client ) Sync ( ) error {
// Mark the client as syncing.
// We will keep syncing until the syncing state changes. Either because
// Sync is called or StopSync is called.
syncingID := cli . incrementSyncingID ( )
2016-12-01 11:33:12 +00:00
nextBatch := cli . Store . LoadNextBatch ( cli . UserID )
filterID := cli . Store . LoadFilterID ( cli . UserID )
2016-11-30 17:24:46 +00:00
if filterID == "" {
2016-12-01 11:33:12 +00:00
filterJSON := cli . Syncer . GetFilterJSON ( cli . UserID )
2016-11-30 17:24:46 +00:00
resFilter , err := cli . CreateFilter ( filterJSON )
if err != nil {
return err
}
filterID = resFilter . FilterID
2016-12-01 11:33:12 +00:00
cli . Store . SaveFilterID ( cli . UserID , filterID )
2016-11-30 17:24:46 +00:00
}
for {
resSync , err := cli . SyncRequest ( 30000 , nextBatch , filterID , false , "" )
if err != nil {
duration , err2 := cli . Syncer . OnFailedSync ( resSync , err )
if err2 != nil {
return err2
}
time . Sleep ( duration )
continue
}
// Check that the syncing state hasn't changed
// Either because we've stopped syncing or another sync has been started.
// We discard the response from our sync.
if cli . getSyncingID ( ) != syncingID {
return nil
}
// Save the token now *before* processing it. This means it's possible
// to not process some events, but it means that we won't get constantly stuck processing
// a malformed/buggy event which keeps making us panic.
2016-12-01 11:33:12 +00:00
cli . Store . SaveNextBatch ( cli . UserID , resSync . NextBatch )
2016-11-30 17:24:46 +00:00
if err = cli . Syncer . ProcessResponse ( resSync , nextBatch ) ; err != nil {
return err
}
nextBatch = resSync . NextBatch
}
}
func ( cli * Client ) incrementSyncingID ( ) uint32 {
cli . syncingMutex . Lock ( )
defer cli . syncingMutex . Unlock ( )
cli . syncingID ++
return cli . syncingID
}
func ( cli * Client ) getSyncingID ( ) uint32 {
cli . syncingMutex . Lock ( )
defer cli . syncingMutex . Unlock ( )
return cli . syncingID
}
// StopSync stops the ongoing sync started by Sync.
func ( cli * Client ) StopSync ( ) {
// Advance the syncing state so that any running Syncs will terminate.
cli . incrementSyncingID ( )
}
2016-12-02 15:21:18 +00:00
// MakeRequest makes a JSON HTTP request to the given URL.
2016-12-02 14:07:41 +00:00
// If "resBody" is not nil, the response body will be json.Unmarshalled into it.
2016-12-01 12:19:25 +00:00
//
2016-12-02 16:51:20 +00:00
// Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along
// with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned
// HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError.
2016-12-02 14:07:41 +00:00
func ( cli * Client ) MakeRequest ( method string , httpURL string , reqBody interface { } , resBody interface { } ) ( [ ] byte , error ) {
2016-12-02 15:21:18 +00:00
var req * http . Request
var err error
if reqBody != nil {
var jsonStr [ ] byte
jsonStr , err = json . Marshal ( reqBody )
2016-12-02 14:07:41 +00:00
if err != nil {
return nil , err
}
2016-12-02 15:21:18 +00:00
req , err = http . NewRequest ( method , httpURL , bytes . NewBuffer ( jsonStr ) )
} else {
req , err = http . NewRequest ( method , httpURL , nil )
2016-11-30 17:24:46 +00:00
}
2016-12-02 15:21:18 +00:00
2016-11-30 17:24:46 +00:00
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
res , err := cli . Client . Do ( req )
if res != nil {
defer res . Body . Close ( )
}
if err != nil {
return nil , err
}
contents , err := ioutil . ReadAll ( res . Body )
2016-12-02 14:07:41 +00:00
if res . StatusCode / 100 != 2 { // not 2xx
2016-12-01 12:19:25 +00:00
var wrap error
var respErr RespError
2016-12-01 17:32:16 +00:00
if _ = json . Unmarshal ( contents , & respErr ) ; respErr . ErrCode != "" {
2016-12-01 12:19:25 +00:00
wrap = respErr
}
// If we failed to decode as RespError, don't just drop the HTTP body, include it in the
// HTTP error instead (e.g proxy errors which return HTML).
msg := "Failed to " + method + " JSON"
if wrap == nil {
msg = msg + ": " + string ( contents )
}
2016-12-02 16:51:20 +00:00
return contents , HTTPError {
2016-12-01 12:19:25 +00:00
Code : res . StatusCode ,
Message : msg ,
WrappedError : wrap ,
2016-11-30 17:24:46 +00:00
}
}
if err != nil {
return nil , err
}
2016-12-02 14:07:41 +00:00
if resBody != nil {
if err = json . Unmarshal ( contents , & resBody ) ; err != nil {
return nil , err
}
}
2016-11-30 17:24:46 +00:00
return contents , nil
}
// CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
2016-12-02 14:07:41 +00:00
func ( cli * Client ) CreateFilter ( filter json . RawMessage ) ( resp * RespCreateFilter , err error ) {
2016-11-30 17:24:46 +00:00
urlPath := cli . BuildURL ( "user" , cli . UserID , "filter" )
2016-12-02 15:21:18 +00:00
_ , err = cli . MakeRequest ( "POST" , urlPath , & filter , & resp )
2016-12-02 14:07:41 +00:00
return
2016-11-30 17:24:46 +00:00
}
// SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
2016-12-02 14:07:41 +00:00
func ( cli * Client ) SyncRequest ( timeout int , since , filterID string , fullState bool , setPresence string ) ( resp * RespSync , err error ) {
2016-11-30 17:24:46 +00:00
query := map [ string ] string {
"timeout" : strconv . Itoa ( timeout ) ,
}
if since != "" {
query [ "since" ] = since
}
if filterID != "" {
query [ "filter" ] = filterID
}
if setPresence != "" {
query [ "set_presence" ] = setPresence
}
if fullState {
query [ "full_state" ] = "true"
}
urlPath := cli . BuildURLWithQuery ( [ ] string { "sync" } , query )
2016-12-02 15:21:18 +00:00
_ , err = cli . MakeRequest ( "GET" , urlPath , nil , & resp )
2016-12-02 14:07:41 +00:00
return
2016-11-30 17:24:46 +00:00
}
2016-12-02 16:51:20 +00:00
func ( cli * Client ) register ( u string , req * ReqRegister ) ( resp * RespRegister , uiaResp * RespUserInteractive , err error ) {
var bodyBytes [ ] byte
bodyBytes , err = cli . MakeRequest ( "POST" , u , req , nil )
if err != nil {
httpErr , ok := err . ( HTTPError )
if ! ok { // network error
return
}
if httpErr . Code == 401 {
// body should be RespUserInteractive, if it isn't, fail with the error
err = json . Unmarshal ( bodyBytes , & uiaResp )
return
}
return
}
// body should be RespRegister
err = json . Unmarshal ( bodyBytes , & resp )
return
}
// Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
//
// Registers with kind=user. For kind=guest, see RegisterGuest.
2016-12-02 16:52:32 +00:00
func ( cli * Client ) Register ( req * ReqRegister ) ( * RespRegister , * RespUserInteractive , error ) {
2016-12-02 16:51:20 +00:00
u := cli . BuildURL ( "register" )
return cli . register ( u , req )
}
// RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
// with kind=guest.
//
// For kind=user, see Register.
2016-12-02 16:52:32 +00:00
func ( cli * Client ) RegisterGuest ( req * ReqRegister ) ( * RespRegister , * RespUserInteractive , error ) {
2016-12-02 16:51:20 +00:00
query := map [ string ] string {
"kind" : "guest" ,
}
u := cli . BuildURLWithQuery ( [ ] string { "register" } , query )
return cli . register ( u , req )
}
2016-12-02 17:35:07 +00:00
// RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth
//
// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
2016-12-05 15:05:03 +00:00
// this way. If the homeserver does not, an error is returned. If "setOnClient" is true, the access_token and user_id will be set on
// this client instance.
2016-12-02 17:35:07 +00:00
//
// res, err := cli.RegisterDummy(&gomatrix.ReqRegister{
// Username: "alice",
// Password: "wonderland",
2016-12-05 15:05:03 +00:00
// }, false)
2016-12-02 17:35:07 +00:00
// if err != nil {
// panic(err)
// }
// token := res.AccessToken
2016-12-05 15:05:03 +00:00
func ( cli * Client ) RegisterDummy ( req * ReqRegister , setOnClient bool ) ( * RespRegister , error ) {
2016-12-02 17:35:07 +00:00
res , uia , err := cli . Register ( req )
if err != nil && uia == nil {
return nil , err
}
if uia != nil && uia . HasSingleStageFlow ( "m.login.dummy" ) {
req . Auth = struct {
Type string ` json:"type" `
Session string ` json:"session,omitempty" `
} { "m.login.dummy" , uia . Session }
res , _ , err = cli . Register ( req )
if err != nil {
return nil , err
}
}
if res == nil {
return nil , fmt . Errorf ( "registration failed: does this server support m.login.dummy?" )
}
2016-12-05 15:05:03 +00:00
if setOnClient {
cli . UserID = res . UserID
cli . AccessToken = res . AccessToken
}
2016-12-02 17:35:07 +00:00
return res , nil
}
2016-12-01 12:19:25 +00:00
// JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
//
// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
// be JSON encoded and used as the request body.
2016-12-02 14:07:41 +00:00
func ( cli * Client ) JoinRoom ( roomIDorAlias , serverName string , content interface { } ) ( resp * RespJoinRoom , err error ) {
2016-12-01 12:19:25 +00:00
var urlPath string
if serverName != "" {
urlPath = cli . BuildURLWithQuery ( [ ] string { "join" , roomIDorAlias } , map [ string ] string {
"server_name" : serverName ,
} )
} else {
urlPath = cli . BuildURL ( "join" , roomIDorAlias )
}
2016-12-02 15:21:18 +00:00
_ , err = cli . MakeRequest ( "POST" , urlPath , content , & resp )
2016-12-02 14:07:41 +00:00
return
2016-12-01 12:19:25 +00:00
}
// SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
2016-12-02 14:07:41 +00:00
func ( cli * Client ) SetDisplayName ( displayName string ) ( err error ) {
2016-12-01 12:19:25 +00:00
urlPath := cli . BuildURL ( "profile" , cli . UserID , "displayname" )
s := struct {
DisplayName string ` json:"displayname" `
} { displayName }
2016-12-02 14:07:41 +00:00
_ , err = cli . MakeRequest ( "PUT" , urlPath , & s , nil )
return
2016-12-01 12:19:25 +00:00
}
// SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
2016-12-02 14:07:41 +00:00
func ( cli * Client ) SendMessageEvent ( roomID string , eventType string , contentJSON interface { } ) ( resp * RespSendEvent , err error ) {
2016-12-01 12:19:25 +00:00
txnID := "go" + strconv . FormatInt ( time . Now ( ) . UnixNano ( ) , 10 )
urlPath := cli . BuildURL ( "rooms" , roomID , "send" , eventType , txnID )
2016-12-02 15:21:18 +00:00
_ , err = cli . MakeRequest ( "PUT" , urlPath , contentJSON , & resp )
2016-12-02 14:07:41 +00:00
return
2016-12-01 12:19:25 +00:00
}
// SendText sends an m.room.message event into the given room with a msgtype of m.text
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
func ( cli * Client ) SendText ( roomID , text string ) ( * RespSendEvent , error ) {
return cli . SendMessageEvent ( roomID , "m.room.message" ,
TextMessage { "m.text" , text } )
}
2016-12-01 17:32:16 +00:00
// LeaveRoom leaves the given room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
2016-12-02 14:07:41 +00:00
func ( cli * Client ) LeaveRoom ( roomID string ) ( resp * RespLeaveRoom , err error ) {
2016-12-01 17:32:16 +00:00
u := cli . BuildURL ( "rooms" , roomID , "leave" )
2016-12-02 15:21:18 +00:00
_ , err = cli . MakeRequest ( "POST" , u , struct { } { } , & resp )
2016-12-02 14:07:41 +00:00
return
2016-12-01 17:32:16 +00:00
}
2016-12-02 15:36:09 +00:00
// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
// the HTTP response body, or return an error.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
func ( cli * Client ) StateEvent ( roomID , eventType , stateKey string , outContent interface { } ) ( err error ) {
u := cli . BuildURL ( "rooms" , roomID , "state" , eventType , stateKey )
_ , err = cli . MakeRequest ( "GET" , u , nil , outContent )
return
}
2016-12-01 12:19:25 +00:00
// UploadLink uploads an HTTP URL and then returns an MXC URI.
func ( cli * Client ) UploadLink ( link string ) ( * RespMediaUpload , error ) {
res , err := cli . Client . Get ( link )
if res != nil {
defer res . Body . Close ( )
}
if err != nil {
return nil , err
}
return cli . UploadToContentRepo ( res . Body , res . Header . Get ( "Content-Type" ) , res . ContentLength )
}
// UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI.
// See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
func ( cli * Client ) UploadToContentRepo ( content io . Reader , contentType string , contentLength int64 ) ( * RespMediaUpload , error ) {
req , err := http . NewRequest ( "POST" , cli . BuildBaseURL ( "_matrix/media/r0/upload" ) , content )
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , contentType )
req . ContentLength = contentLength
res , err := cli . Client . Do ( req )
if res != nil {
defer res . Body . Close ( )
}
if err != nil {
return nil , err
}
if res . StatusCode != 200 {
return nil , HTTPError {
Message : "Upload request failed" ,
Code : res . StatusCode ,
}
}
var m RespMediaUpload
if err := json . NewDecoder ( res . Body ) . Decode ( & m ) ; err != nil {
return nil , err
}
return & m , nil
}
2016-11-29 17:03:42 +00:00
// NewClient creates a new Matrix Client ready for syncing
2016-11-30 11:46:33 +00:00
func NewClient ( homeserverURL , userID , accessToken string ) ( * Client , error ) {
hsURL , err := url . Parse ( homeserverURL )
if err != nil {
return nil , err
}
2016-12-01 11:33:12 +00:00
// By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
// The client will work with this storer: it just won't remember across restarts.
// In practice, a database backend should be used.
store := NewInMemoryStore ( )
2016-11-29 17:03:42 +00:00
cli := Client {
AccessToken : accessToken ,
2016-11-30 11:46:33 +00:00
HomeserverURL : hsURL ,
2016-11-29 17:03:42 +00:00
UserID : userID ,
Prefix : "/_matrix/client/r0" ,
2016-12-01 11:33:12 +00:00
Syncer : NewDefaultSyncer ( userID , store ) ,
Store : store ,
2016-11-29 17:03:42 +00:00
}
2016-11-30 11:46:33 +00:00
// By default, use the default HTTP client.
cli . Client = http . DefaultClient
2016-11-29 17:03:42 +00:00
2016-11-30 11:46:33 +00:00
return & cli , nil
2016-11-29 17:03:42 +00:00
}