From 92d380aa86dfd3e60f5b8d826ec96c0fbc17614a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 21 Feb 2020 15:12:22 +0100 Subject: [PATCH] Easybridge has an avatar --- appservice/server.go | 6 ++++ connector/connector.go | 19 +++++++----- connector/mediaobject.go | 50 ++++++++++++++++++++++++++++++++ main.go | 8 +++--- mxlib/api.go | 8 ++++++ mxlib/client.go | 62 ++++++++++++++++++++++++++++++++++++++++ mxlib/mediaobject.go | 54 ++++++++++++++++++++++++++++++++++ 7 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 connector/mediaobject.go create mode 100644 mxlib/mediaobject.go diff --git a/appservice/server.go b/appservice/server.go index d3ecb43..48e26a6 100644 --- a/appservice/server.go +++ b/appservice/server.go @@ -45,6 +45,12 @@ func Start(r *mxlib.Registration, c *Config) (chan error, error) { if err != nil { return nil, err } + err = mx.ProfileAvatar(ezbrMxId(), &connector.FileMediaObject{ + Path: "easybridge.jpg", + }) + if err != nil { + return nil, err + } router := mux.NewRouter() router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn) diff --git a/connector/connector.go b/connector/connector.go index 5df010a..d05f4a2 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -1,5 +1,9 @@ package connector +import ( + "io" +) + /* A generic connector framework for instant messaging protocols. @@ -126,13 +130,14 @@ type RoomInfo struct { } type MediaObject interface { - Size() int - MimeType() string + Filename() string + Size() int64 + Mimetype() string - // AsBytes: must always be implemented - AsBytes() ([]byte, error) + // Read: must always be implemented + Read() (io.ReadCloser, error) - // AsString: not mandatory, may return an empty string - // If so, AsBytes() is the only way to retrieve the object - AsURL() string + // URL(): not mandatory, may return an empty string + // If so, Read() is the only way to retrieve the object + URL() string } diff --git a/connector/mediaobject.go b/connector/mediaobject.go new file mode 100644 index 0000000..75635ee --- /dev/null +++ b/connector/mediaobject.go @@ -0,0 +1,50 @@ +package connector + +import ( + "io" + "net/http" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" +) + +type FileMediaObject struct { + Path string +} + +func (m *FileMediaObject) Filename() string { + return filepath.Base(m.Path) +} + +func (m *FileMediaObject) Size() int64 { + fi, err := os.Stat(m.Path) + if err != nil { + log.Fatal(err) + } + return fi.Size() +} + +func (m *FileMediaObject) Mimetype() string { + f, err := os.Open(m.Path) + if err != nil { + log.Fatal(err) + } + defer f.Close() + + buffer := make([]byte, 512) + _, err = f.Read(buffer) + if err != nil { + log.Fatal(err) + } + + return http.DetectContentType(buffer) +} + +func (m *FileMediaObject) Read() (io.ReadCloser, error) { + return os.Open(m.Path) +} + +func (m *FileMediaObject) URL() string { + return "" +} diff --git a/main.go b/main.go index 50f8bc7..60272a7 100644 --- a/main.go +++ b/main.go @@ -200,7 +200,7 @@ func main() { } conn.SetHandler(account) appservice.AddAccount(account) - go connectAndJoin(conn, params) + go connectAndJoin(account, params) } } @@ -210,15 +210,15 @@ func main() { } } -func connectAndJoin(conn connector.Connector, params ConfigAccount) { +func connectAndJoin(account *appservice.Account, params ConfigAccount) { log.Printf("Connecting to %s", params.Protocol) - err := conn.Configure(params.Config) + err := account.Conn.Configure(params.Config) if err != nil { log.Printf("Could not connect to %s: %s", params.Protocol, err) } else { log.Printf("Connected to %s, now joining %#v", params.Protocol, params.Rooms) for _, room := range params.Rooms { - err := conn.Join(connector.RoomID(room)) + err := account.Conn.Join(connector.RoomID(room)) if err != nil { log.Printf("Could not join %s: %s", room, err) } diff --git a/mxlib/api.go b/mxlib/api.go index 6dfe56e..7752abc 100644 --- a/mxlib/api.go +++ b/mxlib/api.go @@ -83,3 +83,11 @@ type RoomJoinResponse struct { type RoomSendResponse struct { EventId string `json:"event_id"` } + +type UploadResponse struct { + ContentUri string `json:"content_uri"` +} + +type ProfileAvatarUrl struct { + AvatarUrl string `json:"avatar_url"` +} diff --git a/mxlib/client.go b/mxlib/client.go index 2385d6e..6d87663 100644 --- a/mxlib/client.go +++ b/mxlib/client.go @@ -1,6 +1,7 @@ package mxlib import ( + "strings" "bytes" "encoding/json" "fmt" @@ -9,6 +10,8 @@ import ( "time" log "github.com/sirupsen/logrus" + + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" ) type Client struct { @@ -123,6 +126,29 @@ func (mx *Client) ProfileDisplayname(userid string, displayname string) error { 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 + } + mxc_uri := fmt.Sprintf("mxc://%s/%s", mxc.MxcServer, mxc.MxcMediaId) + + req := ProfileAvatarUrl{ + AvatarUrl: mxc_uri, + } + 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) @@ -252,3 +278,39 @@ func (mx *Client) RoomTopicAs(room string, topic string, as_user string) error { } return mx.PutStateAs(room, "m.room.topic", "", content, as_user) } + +func (mx *Client) UploadMedia(m connector.MediaObject) (*MediaObject, error) { + 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() + + 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(), + MxcServer: mxc[0], + MxcMediaId: mxc[1], + } + return media, nil +} + diff --git a/mxlib/mediaobject.go b/mxlib/mediaobject.go new file mode 100644 index 0000000..8a730d2 --- /dev/null +++ b/mxlib/mediaobject.go @@ -0,0 +1,54 @@ +package mxlib + +import ( + "io" + "fmt" + "net/url" + "net/http" +) + +type MediaObject struct { + mxClient *Client + filename string + size int64 + mimetype string + MxcServer string + MxcMediaId string +} + +func (m *MediaObject) Filename() string { + return m.filename +} + +func (m *MediaObject) Size() int64 { + return m.size +} + +func (m *MediaObject) Mimetype() string { + return m.mimetype +} + +func (m *MediaObject) Read() (io.ReadCloser, error) { + req, err := http.NewRequest("GET", m.URL(), nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+m.mxClient.Token) + + resp, err := m.mxClient.httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP error %d", resp.StatusCode) + } + + return resp.Body, nil +} + +func (m *MediaObject) URL() string { + return fmt.Sprintf("%s/_matrix/media/r0/download/%s/%s/%s", + m.mxClient.Server, m.MxcServer, m.MxcMediaId, url.QueryEscape(m.filename)) +}