package mxlib import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" log "github.com/sirupsen/logrus" "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" ) type Client struct { Server string Token string httpClient *http.Client } func NewClient(server string, token string) *Client { tr := &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second, DisableCompression: true, } return &Client{ Server: server, Token: token, httpClient: &http.Client{Transport: tr}, } } func (mx *Client) GetApiCall(endpoint string, response interface{}) error { log.Debugf("Matrix GET request: %s\n", endpoint) req, err := http.NewRequest("GET", mx.Server+endpoint, nil) if err != nil { return err } return mx.DoAndParse(req, response) } func (mx *Client) PutApiCall(endpoint string, data interface{}, response interface{}) error { body, err := json.Marshal(data) if err != nil { return err } log.Debugf("Matrix PUT request: %s\n", endpoint) req, err := http.NewRequest("PUT", mx.Server+endpoint, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Add("Content-Type", "application/json") return mx.DoAndParse(req, response) } func (mx *Client) PostApiCall(endpoint string, data interface{}, response interface{}) error { body, err := json.Marshal(data) if err != nil { return err } log.Debugf("Matrix POST request: %s\n", endpoint) req, err := http.NewRequest("POST", mx.Server+endpoint, bytes.NewBuffer(body)) if err != nil { return err } req.Header.Add("Content-Type", "application/json") return mx.DoAndParse(req, response) } func (mx *Client) DoAndParse(req *http.Request, response interface{}) error { if mx.Token != "" { req.Header.Add("Authorization", "Bearer "+mx.Token) } resp, err := mx.httpClient.Do(req) if err != nil { return err } if resp.StatusCode != http.StatusOK { var e MxError err = json.NewDecoder(resp.Body).Decode(&e) if err != nil { return err } log.Debugf("Response: %d %#v\n", resp.StatusCode, e) return &e } err = json.NewDecoder(resp.Body).Decode(response) if err != nil { return err } log.Debugf("Response: 200 OK") return nil } // ---- func (mx *Client) PasswordLogin(username string, password string, device_id string, device_name string) (string, error) { req := PasswordLoginRequest{ Type: "m.login.password", Identifier: map[string]string{ "type": "m.id.user", "user": username, }, Password: password, DeviceID: device_id, InitialDeviceDisplayNAme: device_name, } var rep LoginResponse err := mx.PostApiCall("/_matrix/client/r0/login", &req, &rep) if err != nil { return "", err } if mx.Token == "" { mx.Token = rep.AccessToken } return rep.UserID, nil } func (mx *Client) RegisterUser(username string) error { req := RegisterRequest{ Username: username, } var rep RegisterResponse return mx.PostApiCall("/_matrix/client/r0/register?kind=user", &req, &rep) } func (mx *Client) ProfileDisplayname(userid string, displayname string) error { req := ProfileDisplaynameRequest{ Displayname: displayname, } var rep struct{} err := mx.PutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/displayname?user_id=%s", url.QueryEscape(userid), url.QueryEscape(userid)), &req, &rep) return err } func (mx *Client) ProfileAvatar(userid string, m connector.MediaObject) error { var mxc *MediaObject if mxm, ok := m.(*MediaObject); ok { mxc = mxm } else { mxm, err := mx.UploadMedia(m) if err != nil { return err } mxc = mxm } req := ProfileAvatarUrl{ AvatarUrl: mxc.MxcUri(), } var rep struct{} err := mx.PutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/avatar_url?user_id=%s", url.QueryEscape(userid), url.QueryEscape(userid)), &req, &rep) return err } func (mx *Client) DirectoryRoom(alias string) (string, error) { var rep DirectoryRoomResponse err := mx.GetApiCall("/_matrix/client/r0/directory/room/"+url.QueryEscape(alias), &rep) if err != nil { return "", err } return rep.RoomId, nil } func (mx *Client) CreateRoom(name string, alias string, invite []string) (string, error) { rq := CreateRoomRequest{ Preset: "private_chat", RoomAliasName: alias, Name: name, Topic: "", Invite: invite, CreationContent: map[string]interface{}{ "m.federate": false, }, PowerLevels: map[string]interface{}{ "invite": 100, "events": map[string]interface{}{ "m.room.topic": 0, "m.room.avatar": 0, }, }, } var rep CreateRoomResponse err := mx.PostApiCall("/_matrix/client/r0/createRoom", &rq, &rep) if err != nil { return "", err } return rep.RoomId, nil } func (mx *Client) CreateDirectRoomAs(invite []string, as_user string) (string, error) { rq := CreateDirectRoomRequest{ Preset: "private_chat", Topic: "", Invite: invite, CreationContent: map[string]interface{}{ "m.federate": false, }, PowerLevels: map[string]interface{}{ "invite": 100, }, IsDirect: true, } var rep CreateRoomResponse err := mx.PostApiCall("/_matrix/client/r0/createRoom?user_id="+url.QueryEscape(as_user), &rq, &rep) if err != nil { return "", err } return rep.RoomId, nil } func (mx *Client) RoomInvite(room string, user string) error { rq := RoomInviteRequest{ UserId: user, } var rep struct{} err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/invite", &rq, &rep) return err } func (mx *Client) RoomKick(room string, user string, reason string) error { rq := RoomKickRequest{ UserId: user, Reason: reason, } var rep struct{} err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/kick", &rq, &rep) return err } func (mx *Client) RoomJoinAs(room string, user string) error { rq := struct{}{} var rep RoomJoinResponse err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/join?user_id="+url.QueryEscape(user), &rq, &rep) return err } func (mx *Client) RoomLeaveAs(room string, user string) error { rq := struct{}{} var rep struct{} err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/leave?user_id="+url.QueryEscape(user), &rq, &rep) return err } func (mx *Client) SendAs(room string, event_type string, content map[string]interface{}, user string) error { txn_id := time.Now().UnixNano() var rep RoomSendResponse err := mx.PutApiCall(fmt.Sprintf( "/_matrix/client/r0/rooms/%s/send/%s/%d?user_id=%s", url.QueryEscape(room), event_type, txn_id, url.QueryEscape(user)), &content, &rep) return err } func (mx *Client) SendMessageAs(room string, typ string, body string, user string) error { content := map[string]interface{}{ "msgtype": typ, "body": body, } return mx.SendAs(room, "m.room.message", content, user) } func (mx *Client) PutStateAs(room string, event_type string, key string, content map[string]interface{}, as_user string) error { var rep RoomSendResponse err := mx.PutApiCall(fmt.Sprintf( "/_matrix/client/r0/rooms/%s/state/%s/%s?user_id=%s", url.QueryEscape(room), event_type, key, url.QueryEscape(as_user)), &content, &rep) return err } func (mx *Client) RoomNameAs(room string, name string, as_user string) error { content := map[string]interface{}{ "name": name, } return mx.PutStateAs(room, "m.room.name", "", content, as_user) } func (mx *Client) RoomAvatarAs(room string, pic connector.MediaObject, as_user string) error { mo, err := mx.UploadMedia(pic) if err != nil { return err } content := map[string]interface{}{ "url": mo.MxcUri(), "info": map[string]interface{}{ "mimetype": mo.Mimetype(), "size": mo.Size(), }, } return mx.PutStateAs(room, "m.room.avatar", "", content, as_user) } func (mx *Client) RoomTopicAs(room string, topic string, as_user string) error { content := map[string]interface{}{ "topic": topic, } return mx.PutStateAs(room, "m.room.topic", "", content, as_user) } func (mx *Client) UploadMedia(m connector.MediaObject) (*MediaObject, error) { // Return early if this is already a Matrix media object if mxm, ok := m.(*MediaObject); ok { return mxm, nil } reader, err := m.Read() if err != nil { return nil, err } defer reader.Close() req, err := http.NewRequest("POST", mx.Server+"/_matrix/media/r0/upload?filename="+url.QueryEscape(m.Filename()), reader) req.Header.Add("Content-Type", m.Mimetype()) req.ContentLength = m.Size() // TODO: this wasn't specified as mandatory in the matrix client/server spec, do a PR to fix this var resp UploadResponse err = mx.DoAndParse(req, &resp) if err != nil { return nil, err } mxc := strings.Split(strings.Replace(resp.ContentUri, "mxc://", "", 1), "/") if len(mxc) != 2 { return nil, fmt.Errorf("Invalid mxc:// returned: %s", resp.ContentUri) } media := &MediaObject{ mxClient: mx, filename: m.Filename(), size: m.Size(), mimetype: m.Mimetype(), imageSize: m.ImageSize(), MxcServer: mxc[0], MxcMediaId: mxc[1], } return media, nil } func (mx *Client) ParseMediaInfo(content map[string]interface{}) *MediaObject { // Content is an event content of type m.file or m.image info := content["info"].(map[string]interface{}) mxc := strings.Split(strings.Replace(content["url"].(string), "mxc://", "", 1), "/") media := &MediaObject{ mxClient: mx, filename: content["body"].(string), size: int64(info["size"].(float64)), mimetype: info["mimetype"].(string), MxcServer: mxc[0], MxcMediaId: mxc[1], } if content["msgtype"].(string) == "m.image" { media.imageSize = &connector.ImageSize{ Width: int(info["w"].(float64)), Height: int(info["h"].(float64)), } } return media }