400 lines
10 KiB
Go
400 lines
10 KiB
Go
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{
|
|
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
|
|
}
|