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) DeleteApiCall(endpoint string, response interface{}) error {
log.Debugf("Matrix DELETE request: %s\n", endpoint)
req, err := http.NewRequest("DELETE", mx.Server+endpoint, nil)
if err != nil {
return err
}
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{
Auth: RegisterRequestAuth{
Type: "m.login.application_service",
},
Type: "m.login.application_service",
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) DirectoryDeleteRoom(alias string) error {
var rep struct{}
err := mx.DeleteApiCall("/_matrix/client/r0/directory/room/"+url.QueryEscape(alias), &rep)
if err != nil {
return err
}
return 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
}