aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2020-02-21 18:08:40 +0100
committerAlex Auvolat <alex@adnab.me>2020-02-21 18:08:40 +0100
commitfd768a10be36ec31f674fa291fcbe77b78a2855c (patch)
treebb9e736bfc9425e1ce5ee22379e8d46470af4d18
parentddd5936fb1f92432123a9a30d1d3a1fa644a4f8e (diff)
downloadeasybridge-fd768a10be36ec31f674fa291fcbe77b78a2855c.tar.gz
easybridge-fd768a10be36ec31f674fa291fcbe77b78a2855c.zip
Mattermost media objects in both ways + user/team profile pictures from MM to Matrix
-rw-r--r--appservice/account.go19
-rw-r--r--appservice/server.go3
-rw-r--r--connector/connector.go2
-rw-r--r--connector/irc/irc.go14
-rw-r--r--connector/mattermost/mattermost.go146
-rw-r--r--connector/mediaobject.go3
-rw-r--r--connector/xmpp/xmpp.go13
-rw-r--r--mxlib/client.go40
-rw-r--r--mxlib/mediaobject.go4
9 files changed, 198 insertions, 46 deletions
diff --git a/appservice/account.go b/appservice/account.go
index a2b95d7..2df2930 100644
--- a/appservice/account.go
+++ b/appservice/account.go
@@ -137,7 +137,10 @@ func (a *Account) userInfoUpdatedInternal(user UserID, info *UserInfo) error {
}
if info.Avatar != nil {
- err = fmt.Errorf("Avatar: not implemented")
+ err2 := mx.ProfileAvatar(mx_user_id, info.Avatar)
+ if err2 != nil {
+ err = err2
+ }
}
return err
@@ -182,8 +185,10 @@ func (a *Account) roomInfoUpdatedInternal(roomId RoomID, author UserID, info *Ro
}
if info.Picture != nil {
- // TODO
- err = fmt.Errorf("Picture: not implemented")
+ err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid)
+ if err2 != nil {
+ err = err2
+ }
}
return err
@@ -254,8 +259,8 @@ func (a *Account) eventInternal(event *Event) error {
return err
}
- if event.Attachements != nil {
- for _, file := range event.Attachements {
+ if event.Attachments != nil {
+ for _, file := range event.Attachments {
mxfile, err := mx.UploadMedia(file)
if err != nil {
return err
@@ -270,8 +275,8 @@ func (a *Account) eventInternal(event *Event) error {
content["info"] = map[string]interface{} {
"mimetype": mxfile.Mimetype(),
"size": mxfile.Size(),
- "width": sz.Width,
- "height": sz.Height,
+ "w": sz.Width,
+ "h": sz.Height,
}
} else {
content["msgtype"] = "m.file"
diff --git a/appservice/server.go b/appservice/server.go
index 5a5cebe..d96f27c 100644
--- a/appservice/server.go
+++ b/appservice/server.go
@@ -123,6 +123,9 @@ func handleTxnEvent(e *mxlib.Event) error {
typ := e.Content["msgtype"].(string)
if typ == "m.emote" {
ev.Type = connector.EVENT_MESSAGE
+ } else if typ == "m.file" || typ == "m.image" {
+ ev.Text = ""
+ ev.Attachments = []connector.MediaObject{mx.ParseMediaInfo(e.Content)}
}
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
diff --git a/connector/connector.go b/connector/connector.go
index 2bf1704..2235318 100644
--- a/connector/connector.go
+++ b/connector/connector.go
@@ -115,7 +115,7 @@ type Event struct {
Text string
// Attached files such as images
- Attachements []MediaObject
+ Attachments []MediaObject
}
type UserInfo struct {
diff --git a/connector/irc/irc.go b/connector/irc/irc.go
index 38aa79d..2ed3923 100644
--- a/connector/irc/irc.go
+++ b/connector/irc/irc.go
@@ -205,9 +205,17 @@ func (irc *IRC) Send(event *Event) error {
return fmt.Errorf("Invalid target")
}
- if event.Attachements != nil && len(event.Attachements) > 0 {
- // TODO find a way to send them using some hosing of some kind
- return fmt.Errorf("Attachements not supported on IRC")
+ if event.Attachments != nil && len(event.Attachments) > 0 {
+ for _, at := range event.Attachments {
+ url := at.URL()
+ if url == "" {
+ // TODO find a way to send them using some hosing of some kind
+ return fmt.Errorf("Attachment without URL sent to IRC")
+ } else {
+ irc.conn.Cmd.Message(dest, fmt.Sprintf("%s (%s, %dkb)",
+ url, at.Mimetype(), at.Size()/1024))
+ }
+ }
}
if event.Type == EVENT_MESSAGE {
diff --git a/connector/mattermost/mattermost.go b/connector/mattermost/mattermost.go
index 330026a..73ea66b 100644
--- a/connector/mattermost/mattermost.go
+++ b/connector/mattermost/mattermost.go
@@ -1,10 +1,12 @@
package mattermost
import (
+ "net/http"
"fmt"
_ "os"
"strings"
"time"
+ "io/ioutil"
"encoding/json"
"github.com/mattermost/mattermost-server/model"
@@ -210,9 +212,6 @@ func (mm *Mattermost) Leave(roomId RoomID) {
}
func (mm *Mattermost) Send(event *Event) error {
- // TODO: attachements
- // TODO: verify private messages work
-
post := &model.Post{
Message: event.Text,
}
@@ -248,8 +247,30 @@ func (mm *Mattermost) Send(event *Event) error {
return fmt.Errorf("Invalid target")
}
+ if event.Attachments != nil {
+ post.FileIds = []string{}
+ for _, file := range event.Attachments {
+ rdr, err := file.Read()
+ if err != nil {
+ return err
+ }
+ defer rdr.Close()
+ data, err := ioutil.ReadAll(rdr)
+ if err != nil {
+ return err
+ }
+ up_file, err := mm.conn.UploadFile(data, post.ChannelId, file.Filename())
+ if err != nil {
+ log.Warnf("UploadFile error: %s", err)
+ return err
+ }
+ post.FileIds = append(post.FileIds, up_file)
+ }
+ }
+
_, resp := mm.conn.Client.CreatePost(post)
if resp.Error != nil {
+ log.Warnf("CreatePost error: %s", resp.Error)
return resp.Error
}
return nil
@@ -277,16 +298,58 @@ func (mm *Mattermost) handleConnected() {
if len(strings.Split(ch.Name, "__")) == 2 {
continue // This is a DM channel
}
+
id := mm.reverseRoomId(ch.Id)
- chName := ch.DisplayName
- if teamName := mm.conn.GetTeamName(ch.TeamId); teamName != "" {
- chName = teamName + " / " + chName
- }
mm.handler.Joined(id)
- mm.handler.RoomInfoUpdated(id, UserID(""), &RoomInfo{
- Name: chName,
+
+ // Update room info
+ room_info := &RoomInfo{
+ Name: ch.DisplayName,
Topic: ch.Header,
- })
+ }
+ for _, t := range mm.conn.OtherTeams {
+ if t.Id == ch.TeamId {
+ if t.Team.DisplayName != "" {
+ room_info.Name = t.Team.DisplayName + " / " + room_info.Name
+ } else {
+ room_info.Name = t.Team.Name + " / " + room_info.Name
+ }
+ // TODO: cache last update time so we don't do this needlessly
+ if t.Team.LastTeamIconUpdate > 0 {
+ team_img, resp := mm.conn.Client.GetTeamIcon(t.Id, "")
+ if resp.Error == nil {
+ room_info.Picture = &BlobMediaObject{
+ ObjectFilename: t.Team.Name,
+ ObjectMimetype: http.DetectContentType(team_img),
+ ObjectData: team_img,
+ }
+ } else {
+ log.Warnf("Could not get team image: %s", resp.Error)
+ }
+ }
+ break
+ }
+ }
+ mm.handler.RoomInfoUpdated(id, UserID(""), room_info)
+
+ // Update member list
+ members, resp := mm.conn.Client.GetChannelMembers(ch.Id, 0, 1000, "")
+ if resp.Error == nil {
+ for _, mem := range *members {
+ if mem.UserId == mm.conn.User.Id {
+ continue
+ }
+ user := mm.conn.GetUser(mem.UserId)
+ if user != nil {
+ mm.ensureJoined(user, id)
+ mm.updateUserInfo(user)
+ } else {
+ log.Warnf("Could not find joined user: %s", mem.UserId)
+ }
+ }
+ } else {
+ log.Warnf("Could not get channel members: %s", resp.Error)
+ }
}
}
@@ -306,6 +369,45 @@ func (mm *Mattermost) handleLoop(msgCh chan *matterclient.Message, quitCh chan b
}
}
+func (mm *Mattermost) updateUserInfo(user *model.User) {
+ userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
+ userDisp := user.GetDisplayName(model.SHOW_NICKNAME_FULLNAME)
+
+ if lastdn, ok := mm.userdisplaynamemap[userId]; !ok || lastdn != userDisp {
+ ui := &UserInfo{
+ DisplayName: userDisp,
+ }
+ if user.LastPictureUpdate > 0 {
+ // TODO: cache last update time so we don't do this needlessly
+ img, resp := mm.conn.Client.GetProfileImage(user.Id, "")
+ if resp.Error == nil {
+ ui.Avatar = &BlobMediaObject{
+ ObjectFilename: user.Username,
+ ObjectMimetype: http.DetectContentType(img),
+ ObjectData: img,
+ }
+ } else {
+ log.Warnf("Could not get profile picture: %s", resp.Error)
+ }
+ mm.handler.UserInfoUpdated(userId, ui)
+ mm.userdisplaynamemap[userId] = userDisp
+ }
+ }
+}
+
+func (mm *Mattermost) ensureJoined(user *model.User, roomId RoomID) {
+ userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
+ cache_key := fmt.Sprintf("%s / %s", userId, roomId)
+ if _, ok := mm.sentjoinedmap[cache_key]; !ok {
+ mm.handler.Event(&Event{
+ Author: userId,
+ Room: roomId,
+ Type: EVENT_JOIN,
+ })
+ mm.sentjoinedmap[cache_key] = true
+ }
+}
+
func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
channel_name := msg.Data["channel_name"].(string)
post_str := msg.Data["post"].(string)
@@ -326,14 +428,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
return fmt.Errorf("Invalid user")
}
userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
-
- userDisp := user.GetDisplayName(model.SHOW_NICKNAME_FULLNAME)
- if lastdn, ok := mm.userdisplaynamemap[userId]; !ok || lastdn != userDisp {
- mm.handler.UserInfoUpdated(userId, &UserInfo{
- DisplayName: userDisp,
- })
- mm.userdisplaynamemap[userId] = userDisp
- }
+ mm.updateUserInfo(user)
// Build message event
msg_ev := &Event{
@@ -347,7 +442,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
// Handle files
if post.FileIds != nil && len(post.FileIds) > 0 {
- msg_ev.Attachements = []MediaObject{}
+ msg_ev.Attachments = []MediaObject{}
for _, file := range post.Metadata.Files {
blob, resp := mm.conn.Client.GetFile(file.Id)
if resp.Error != nil {
@@ -355,7 +450,6 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
}
media_object := &BlobMediaObject{
ObjectFilename: file.Name,
- ObjectSize: file.Size,
ObjectMimetype: file.MimeType,
ObjectData: blob,
}
@@ -365,7 +459,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
Height: file.Height,
}
}
- msg_ev.Attachements = append(msg_ev.Attachements, media_object)
+ msg_ev.Attachments = append(msg_ev.Attachments, media_object)
}
}
@@ -384,15 +478,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
return fmt.Errorf("Invalid channel id")
}
- cache_key := fmt.Sprintf("%s / %s", userId, roomId)
- if _, ok := mm.sentjoinedmap[cache_key]; !ok {
- mm.handler.Event(&Event{
- Author: userId,
- Room: roomId,
- Type: EVENT_JOIN,
- })
- mm.sentjoinedmap[cache_key] = true
- }
+ mm.ensureJoined(user, roomId)
if post.Type == "system_header_change" {
new_header := post.Props["new_header"].(string)
diff --git a/connector/mediaobject.go b/connector/mediaobject.go
index c6634b7..a8d6f9a 100644
--- a/connector/mediaobject.go
+++ b/connector/mediaobject.go
@@ -97,7 +97,6 @@ func (m *UrlMediaObject) URL() string {
type BlobMediaObject struct {
ObjectFilename string
- ObjectSize int64
ObjectMimetype string
ObjectImageSize *ImageSize
ObjectData []byte
@@ -108,7 +107,7 @@ func (m *BlobMediaObject) Filename() string {
}
func (m *BlobMediaObject) Size() int64 {
- return m.ObjectSize
+ return int64(len(m.ObjectData))
}
func (m *BlobMediaObject) Mimetype() string {
diff --git a/connector/xmpp/xmpp.go b/connector/xmpp/xmpp.go
index b18e670..02d1a96 100644
--- a/connector/xmpp/xmpp.go
+++ b/connector/xmpp/xmpp.go
@@ -308,6 +308,19 @@ func (xm *XMPP) Leave(roomId RoomID) {
}
func (xm *XMPP) Send(event *Event) error {
+ if event.Attachments != nil && len(event.Attachments) > 0 {
+ for _, at := range event.Attachments {
+ url := at.URL()
+ if url == "" {
+ // TODO find a way to send them using some hosing of some kind
+ return fmt.Errorf("Attachment without URL sent to XMPP")
+ } else {
+ event.Text += fmt.Sprintf("\n%s (%s, %dkb)",
+ url, at.Mimetype(), at.Size()/1024)
+ }
+ }
+ }
+
fmt.Printf("xm *XMPP Send %#v\n", event)
if len(event.Recipient) > 0 {
_, err := xm.conn.Send(gxmpp.Chat{
diff --git a/mxlib/client.go b/mxlib/client.go
index d8237d1..e07a67a 100644
--- a/mxlib/client.go
+++ b/mxlib/client.go
@@ -137,10 +137,9 @@ func (mx *Client) ProfileAvatar(userid string, m connector.MediaObject) error {
}
mxc = mxm
}
- mxc_uri := fmt.Sprintf("mxc://%s/%s", mxc.MxcServer, mxc.MxcMediaId)
req := ProfileAvatarUrl{
- AvatarUrl: mxc_uri,
+ AvatarUrl: mxc.MxcUri(),
}
var rep struct{}
err := mx.PutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/avatar_url?user_id=%s",
@@ -272,6 +271,21 @@ func (mx *Client) RoomNameAs(room string, name string, as_user string) error {
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,
@@ -295,7 +309,7 @@ func (mx *Client) UploadMedia(m connector.MediaObject) (*MediaObject, error) {
mx.Server+"/_matrix/media/r0/upload?filename="+url.QueryEscape(m.Filename()),
reader)
req.Header.Add("Content-Type", m.Mimetype())
- req.ContentLength = m.Size()
+ 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)
@@ -320,3 +334,23 @@ func (mx *Client) UploadMedia(m connector.MediaObject) (*MediaObject, error) {
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
+}
diff --git a/mxlib/mediaobject.go b/mxlib/mediaobject.go
index 1c35187..f29127b 100644
--- a/mxlib/mediaobject.go
+++ b/mxlib/mediaobject.go
@@ -59,3 +59,7 @@ 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))
}
+
+func (m *MediaObject) MxcUri() string {
+ return fmt.Sprintf("mxc://%s/%s", m.MxcServer, m.MxcMediaId)
+}