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 {
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"

View file

@ -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 {

View file

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

View file

@ -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 {

View file

@ -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)

View file

@ -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 {

View file

@ -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{

View file

@ -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
}

View file

@ -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)
}