gomuks/matrix/matrix.go

684 lines
19 KiB
Go
Raw Normal View History

// gomuks - A terminal Matrix client written in Go.
2019-01-17 13:13:25 +01:00
// Copyright (C) 2019 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
2019-01-17 13:13:25 +01:00
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2019-01-17 13:13:25 +01:00
// GNU Affero General Public License for more details.
//
2019-01-17 13:13:25 +01:00
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2018-03-18 20:24:03 +01:00
package matrix
import (
"bytes"
2019-01-17 13:13:25 +01:00
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
2018-03-14 23:14:39 +01:00
"time"
2019-01-17 13:13:25 +01:00
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/format"
"maunium.net/go/gomuks/config"
"maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/pushrules"
"maunium.net/go/gomuks/matrix/rooms"
)
2018-11-13 23:00:35 +01:00
// Container is a wrapper for a mautrix Client and some other stuff.
2018-03-21 22:29:58 +01:00
//
// It is used for all Matrix calls from the UI and Matrix event handlers.
2018-03-18 20:24:03 +01:00
type Container struct {
2018-11-13 23:00:35 +01:00
client *mautrix.Client
syncer *GomuksSyncer
2018-03-18 20:24:03 +01:00
gmx ifc.Gomuks
ui ifc.GomuksUI
config *config.Config
history *HistoryManager
running bool
stop chan bool
2018-03-14 23:14:39 +01:00
typing int64
}
2018-03-21 22:29:58 +01:00
// NewContainer creates a new Container for the given Gomuks instance.
func NewContainer(gmx ifc.Gomuks) *Container {
2018-03-18 20:24:03 +01:00
c := &Container{
2018-03-13 20:58:43 +01:00
config: gmx.Config(),
ui: gmx.UI(),
gmx: gmx,
}
2018-03-13 20:58:43 +01:00
return c
}
2018-11-13 23:00:35 +01:00
// Client returns the underlying mautrix Client.
func (c *Container) Client() *mautrix.Client {
return c.client
}
2018-11-13 23:00:35 +01:00
type mxLogger struct{}
func (log mxLogger) Debugfln(message string, args ...interface{}) {
debug.Printf("[Matrix] "+message, args...)
}
// InitClient initializes the mautrix client and connects to the homeserver specified in the config.
2018-03-18 20:24:03 +01:00
func (c *Container) InitClient() error {
if len(c.config.HS) == 0 {
return fmt.Errorf("no homeserver in config")
}
2018-03-13 20:58:43 +01:00
if c.client != nil {
c.Stop()
c.client = nil
}
var mxid, accessToken string
if len(c.config.AccessToken) > 0 {
accessToken = c.config.AccessToken
2018-03-21 22:29:58 +01:00
mxid = c.config.UserID
}
var err error
2018-11-13 23:00:35 +01:00
c.client, err = mautrix.NewClient(c.config.HS, mxid, accessToken)
if err != nil {
return err
}
2018-11-13 23:00:35 +01:00
c.client.Logger = mxLogger{}
c.history, err = NewHistoryManager(c.config.HistoryPath)
if err != nil {
return err
}
allowInsecure := len(os.Getenv("GOMUKS_ALLOW_INSECURE_CONNECTIONS")) > 0
if allowInsecure {
c.client.Client = &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
}
}
c.stop = make(chan bool, 1)
if len(accessToken) > 0 {
go c.Start()
}
return nil
}
2018-11-13 23:00:35 +01:00
// Initialized returns whether or not the mautrix client is initialized (see InitClient())
2018-03-18 20:24:03 +01:00
func (c *Container) Initialized() bool {
2018-03-13 20:58:43 +01:00
return c.client != nil
}
2018-03-21 22:29:58 +01:00
// Login sends a password login request with the given username and password.
2018-03-18 20:24:03 +01:00
func (c *Container) Login(user, password string) error {
2018-11-13 23:00:35 +01:00
resp, err := c.client.Login(&mautrix.ReqLogin{
2018-04-16 20:30:34 +02:00
Type: "m.login.password",
User: user,
Password: password,
InitialDeviceDisplayName: "gomuks",
})
if err != nil {
return err
}
2018-03-13 20:58:43 +01:00
c.client.SetCredentials(resp.UserID, resp.AccessToken)
2018-03-21 22:29:58 +01:00
c.config.UserID = resp.UserID
c.config.AccessToken = resp.AccessToken
c.config.Save()
go c.Start()
return nil
}
2018-05-10 14:47:24 +02:00
// Logout revokes the access token, stops the syncer and calls the OnLogout() method of the UI.
func (c *Container) Logout() {
c.client.Logout()
c.config.DeleteSession()
c.Stop()
c.client = nil
c.ui.OnLogout()
}
2018-03-21 22:29:58 +01:00
// Stop stops the Matrix syncer.
2018-03-18 20:24:03 +01:00
func (c *Container) Stop() {
2018-03-15 20:28:21 +01:00
if c.running {
2018-04-24 15:51:40 +02:00
debug.Print("Stopping Matrix container...")
2018-03-15 20:28:21 +01:00
c.stop <- true
c.client.StopSync()
debug.Print("Closing history manager...")
err := c.history.Close()
if err != nil {
debug.Print("Error closing history manager:", err)
}
2018-03-15 20:28:21 +01:00
}
2018-03-13 20:58:43 +01:00
}
2018-03-21 22:29:58 +01:00
// UpdatePushRules fetches the push notification rules from the server and stores them in the current Session object.
func (c *Container) UpdatePushRules() {
debug.Print("Updating push rules...")
2018-03-21 22:29:58 +01:00
resp, err := pushrules.GetPushRules(c.client)
if err != nil {
debug.Print("Failed to fetch push rules:", err)
}
c.config.PushRules = resp
c.config.SavePushRules()
}
2018-03-21 22:29:58 +01:00
// PushRules returns the push notification rules. If no push rules are cached, UpdatePushRules() will be called first.
func (c *Container) PushRules() *pushrules.PushRuleset {
if c.config.PushRules == nil {
c.UpdatePushRules()
}
return c.config.PushRules
}
2018-11-13 23:00:35 +01:00
var AccountDataGomuksPreferences = mautrix.NewEventType("net.maunium.gomuks.preferences")
2018-03-21 22:29:58 +01:00
// OnLogin initializes the syncer and updates the room list.
2018-03-18 20:24:03 +01:00
func (c *Container) OnLogin() {
2018-05-01 18:17:57 +02:00
c.ui.OnLogin()
c.client.Store = c.config
2018-03-13 20:58:43 +01:00
2018-04-24 15:51:40 +02:00
debug.Print("Initializing syncer")
c.syncer = NewGomuksSyncer(c.config)
2018-11-13 23:00:35 +01:00
c.syncer.OnEventType(mautrix.EventMessage, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateAliases, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateCanonicalAlias, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateTopic, c.HandleMessage)
c.syncer.OnEventType(mautrix.StateRoomName, c.HandleMessage)
2018-11-13 23:00:35 +01:00
c.syncer.OnEventType(mautrix.StateMember, c.HandleMembership)
c.syncer.OnEventType(mautrix.EphemeralEventReceipt, c.HandleReadReceipt)
c.syncer.OnEventType(mautrix.EphemeralEventTyping, c.HandleTyping)
c.syncer.OnEventType(mautrix.AccountDataDirectChats, c.HandleDirectChatInfo)
c.syncer.OnEventType(mautrix.AccountDataPushRules, c.HandlePushRules)
c.syncer.OnEventType(mautrix.AccountDataRoomTags, c.HandleTag)
c.syncer.OnEventType(AccountDataGomuksPreferences, c.HandlePreferences)
2018-04-24 16:12:08 +02:00
c.syncer.InitDoneCallback = func() {
2018-11-13 23:00:35 +01:00
debug.Print("Initial sync done")
c.config.AuthCache.InitialSyncDone = true
c.config.SaveAuthCache()
c.ui.MainView().InitialSyncDone()
2018-04-24 16:12:08 +02:00
c.ui.Render()
}
c.client.Syncer = c.syncer
2018-04-24 15:51:40 +02:00
debug.Print("Setting existing rooms")
c.ui.MainView().SetRooms(c.config.Rooms)
2018-04-24 15:51:40 +02:00
debug.Print("OnLogin() done.")
}
2018-03-21 22:29:58 +01:00
// Start moves the UI to the main view, calls OnLogin() and runs the syncer forever until stopped with Stop()
2018-03-18 20:24:03 +01:00
func (c *Container) Start() {
defer debug.Recover()
c.OnLogin()
2018-03-20 11:16:32 +01:00
if c.client == nil {
return
}
2018-03-18 20:24:03 +01:00
debug.Print("Starting sync...")
c.running = true
for {
select {
case <-c.stop:
2018-03-18 20:24:03 +01:00
debug.Print("Stopping sync...")
c.running = false
return
default:
2018-03-13 20:58:43 +01:00
if err := c.client.Sync(); err != nil {
2018-11-13 23:00:35 +01:00
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.Code == http.StatusUnauthorized {
2018-05-10 14:47:24 +02:00
debug.Print("Sync() errored with ", err, " -> logging out")
c.Logout()
} else {
debug.Print("Sync() errored", err)
}
2018-03-13 20:58:43 +01:00
} else {
2018-03-18 20:24:03 +01:00
debug.Print("Sync() returned without error")
}
}
}
}
2018-11-13 23:00:35 +01:00
func (c *Container) HandlePreferences(source EventSource, evt *mautrix.Event) {
if source&EventSourceAccountData == 0 {
return
}
orig := c.config.Preferences
err := json.Unmarshal(evt.Content.VeryRaw, &c.config.Preferences)
if err != nil {
debug.Print("Failed to parse updated preferences:", err)
return
}
debug.Print("Updated preferences:", orig, "->", c.config.Preferences)
c.ui.HandleNewPreferences()
}
func (c *Container) SendPreferencesToMatrix() {
defer debug.Recover()
debug.Print("Sending updated preferences:", c.config.Preferences)
u := c.client.BuildURL("user", c.config.UserID, "account_data", "net.maunium.gomuks.preferences")
_, err := c.client.MakeRequest("PUT", u, &c.config.Preferences, nil)
if err != nil {
debug.Print("Failed to update preferences:", err)
}
}
2018-03-21 22:29:58 +01:00
// HandleMessage is the event handler for the m.room.message timeline event.
2018-11-13 23:00:35 +01:00
func (c *Container) HandleMessage(source EventSource, evt *mautrix.Event) {
if source&EventSourceLeave != 0 || source&EventSourceState != 0 {
return
}
mainView := c.ui.MainView()
2018-04-24 15:51:40 +02:00
roomView := mainView.GetRoom(evt.RoomID)
if roomView == nil {
2018-04-24 15:51:40 +02:00
debug.Printf("Failed to handle event %v: No room view found.", evt)
return
}
err := c.history.Append(roomView.MxRoom(), []*mautrix.Event{evt})
if err != nil {
debug.Printf("Failed to add event %s to history: %v", evt.ID, err)
}
// TODO switch to roomView.AddEvent
message := mainView.ParseEvent(roomView, evt)
if message != nil {
2018-04-24 15:51:40 +02:00
roomView.AddMessage(message, ifc.AppendMessage)
roomView.MxRoom().LastReceivedMessage = message.Timestamp()
if c.syncer.FirstSyncDone {
pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should()
mainView.NotifyMessage(roomView.MxRoom(), message, pushRules)
2018-04-24 15:51:40 +02:00
c.ui.Render()
}
2018-04-24 15:51:40 +02:00
} else {
debug.Printf("Parsing event %s type %s %v from %s in %s failed (ParseEvent() returned nil).", evt.ID, evt.Type, evt.Content.Raw, evt.Sender, evt.RoomID)
}
2018-03-13 20:58:43 +01:00
}
2018-05-22 16:23:54 +02:00
// HandleMembership is the event handler for the m.room.member state event.
2018-11-13 23:00:35 +01:00
func (c *Container) HandleMembership(source EventSource, evt *mautrix.Event) {
2018-05-22 16:23:54 +02:00
isLeave := source&EventSourceLeave != 0
isTimeline := source&EventSourceTimeline != 0
isNonTimelineLeave := isLeave && !isTimeline
if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave {
return
} else if evt.StateKey != nil && *evt.StateKey == c.config.UserID {
c.processOwnMembershipChange(evt)
} else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) {
// We don't care about other users' membership events in the initial sync or chats we've left.
return
}
c.HandleMessage(source, evt)
}
2018-11-13 23:00:35 +01:00
func (c *Container) processOwnMembershipChange(evt *mautrix.Event) {
membership := evt.Content.Membership
2018-11-13 23:00:35 +01:00
prevMembership := mautrix.MembershipLeave
2018-05-22 16:23:54 +02:00
if evt.Unsigned.PrevContent != nil {
prevMembership = evt.Unsigned.PrevContent.Membership
2018-05-22 16:23:54 +02:00
}
debug.Printf("Processing own membership change: %s->%s in %s", prevMembership, membership, evt.RoomID)
if membership == prevMembership {
return
}
room := c.GetRoom(evt.RoomID)
switch membership {
case "join":
c.ui.MainView().AddRoom(room)
room.HasLeft = false
case "leave":
c.ui.MainView().RemoveRoom(room)
room.HasLeft = true
case "invite":
// TODO handle
debug.Printf("%s invited the user to %s", evt.Sender, evt.RoomID)
}
}
2018-11-13 23:00:35 +01:00
func (c *Container) parseReadReceipt(evt *mautrix.Event) (largestTimestampEvent string) {
var largestTimestamp int64
for eventID, rawContent := range evt.Content.Raw {
content, ok := rawContent.(map[string]interface{})
if !ok {
continue
}
mRead, ok := content["m.read"].(map[string]interface{})
if !ok {
continue
}
myInfo, ok := mRead[c.config.UserID].(map[string]interface{})
if !ok {
continue
}
ts, ok := myInfo["ts"].(float64)
if int64(ts) > largestTimestamp {
largestTimestamp = int64(ts)
largestTimestampEvent = eventID
}
}
return
}
2018-11-13 23:00:35 +01:00
func (c *Container) HandleReadReceipt(source EventSource, evt *mautrix.Event) {
if source&EventSourceLeave != 0 {
return
}
lastReadEvent := c.parseReadReceipt(evt)
if len(lastReadEvent) == 0 {
return
}
room := c.GetRoom(evt.RoomID)
room.MarkRead(lastReadEvent)
c.ui.Render()
}
2018-11-13 23:00:35 +01:00
func (c *Container) parseDirectChatInfo(evt *mautrix.Event) map[*rooms.Room]bool {
directChats := make(map[*rooms.Room]bool)
for _, rawRoomIDList := range evt.Content.Raw {
roomIDList, ok := rawRoomIDList.([]interface{})
if !ok {
continue
}
for _, rawRoomID := range roomIDList {
roomID, ok := rawRoomID.(string)
if !ok {
continue
}
room := c.GetRoom(roomID)
if room != nil && !room.HasLeft {
directChats[room] = true
}
}
}
return directChats
}
2018-11-13 23:00:35 +01:00
func (c *Container) HandleDirectChatInfo(source EventSource, evt *mautrix.Event) {
directChats := c.parseDirectChatInfo(evt)
for _, room := range c.config.Rooms {
shouldBeDirect := directChats[room]
if shouldBeDirect != room.IsDirect {
room.IsDirect = shouldBeDirect
c.ui.MainView().UpdateTags(room)
}
}
}
2018-03-21 22:29:58 +01:00
// HandlePushRules is the event handler for the m.push_rules account data event.
2018-11-13 23:00:35 +01:00
func (c *Container) HandlePushRules(source EventSource, evt *mautrix.Event) {
debug.Print("Received updated push rules")
var err error
c.config.PushRules, err = pushrules.EventToPushRules(evt)
if err != nil {
debug.Print("Failed to convert event to push rules:", err)
return
}
c.config.SavePushRules()
}
2018-03-25 19:30:34 +02:00
// HandleTag is the event handler for the m.tag account data event.
2018-11-13 23:00:35 +01:00
func (c *Container) HandleTag(source EventSource, evt *mautrix.Event) {
room := c.config.GetRoom(evt.RoomID)
2018-04-24 01:13:17 +02:00
newTags := make([]rooms.RoomTag, len(evt.Content.RoomTags))
2018-04-24 01:13:17 +02:00
index := 0
for tag, info := range evt.Content.RoomTags {
order := "0.5"
if len(info.Order) > 0 {
2018-11-13 23:00:35 +01:00
order = info.Order.String()
}
2018-04-24 01:13:17 +02:00
newTags[index] = rooms.RoomTag{
Tag: tag,
Order: order,
}
index++
}
mainView := c.ui.MainView()
room.RawTags = newTags
mainView.UpdateTags(room)
2018-03-25 19:30:34 +02:00
}
2018-03-21 22:29:58 +01:00
// HandleTyping is the event handler for the m.typing event.
2018-11-13 23:00:35 +01:00
func (c *Container) HandleTyping(source EventSource, evt *mautrix.Event) {
c.ui.MainView().SetTyping(evt.RoomID, evt.Content.TypingUserIDs)
2018-03-14 23:14:39 +01:00
}
func (c *Container) MarkRead(roomID, eventID string) {
urlPath := c.client.BuildURL("rooms", roomID, "receipt", "m.read", eventID)
c.client.MakeRequest("POST", urlPath, struct{}{}, nil)
}
2018-03-21 22:29:58 +01:00
// SendMessage sends a message with the given text to the given room.
2018-11-13 23:00:35 +01:00
func (c *Container) SendMessage(roomID string, msgtype mautrix.MessageType, text string) (string, error) {
defer debug.Recover()
2018-03-15 18:45:52 +01:00
c.SendTyping(roomID, false)
2018-11-13 23:00:35 +01:00
resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage,
mautrix.Content{MsgType: msgtype, Body: text})
if err != nil {
return "", err
}
return resp.EventID, nil
}
2018-03-14 23:14:39 +01:00
var mentionRegex = regexp.MustCompile("\\[(.+?)]\\(https://matrix.to/#/@.+?:.+?\\)")
var roomRegex = regexp.MustCompile("\\[.+?]\\(https://matrix.to/#/(#.+?:[^/]+?)\\)")
// SendMarkdownMessage sends a message with the given markdown text to the given room.
2018-11-13 23:00:35 +01:00
func (c *Container) SendMarkdownMessage(roomID string, msgtype mautrix.MessageType, text string) (string, error) {
defer debug.Recover()
2019-03-28 22:28:27 +01:00
content := format.RenderMarkdown(text)
content.MsgType = msgtype
// Remove markdown link stuff from plaintext mentions and room links
2019-03-28 22:28:27 +01:00
content.Body = mentionRegex.ReplaceAllString(content.Body, "$1")
content.Body = roomRegex.ReplaceAllString(content.Body, "$1")
c.SendTyping(roomID, false)
roomView := c.ui.MainView().GetRoom(roomID)
txnID := c.client.TxnID()
localEcho := roomView.ParseEvent(&mautrix.Event{
Sender: c.config.UserID,
Type: mautrix.EventMessage,
Timestamp: time.Now().UnixNano() / 1e6,
RoomID: roomID,
Content: content,
Unsigned: mautrix.Unsigned{
TransactionID: txnID,
OutgoingState: mautrix.EventStateLocalEcho,
},
})
roomView.AppendMessage(localEcho)
resp, err := c.client.SendMessageEvent(roomID, mautrix.EventMessage, content, mautrix.ReqSendEvent{TransactionID: txnID})
if err != nil {
roomView.MarkMessageFailed(localEcho, err)
return "", err
}
return resp.EventID, nil
}
2018-03-21 22:29:58 +01:00
// SendTyping sets whether or not the user is typing in the given room.
2018-03-18 20:24:03 +01:00
func (c *Container) SendTyping(roomID string, typing bool) {
defer debug.Recover()
2018-03-18 20:24:03 +01:00
ts := time.Now().Unix()
if c.typing > ts && typing {
2018-03-14 23:14:39 +01:00
return
}
2018-03-15 18:45:52 +01:00
if typing {
2018-03-21 22:29:58 +01:00
c.client.UserTyping(roomID, true, 20000)
c.typing = ts + 15
2018-03-15 18:45:52 +01:00
} else {
c.client.UserTyping(roomID, false, 0)
c.typing = 0
}
2018-03-14 23:14:39 +01:00
}
2018-03-21 22:29:58 +01:00
// JoinRoom makes the current user try to join the given room.
2018-05-10 19:07:24 +02:00
func (c *Container) JoinRoom(roomID, server string) (*rooms.Room, error) {
resp, err := c.client.JoinRoom(roomID, server, nil)
2018-03-18 20:24:03 +01:00
if err != nil {
return nil, err
2018-03-18 20:24:03 +01:00
}
room := c.GetRoom(resp.RoomID)
room.HasLeft = false
return room, nil
2018-03-18 20:24:03 +01:00
}
2018-03-21 22:29:58 +01:00
// LeaveRoom makes the current user leave the given room.
2018-03-18 20:24:03 +01:00
func (c *Container) LeaveRoom(roomID string) error {
_, err := c.client.LeaveRoom(roomID)
2018-03-16 15:24:11 +01:00
if err != nil {
return err
}
room := c.GetRoom(roomID)
room.HasLeft = true
2018-03-16 15:24:11 +01:00
return nil
}
2018-03-21 22:29:58 +01:00
// GetHistory fetches room history.
func (c *Container) GetHistory(room *rooms.Room, limit int) ([]*mautrix.Event, error) {
events, err := c.history.Load(room, limit)
if err != nil {
return nil, err
}
if len(events) > 0 {
debug.Printf("Loaded %d events for %s from local cache", len(events), room.ID)
return events, nil
}
resp, err := c.client.Messages(room.ID, room.PrevBatch, "", 'b', limit)
if err != nil {
return nil, err
}
if len(resp.Chunk) > 0 {
err = c.history.Prepend(room, resp.Chunk)
if err != nil {
return nil, err
}
}
room.PrevBatch = resp.End
debug.Printf("Loaded %d events for %s from server from %s to %s", len(resp.Chunk), room.ID, resp.Start, resp.End)
return resp.Chunk, nil
}
func (c *Container) GetEvent(room *rooms.Room, eventID string) (*mautrix.Event, error) {
event, err := c.history.Get(room, eventID)
if event != nil || err != nil {
debug.Printf("Found event %s in local cache", eventID)
return event, err
}
event, err = c.client.GetEvent(room.ID, eventID)
if err != nil {
return nil, err
}
debug.Printf("Loaded event %s from server", eventID)
return event, nil
}
2018-03-21 22:29:58 +01:00
// GetRoom gets the room instance stored in the session.
2018-03-18 20:24:03 +01:00
func (c *Container) GetRoom(roomID string) *rooms.Room {
return c.config.GetRoom(roomID)
2018-03-14 23:14:39 +01:00
}
var mxcRegex = regexp.MustCompile("mxc://(.+)/(.+)")
2018-05-01 18:17:57 +02:00
// Download fetches the given Matrix content (mxc) URL and returns the data, homeserver, file ID and potential errors.
//
// The file will be either read from the media cache (if found) or downloaded from the server.
func (c *Container) Download(mxcURL string) (data []byte, hs, id string, err error) {
parts := mxcRegex.FindStringSubmatch(mxcURL)
if parts == nil || len(parts) != 3 {
err = fmt.Errorf("invalid matrix content URL")
return
}
2018-04-15 13:03:05 +02:00
hs = parts[1]
id = parts[2]
cacheFile := c.GetCachePath(hs, id)
var info os.FileInfo
if info, err = os.Stat(cacheFile); err == nil && !info.IsDir() {
data, err = ioutil.ReadFile(cacheFile)
if err == nil {
return
}
}
2018-04-15 13:03:05 +02:00
data, err = c.download(hs, id, cacheFile)
return
}
func (c *Container) GetDownloadURL(hs, id string) string {
dlURL, _ := url.Parse(c.client.HomeserverURL.String())
dlURL.Path = path.Join(dlURL.Path, "/_matrix/media/v1/download", hs, id)
return dlURL.String()
}
func (c *Container) download(hs, id, cacheFile string) (data []byte, err error) {
var resp *http.Response
resp, err = c.client.Client.Get(c.GetDownloadURL(hs, id))
if err != nil {
return
}
defer resp.Body.Close()
var buf bytes.Buffer
_, err = io.Copy(&buf, resp.Body)
if err != nil {
return
}
data = buf.Bytes()
err = ioutil.WriteFile(cacheFile, data, 0600)
return
}
2018-05-01 18:17:57 +02:00
// GetCachePath gets the path to the cached version of the given homeserver:fileID combination.
// The file may or may not exist, use Download() to ensure it has been cached.
func (c *Container) GetCachePath(homeserver, fileID string) string {
dir := filepath.Join(c.config.MediaDir, homeserver)
err := os.MkdirAll(dir, 0700)
if err != nil {
return ""
}
return filepath.Join(dir, fileID)
}