aboutsummaryrefslogblamecommitdiff
path: root/mxlib/client.go
blob: e382dddc978c25f1ca69444d20805af49492e3e9 (plain) (tree)
1
2
3
4
5
6
7
8
9







                       
                 


                                        

                                                           


































































                                                                                              


                                                                   


























                                                                      





















                                                                                                                         


















                                                                                                










                                                                               

                                
                                        







                                                                                               


























































































































                                                                                                                                   








                                                                                              
                                              




                                                                         





                                                                                

                                                                              




                                                                









                                                                                             
                                                                                                                                      












                                                                                      





                                          




                                   




                                                                                            




                                                          



                                                       
                                                         




                                                         
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 %s\n", endpoint, string(body))

	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 %s\n", endpoint, string(body))

	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: %#v\n", response)
	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
}