Mattermost media objects in both ways + user/team profile pictures from MM to Matrix

This commit is contained in:
Alex 2020-02-21 18:08:40 +01:00
parent ddd5936fb1
commit fd768a10be
9 changed files with 198 additions and 46 deletions

View file

@ -137,7 +137,10 @@ func (a *Account) userInfoUpdatedInternal(user UserID, info *UserInfo) error {
} }
if info.Avatar != nil { 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 return err
@ -182,8 +185,10 @@ func (a *Account) roomInfoUpdatedInternal(roomId RoomID, author UserID, info *Ro
} }
if info.Picture != nil { if info.Picture != nil {
// TODO err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid)
err = fmt.Errorf("Picture: not implemented") if err2 != nil {
err = err2
}
} }
return err return err
@ -254,8 +259,8 @@ func (a *Account) eventInternal(event *Event) error {
return err return err
} }
if event.Attachements != nil { if event.Attachments != nil {
for _, file := range event.Attachements { for _, file := range event.Attachments {
mxfile, err := mx.UploadMedia(file) mxfile, err := mx.UploadMedia(file)
if err != nil { if err != nil {
return err return err
@ -270,8 +275,8 @@ func (a *Account) eventInternal(event *Event) error {
content["info"] = map[string]interface{} { content["info"] = map[string]interface{} {
"mimetype": mxfile.Mimetype(), "mimetype": mxfile.Mimetype(),
"size": mxfile.Size(), "size": mxfile.Size(),
"width": sz.Width, "w": sz.Width,
"height": sz.Height, "h": sz.Height,
} }
} else { } else {
content["msgtype"] = "m.file" content["msgtype"] = "m.file"

View file

@ -123,6 +123,9 @@ func handleTxnEvent(e *mxlib.Event) error {
typ := e.Content["msgtype"].(string) typ := e.Content["msgtype"].(string)
if typ == "m.emote" { if typ == "m.emote" {
ev.Type = connector.EVENT_MESSAGE 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 { if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {

View file

@ -115,7 +115,7 @@ type Event struct {
Text string Text string
// Attached files such as images // Attached files such as images
Attachements []MediaObject Attachments []MediaObject
} }
type UserInfo struct { type UserInfo struct {

View file

@ -205,9 +205,17 @@ func (irc *IRC) Send(event *Event) error {
return fmt.Errorf("Invalid target") return fmt.Errorf("Invalid target")
} }
if event.Attachements != nil && len(event.Attachements) > 0 { if event.Attachments != nil && len(event.Attachments) > 0 {
// TODO find a way to send them using some hosing of some kind for _, at := range event.Attachments {
return fmt.Errorf("Attachements not supported on IRC") 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 { if event.Type == EVENT_MESSAGE {

View file

@ -1,10 +1,12 @@
package mattermost package mattermost
import ( import (
"net/http"
"fmt" "fmt"
_ "os" _ "os"
"strings" "strings"
"time" "time"
"io/ioutil"
"encoding/json" "encoding/json"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
@ -210,9 +212,6 @@ func (mm *Mattermost) Leave(roomId RoomID) {
} }
func (mm *Mattermost) Send(event *Event) error { func (mm *Mattermost) Send(event *Event) error {
// TODO: attachements
// TODO: verify private messages work
post := &model.Post{ post := &model.Post{
Message: event.Text, Message: event.Text,
} }
@ -248,8 +247,30 @@ func (mm *Mattermost) Send(event *Event) error {
return fmt.Errorf("Invalid target") 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) _, resp := mm.conn.Client.CreatePost(post)
if resp.Error != nil { if resp.Error != nil {
log.Warnf("CreatePost error: %s", resp.Error)
return resp.Error return resp.Error
} }
return nil return nil
@ -277,16 +298,58 @@ func (mm *Mattermost) handleConnected() {
if len(strings.Split(ch.Name, "__")) == 2 { if len(strings.Split(ch.Name, "__")) == 2 {
continue // This is a DM channel continue // This is a DM channel
} }
id := mm.reverseRoomId(ch.Id) 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.Joined(id)
mm.handler.RoomInfoUpdated(id, UserID(""), &RoomInfo{
Name: chName, // Update room info
room_info := &RoomInfo{
Name: ch.DisplayName,
Topic: ch.Header, 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 { func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
channel_name := msg.Data["channel_name"].(string) channel_name := msg.Data["channel_name"].(string)
post_str := msg.Data["post"].(string) post_str := msg.Data["post"].(string)
@ -326,14 +428,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
return fmt.Errorf("Invalid user") return fmt.Errorf("Invalid user")
} }
userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server)) userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
mm.updateUserInfo(user)
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
}
// Build message event // Build message event
msg_ev := &Event{ msg_ev := &Event{
@ -347,7 +442,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
// Handle files // Handle files
if post.FileIds != nil && len(post.FileIds) > 0 { if post.FileIds != nil && len(post.FileIds) > 0 {
msg_ev.Attachements = []MediaObject{} msg_ev.Attachments = []MediaObject{}
for _, file := range post.Metadata.Files { for _, file := range post.Metadata.Files {
blob, resp := mm.conn.Client.GetFile(file.Id) blob, resp := mm.conn.Client.GetFile(file.Id)
if resp.Error != nil { if resp.Error != nil {
@ -355,7 +450,6 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
} }
media_object := &BlobMediaObject{ media_object := &BlobMediaObject{
ObjectFilename: file.Name, ObjectFilename: file.Name,
ObjectSize: file.Size,
ObjectMimetype: file.MimeType, ObjectMimetype: file.MimeType,
ObjectData: blob, ObjectData: blob,
} }
@ -365,7 +459,7 @@ func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
Height: file.Height, 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") return fmt.Errorf("Invalid channel id")
} }
cache_key := fmt.Sprintf("%s / %s", userId, roomId) mm.ensureJoined(user, roomId)
if _, ok := mm.sentjoinedmap[cache_key]; !ok {
mm.handler.Event(&Event{
Author: userId,
Room: roomId,
Type: EVENT_JOIN,
})
mm.sentjoinedmap[cache_key] = true
}
if post.Type == "system_header_change" { if post.Type == "system_header_change" {
new_header := post.Props["new_header"].(string) new_header := post.Props["new_header"].(string)

View file

@ -97,7 +97,6 @@ func (m *UrlMediaObject) URL() string {
type BlobMediaObject struct { type BlobMediaObject struct {
ObjectFilename string ObjectFilename string
ObjectSize int64
ObjectMimetype string ObjectMimetype string
ObjectImageSize *ImageSize ObjectImageSize *ImageSize
ObjectData []byte ObjectData []byte
@ -108,7 +107,7 @@ func (m *BlobMediaObject) Filename() string {
} }
func (m *BlobMediaObject) Size() int64 { func (m *BlobMediaObject) Size() int64 {
return m.ObjectSize return int64(len(m.ObjectData))
} }
func (m *BlobMediaObject) Mimetype() string { func (m *BlobMediaObject) Mimetype() string {

View file

@ -308,6 +308,19 @@ func (xm *XMPP) Leave(roomId RoomID) {
} }
func (xm *XMPP) Send(event *Event) error { 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) fmt.Printf("xm *XMPP Send %#v\n", event)
if len(event.Recipient) > 0 { if len(event.Recipient) > 0 {
_, err := xm.conn.Send(gxmpp.Chat{ _, err := xm.conn.Send(gxmpp.Chat{

View file

@ -137,10 +137,9 @@ func (mx *Client) ProfileAvatar(userid string, m connector.MediaObject) error {
} }
mxc = mxm mxc = mxm
} }
mxc_uri := fmt.Sprintf("mxc://%s/%s", mxc.MxcServer, mxc.MxcMediaId)
req := ProfileAvatarUrl{ req := ProfileAvatarUrl{
AvatarUrl: mxc_uri, AvatarUrl: mxc.MxcUri(),
} }
var rep struct{} var rep struct{}
err := mx.PutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/avatar_url?user_id=%s", 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) 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 { func (mx *Client) RoomTopicAs(room string, topic string, as_user string) error {
content := map[string]interface{}{ content := map[string]interface{}{
"topic": topic, "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()), mx.Server+"/_matrix/media/r0/upload?filename="+url.QueryEscape(m.Filename()),
reader) reader)
req.Header.Add("Content-Type", m.Mimetype()) 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 var resp UploadResponse
err = mx.DoAndParse(req, &resp) err = mx.DoAndParse(req, &resp)
@ -320,3 +334,23 @@ func (mx *Client) UploadMedia(m connector.MediaObject) (*MediaObject, error) {
return media, nil 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
}

View file

@ -59,3 +59,7 @@ func (m *MediaObject) URL() string {
return fmt.Sprintf("%s/_matrix/media/r0/download/%s/%s/%s", return fmt.Sprintf("%s/_matrix/media/r0/download/%s/%s/%s",
m.mxClient.Server, m.MxcServer, m.MxcMediaId, url.QueryEscape(m.filename)) 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)
}