Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ paths:
If a phone number is provided and a mapping exists, the message is sent on behalf of the mapped Matrix user.
If a phone number is provided but no mapping exists, the phone number is used as the sender ID.

**File Attachments:**
To send files, set `content_type` to `application/x-acro-filetransfer+json` and provide the
file transfer JSON in the `body` field. See the FileTransferMessage schema for the format.
Matrix media URLs (mxc://) must already be uploaded to the Matrix content repository.

Authentication is handled by the Application Service backend; the `password` field is ignored.
requestBody:
required: true
Expand All @@ -87,10 +92,17 @@ paths:
description: Recipient Matrix ID, Alias, or mapped phone number.
body:
type: string
description: The message content.
description: |
The message content. For plain text messages, this is the text body.
For file attachments (when content_type is application/x-acro-filetransfer+json),
this contains the JSON-encoded FileTransferMessage.
content_type:
type: string
default: text/plain
description: |
MIME content type. Supported values:
- `text/plain` (default): Plain text message
- `application/x-acro-filetransfer+json`: File attachment message
disposition_notification:
type: string
description: Opaque string for read receipts.
Expand Down Expand Up @@ -279,10 +291,16 @@ components:
description: Recipient identifier (phone number or user name). Only present in sent messages.
sms_text:
type: string
description: Message body (UTF-8 encoded).
description: |
Message body (UTF-8 encoded). For plain text messages, this is the text content.
For file attachments (when content_type is application/x-acro-filetransfer+json),
this contains a JSON-encoded FileTransferMessage.
content_type:
type: string
description: MIME content-type. Defaults to text/plain if omitted.
description: |
MIME content-type. Values:
- `text/plain` (default): Plain text message, sms_text contains the text
- `application/x-acro-filetransfer+json`: File attachment, sms_text contains FileTransferMessage JSON
disposition_notification:
type: string
description: Opaque string from Send Message request. Can be omitted if empty.
Expand All @@ -292,6 +310,66 @@ components:
stream_id:
type: string
description: Identifier for the conversation stream (Room ID or identifier).
FileTransferMessage:
type: object
description: |
Acrobits file transfer message format used when content_type is application/x-acro-filetransfer+json.
See https://doc.acrobits.net/api/client/x-acro-filetransfer.html
properties:
body:
type: string
description: Optional text message applying to the attachment(s).
attachments:
type: array
description: Array of file attachments.
items:
$ref: '#/components/schemas/Attachment'
required:
- attachments
Attachment:
type: object
description: A single file attachment in the Acrobits file transfer format.
properties:
content-type:
type: string
description: MIME type of the file. If not present, image/jpeg is assumed.
default: image/jpeg
content-url:
type: string
description: URL to download the file content (Matrix mxc:// URL or HTTP URL).
content-size:
type: integer
format: int64
description: Size of the file in bytes. Used to decide download behavior.
filename:
type: string
description: Original filename on the sending device.
description:
type: string
description: Text description for the attachment (not widely used).
encryption-key:
type: string
description: Hex-encoded AES128/192/256 CTR key for decryption. If present, content is encrypted.
hash:
type: string
description: CRC32 digest of the decrypted binary data for integrity verification.
preview:
$ref: '#/components/schemas/AttachmentPreview'
required:
- content-url
AttachmentPreview:
type: object
description: Low quality preview image for an attachment.
properties:
content-type:
type: string
description: MIME type of the preview. If not present, image/jpeg is assumed.
default: image/jpeg
content:
type: string
description: BASE64 encoded preview image data, or URL to thumbnail.
required:
- content
FetchMessagesResponse:
type: object
description: Response from the fetch_messages endpoint following Acrobits Modern API specification.
Expand Down
57 changes: 57 additions & 0 deletions matrix/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,40 @@ func (mc *MatrixClient) ListJoinedRooms(ctx context.Context, userID id.UserID) (
return resp.JoinedRooms, nil
}

// UploadMedia uploads media to the Matrix content repository and returns the MXC URI.
// This follows the Matrix spec: https://spec.matrix.org/v1.2/client-server-api/#content-repository
// The returned URI can be used in message events via the `url` field.
func (mc *MatrixClient) UploadMedia(ctx context.Context, userID id.UserID, contentType string, data []byte) (id.ContentURIString, error) {
mc.mu.Lock()
defer mc.mu.Unlock()

logger.Debug().
Str("user_id", string(userID)).
Str("content_type", contentType).
Int("size", len(data)).
Msg("matrix: uploading media to content repository")

mc.cli.UserID = userID

resp, err := mc.cli.UploadBytes(ctx, data, contentType)
if err != nil {
logger.Error().
Str("user_id", string(userID)).
Str("content_type", contentType).
Err(err).
Msg("matrix: failed to upload media")
return "", fmt.Errorf("upload media: %w", err)
}

contentURI := resp.ContentURI.CUString()
logger.Debug().
Str("user_id", string(userID)).
Str("content_uri", string(contentURI)).
Msg("matrix: media uploaded successfully")

return contentURI, nil
}

// SetPusher registers or updates a push gateway for the specified user.
// This is used to configure Matrix to send push notifications to the proxy's /_matrix/push/v1/notify endpoint.
func (mc *MatrixClient) SetPusher(ctx context.Context, userID id.UserID, req *models.SetPusherRequest) error {
Expand Down Expand Up @@ -247,3 +281,26 @@ func (mc *MatrixClient) SetPusher(ctx context.Context, userID id.UserID, req *mo
Msg("matrix: pusher set successfully")
return nil
}

// ResolveMXC converts an MXC URI (mxc://server/mediaId) to a downloadable HTTP URL.
// It uses the configured homeserver URL to construct the download link.
func (mc *MatrixClient) ResolveMXC(mxcURI string) string {
if !strings.HasPrefix(mxcURI, "mxc://") {
return mxcURI
}

// Parse the MXC URI
// Format: mxc://<server-name>/<media-id>
trimmed := strings.TrimPrefix(mxcURI, "mxc://")
parts := strings.SplitN(trimmed, "/", 2)
if len(parts) != 2 {
return mxcURI
}
serverName := parts[0]
mediaID := parts[1]

// Construct the download URL
// Format: /_matrix/media/v3/download/<server-name>/<media-id>
baseURL := strings.TrimSuffix(mc.homeserverURL, "/")
return fmt.Sprintf("%s/_matrix/media/v3/download/%s/%s", baseURL, serverName, mediaID)
}
72 changes: 72 additions & 0 deletions models/filetransfer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package models

// FileTransferContentType is the MIME type for Acrobits file transfer messages.
const FileTransferContentType = "application/x-acro-filetransfer+json"

// FileTransferMessage represents the Acrobits file transfer format.
// This is used when Content-Type is application/x-acro-filetransfer+json.
// See: https://doc.acrobits.net/api/client/x-acro-filetransfer.html
type FileTransferMessage struct {
// Body is an optional text message applying to the attachment(s).
Body string `json:"body,omitempty"`
// Attachments is the array of individual attachment dictionaries.
Attachments []Attachment `json:"attachments"`
}

// Attachment represents a single file attachment in the Acrobits file transfer format.
type Attachment struct {
// ContentType is optional. If not present, image/jpeg is assumed.
ContentType string `json:"content-type,omitempty"`
// ContentURL is mandatory. It is the location from where to download the data.
ContentURL string `json:"content-url"`
// ContentSize is optional. Used to decide whether to download automatically.
ContentSize int64 `json:"content-size,omitempty"`
// Filename is optional. Original filename on the sending device.
Filename string `json:"filename,omitempty"`
// Description is optional. Text for the particular attachment (not used so far).
Description string `json:"description,omitempty"`
// EncryptionKey is optional. Hex-encoded AES128/192/256 CTR key for decryption.
EncryptionKey string `json:"encryption-key,omitempty"`
// Hash is optional. CRC32 digest of the decrypted binary data.
Hash string `json:"hash,omitempty"`
// Preview is optional. Low quality representation of the data to be downloaded.
Preview *AttachmentPreview `json:"preview,omitempty"`
}

// AttachmentPreview represents a preview image for an attachment.
type AttachmentPreview struct {
// ContentType is optional. If not present, image/jpeg is assumed.
ContentType string `json:"content-type,omitempty"`
// Content is mandatory. BASE64 representation of the preview image.
Content string `json:"content"`
}

// IsImageContentType checks if the content type is an image type.
func IsImageContentType(contentType string) bool {
switch contentType {
case "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/bmp", "image/tiff":
return true
default:
return false
}
}

// IsVideoContentType checks if the content type is a video type.
func IsVideoContentType(contentType string) bool {
switch contentType {
case "video/mp4", "video/webm", "video/ogg", "video/quicktime", "video/x-msvideo":
return true
default:
return false
}
}

// IsAudioContentType checks if the content type is an audio type.
func IsAudioContentType(contentType string) bool {
switch contentType {
case "audio/mpeg", "audio/mp3", "audio/ogg", "audio/wav", "audio/webm", "audio/aac", "audio/flac":
return true
default:
return false
}
}
Loading