|
5 | 5 | "encoding/json" |
6 | 6 | "errors" |
7 | 7 | "fmt" |
| 8 | + "io" |
| 9 | + "net/http" |
8 | 10 | "os" |
9 | 11 | "strconv" |
10 | 12 | "strings" |
@@ -216,51 +218,98 @@ func (s *MessageService) SendMessage(ctx context.Context, req *models.SendMessag |
216 | 218 | Body: ftMsg.Body, |
217 | 219 | } |
218 | 220 | } else { |
219 | | - // Convert to Matrix event content |
| 221 | + // Download attachments from Acrobits and upload to Matrix content repository |
220 | 222 | msgType, rawContent, err := models.FileTransferToMatrixEventContent(ftMsg) |
221 | 223 | if err != nil { |
222 | 224 | logger.Warn().Err(err).Msg("failed to convert file transfer to Matrix format") |
223 | 225 | return nil, fmt.Errorf("failed to convert file transfer: %w", err) |
224 | 226 | } |
225 | 227 |
|
226 | | - // Build the Matrix event content |
227 | | - content = &event.MessageEventContent{ |
228 | | - MsgType: event.MessageType(msgType), |
229 | | - Body: rawContent["body"].(string), |
| 228 | + // Download and upload the main attachment |
| 229 | + acrobitsURL := "" |
| 230 | + if url, ok := rawContent["url"].(string); ok { |
| 231 | + acrobitsURL = url |
230 | 232 | } |
231 | 233 |
|
232 | | - // Set the URL for media messages |
233 | | - if url, ok := rawContent["url"].(string); ok { |
234 | | - content.URL = id.ContentURIString(url) |
| 234 | + mimetype := "" |
| 235 | + if info, ok := rawContent["info"].(map[string]interface{}); ok { |
| 236 | + if mt, ok := info["mimetype"].(string); ok { |
| 237 | + mimetype = mt |
| 238 | + } |
235 | 239 | } |
236 | 240 |
|
237 | | - // Set the filename if present |
238 | | - if filename, ok := rawContent["filename"].(string); ok { |
239 | | - content.FileName = filename |
| 241 | + matrixURL := "" |
| 242 | + uploadSuccess := true |
| 243 | + if acrobitsURL != "" { |
| 244 | + logger.Debug().Str("content_url", acrobitsURL).Msg("downloading attachment from Acrobits") |
| 245 | + fileData, err := s.downloadFile(ctx, acrobitsURL) |
| 246 | + if err != nil { |
| 247 | + logger.Warn().Err(err).Str("content_url", acrobitsURL).Msg("failed to download attachment, falling back to text message") |
| 248 | + // Fallback: send as text message only |
| 249 | + content = &event.MessageEventContent{ |
| 250 | + MsgType: event.MsgText, |
| 251 | + Body: ftMsg.Body, |
| 252 | + } |
| 253 | + uploadSuccess = false |
| 254 | + } else { |
| 255 | + // Upload to Matrix content repository |
| 256 | + logger.Debug().Str("content_url", acrobitsURL).Int("size", len(fileData)).Msg("uploading attachment to Matrix content repository") |
| 257 | + uploadedURL, err := s.matrixClient.UploadMedia(ctx, senderMatrix, mimetype, fileData) |
| 258 | + if err != nil { |
| 259 | + logger.Warn().Err(err).Str("content_url", acrobitsURL).Msg("failed to upload attachment to Matrix, falling back to text message") |
| 260 | + // Fallback: send as text message only |
| 261 | + content = &event.MessageEventContent{ |
| 262 | + MsgType: event.MsgText, |
| 263 | + Body: ftMsg.Body, |
| 264 | + } |
| 265 | + uploadSuccess = false |
| 266 | + } else { |
| 267 | + matrixURL = string(uploadedURL) |
| 268 | + logger.Debug().Str("matrix_url", matrixURL).Str("content_url", acrobitsURL).Msg("attachment uploaded to Matrix content repository") |
| 269 | + } |
| 270 | + } |
240 | 271 | } |
241 | 272 |
|
242 | | - // Set info block if present |
243 | | - if info, ok := rawContent["info"].(map[string]interface{}); ok { |
244 | | - content.Info = &event.FileInfo{} |
245 | | - if mimetype, ok := info["mimetype"].(string); ok { |
246 | | - content.Info.MimeType = mimetype |
| 273 | + if uploadSuccess { |
| 274 | + // Build the Matrix event content |
| 275 | + content = &event.MessageEventContent{ |
| 276 | + MsgType: event.MessageType(msgType), |
| 277 | + Body: rawContent["body"].(string), |
247 | 278 | } |
248 | | - if size, ok := info["size"].(int64); ok { |
249 | | - content.Info.Size = int(size) |
| 279 | + |
| 280 | + // Set the URL for media messages (use uploaded Matrix URL) |
| 281 | + if matrixURL != "" { |
| 282 | + content.URL = id.ContentURIString(matrixURL) |
250 | 283 | } |
251 | | - // Handle thumbnail info |
252 | | - if thumbnailURL, ok := info["thumbnail_url"].(string); ok { |
253 | | - content.Info.ThumbnailURL = id.ContentURIString(thumbnailURL) |
| 284 | + |
| 285 | + // Set the filename if present |
| 286 | + if filename, ok := rawContent["filename"].(string); ok { |
| 287 | + content.FileName = filename |
254 | 288 | } |
255 | | - if thumbnailInfo, ok := info["thumbnail_info"].(map[string]interface{}); ok { |
256 | | - content.Info.ThumbnailInfo = &event.FileInfo{} |
257 | | - if tm, ok := thumbnailInfo["mimetype"].(string); ok { |
258 | | - content.Info.ThumbnailInfo.MimeType = tm |
| 289 | + |
| 290 | + // Set info block if present |
| 291 | + if info, ok := rawContent["info"].(map[string]interface{}); ok { |
| 292 | + content.Info = &event.FileInfo{} |
| 293 | + if mimetype, ok := info["mimetype"].(string); ok { |
| 294 | + content.Info.MimeType = mimetype |
| 295 | + } |
| 296 | + if size, ok := info["size"].(int64); ok { |
| 297 | + content.Info.Size = int(size) |
| 298 | + } |
| 299 | + // Handle thumbnail info |
| 300 | + if thumbnailURL, ok := info["thumbnail_url"].(string); ok { |
| 301 | + content.Info.ThumbnailURL = id.ContentURIString(thumbnailURL) |
| 302 | + } |
| 303 | + if thumbnailInfo, ok := info["thumbnail_info"].(map[string]interface{}); ok { |
| 304 | + content.Info.ThumbnailInfo = &event.FileInfo{} |
| 305 | + if tm, ok := thumbnailInfo["mimetype"].(string); ok { |
| 306 | + content.Info.ThumbnailInfo.MimeType = tm |
| 307 | + } |
259 | 308 | } |
260 | 309 | } |
261 | | - } |
262 | 310 |
|
263 | | - logger.Debug().Str("msg_type", msgType).Str("url", string(content.URL)).Msg("converted file transfer to Matrix media message") |
| 311 | + logger.Debug().Str("msg_type", msgType).Str("matrix_url", matrixURL).Msg("converted file transfer to Matrix media message") |
| 312 | + } |
264 | 313 | } |
265 | 314 | } else { |
266 | 315 | // Regular text message |
@@ -887,3 +936,41 @@ func (s *MessageService) clearBatchToken(userID string) { |
887 | 936 | defer s.mu.Unlock() |
888 | 937 | delete(s.batchTokens, userID) |
889 | 938 | } |
| 939 | + |
| 940 | +// downloadFile downloads a file from the given URL with a context timeout. |
| 941 | +// Returns the file contents as bytes, or an error if the download fails. |
| 942 | +func (s *MessageService) downloadFile(ctx context.Context, url string) ([]byte, error) { |
| 943 | + // Create a new request with context |
| 944 | + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) |
| 945 | + if err != nil { |
| 946 | + return nil, fmt.Errorf("failed to create download request: %w", err) |
| 947 | + } |
| 948 | + |
| 949 | + // Execute the request |
| 950 | + client := &http.Client{Timeout: 30 * time.Second} |
| 951 | + resp, err := client.Do(req) |
| 952 | + if err != nil { |
| 953 | + return nil, fmt.Errorf("failed to download file: %w", err) |
| 954 | + } |
| 955 | + defer resp.Body.Close() |
| 956 | + |
| 957 | + // Check response status |
| 958 | + if resp.StatusCode < 200 || resp.StatusCode >= 300 { |
| 959 | + return nil, fmt.Errorf("download failed with status %d", resp.StatusCode) |
| 960 | + } |
| 961 | + |
| 962 | + // Read the response body with a reasonable size limit (100MB) |
| 963 | + const maxSize = 100 * 1024 * 1024 // 100MB |
| 964 | + limitedReader := io.LimitReader(resp.Body, maxSize+1) |
| 965 | + data, err := io.ReadAll(limitedReader) |
| 966 | + if err != nil { |
| 967 | + return nil, fmt.Errorf("failed to read file: %w", err) |
| 968 | + } |
| 969 | + |
| 970 | + if len(data) > maxSize { |
| 971 | + return nil, fmt.Errorf("file too large: %d bytes exceeds limit of %d bytes", len(data), maxSize) |
| 972 | + } |
| 973 | + |
| 974 | + logger.Debug().Str("url", url).Int("size", len(data)).Int("status", resp.StatusCode).Msg("file downloaded successfully") |
| 975 | + return data, nil |
| 976 | +} |
0 commit comments