matrix: file transfer both ways — send-file one-shot (upload + m.image/m.file by ext), incoming attachments downloaded to /tmp/matrix-files and path forwarded to agent
👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code <noreply@letta.com>
This commit is contained in:
1 parent
dcf6d8df75
commit
e212e3e120
2 files changed
+308
-6
No files matched your search
+148
-2
@@ -53,6 +53,122 @@ fn http(alloc: std.mem.Allocator, io: Io, method: []const u8, url: []const u8, t
|
||||
|
||||
pub const Session = struct { token: []u8, user_id: []u8 }; // caller frees both
|
||||
|
||||
/// HTTP with a raw (non-JSON) body + explicit content type — media upload.
|
||||
/// Returns body (allocated; caller frees).
|
||||
fn httpRaw(alloc: std.mem.Allocator, io: Io, method: []const u8, url: []const u8, token: ?[]const u8, content_type: []const u8, file_path: ?[]const u8) ![]u8 {
|
||||
// Arena for argv/header scratch — freed wholesale at scope exit (unlike
|
||||
// http(), which leaks its header string per call).
|
||||
var arena_state = std.heap.ArenaAllocator.init(alloc);
|
||||
defer arena_state.deinit();
|
||||
const a = arena_state.allocator();
|
||||
|
||||
var argv: std.ArrayList([]const u8) = .empty;
|
||||
defer argv.deinit(a); // buffer lives in the arena — no-op free
|
||||
try argv.appendSlice(a, &.{ "curl", "-sS", "-m", "120", "-X", method });
|
||||
if (token) |t| {
|
||||
const h = try std.fmt.allocPrint(a, "Authorization: Bearer {s}", .{t});
|
||||
try argv.appendSlice(a, &.{ "-H", h });
|
||||
}
|
||||
if (content_type.len > 0) {
|
||||
const ct = try std.fmt.allocPrint(a, "Content-Type: {s}", .{content_type});
|
||||
try argv.appendSlice(a, &.{ "-H", ct });
|
||||
}
|
||||
if (file_path) |p| {
|
||||
const d = try std.fmt.allocPrint(a, "@{s}", .{p});
|
||||
try argv.appendSlice(a, &.{ "--data-binary", d });
|
||||
}
|
||||
try argv.append(a, url);
|
||||
|
||||
var child = try std.process.spawn(io, .{
|
||||
.argv = argv.items,
|
||||
.stdin = .ignore,
|
||||
.stdout = .pipe,
|
||||
.stderr = .ignore,
|
||||
});
|
||||
const out = child.stdout.?;
|
||||
var buf: std.ArrayList(u8) = .empty;
|
||||
errdefer buf.deinit(alloc);
|
||||
var tmp: [16 * 1024]u8 = undefined;
|
||||
while (true) {
|
||||
const got = out.readStreaming(io, &.{&tmp}) catch break;
|
||||
if (got == 0) break;
|
||||
try buf.appendSlice(alloc, tmp[0..got]);
|
||||
}
|
||||
_ = child.wait(io) catch {};
|
||||
return buf.toOwnedSlice(alloc);
|
||||
}
|
||||
|
||||
/// Upload a local file to the homeserver media store.
|
||||
/// Returns the mxc:// URI (allocated; caller frees).
|
||||
pub fn uploadFile(alloc: std.mem.Allocator, io: Io, token: []const u8, path: []const u8, filename: []const u8, content_type: []const u8) ![]u8 {
|
||||
const url = try std.fmt.allocPrint(alloc, "{s}/_matrix/media/v3/upload?filename={s}", .{ HOMESERVER, filename });
|
||||
defer alloc.free(url);
|
||||
const resp = try httpRaw(alloc, io, "POST", url, token, content_type, path);
|
||||
defer alloc.free(resp);
|
||||
const uri = letta.jsonStr(alloc, resp, 0, "content_uri") orelse {
|
||||
std.debug.print("matrix: upload failed, resp: {s}\n", .{resp[0..@min(resp.len, 400)]});
|
||||
return error.UploadFailed;
|
||||
};
|
||||
return uri.val;
|
||||
}
|
||||
|
||||
/// Download media by mxc:// URI. Returns raw bytes (allocated; caller frees).
|
||||
pub fn downloadFile(alloc: std.mem.Allocator, io: Io, token: []const u8, mxc: []const u8) ![]u8 {
|
||||
// mxc://server/mediaId
|
||||
if (!std.mem.startsWith(u8, mxc, "mxc://")) return error.BadMxc;
|
||||
const rest = mxc["mxc://".len..];
|
||||
const slash = std.mem.indexOfScalar(u8, rest, '/') orelse return error.BadMxc;
|
||||
const server = rest[0..slash];
|
||||
const media_id = rest[slash + 1 ..];
|
||||
const url = try std.fmt.allocPrint(alloc, "{s}/_matrix/media/v3/download/{s}/{s}?allow_redirect=true", .{ HOMESERVER, server, media_id });
|
||||
defer alloc.free(url);
|
||||
const resp = try httpRaw(alloc, io, "GET", url, token, "", null);
|
||||
// Error responses are small JSON with errcode — detect and report.
|
||||
if (resp.len > 0 and resp[0] == '{') {
|
||||
if (letta.jsonStr(alloc, resp, 0, "errcode") != null) {
|
||||
std.debug.print("matrix: download failed, resp: {s}\n", .{resp[0..@min(resp.len, 400)]});
|
||||
alloc.free(resp);
|
||||
return error.DownloadFailed;
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/// Send a file event (m.file / m.image / m.audio / m.video).
|
||||
/// Returns event id (allocated; caller frees).
|
||||
pub fn sendFile(alloc: std.mem.Allocator, io: Io, token: []const u8, room: []const u8, msgtype: []const u8, mxc: []const u8, filename: []const u8, mimetype: []const u8, size: u64) ![]u8 {
|
||||
const ts = Io.Clock.now(.real, io).nanoseconds;
|
||||
const url = try std.fmt.allocPrint(alloc, "{s}/_matrix/client/v3/rooms/{s}/send/m.room.message/file-{d}", .{ HOMESERVER, room, ts });
|
||||
defer alloc.free(url);
|
||||
|
||||
var payload: std.ArrayList(u8) = .empty;
|
||||
defer payload.deinit(alloc);
|
||||
try payload.appendSlice(alloc, "{\"msgtype\":\"");
|
||||
try payload.appendSlice(alloc, msgtype);
|
||||
try payload.appendSlice(alloc, "\",\"body\":\"");
|
||||
var name_esc: std.ArrayList(u8) = .empty;
|
||||
defer name_esc.deinit(alloc);
|
||||
try jsonEscape(alloc, &name_esc, filename);
|
||||
try payload.appendSlice(alloc, name_esc.items);
|
||||
try payload.appendSlice(alloc, "\",\"url\":\"");
|
||||
try payload.appendSlice(alloc, mxc);
|
||||
try payload.appendSlice(alloc, "\",\"info\":{\"mimetype\":\"");
|
||||
try payload.appendSlice(alloc, mimetype);
|
||||
try payload.appendSlice(alloc, "\",\"size\":");
|
||||
var sz_buf: [24]u8 = undefined;
|
||||
const sz = std.fmt.bufPrint(&sz_buf, "{d}", .{size}) catch unreachable;
|
||||
try payload.appendSlice(alloc, sz);
|
||||
try payload.appendSlice(alloc, "}}");
|
||||
|
||||
const resp = try http(alloc, io, "PUT", url, token, payload.items);
|
||||
defer alloc.free(resp);
|
||||
const ev = letta.jsonStr(alloc, resp, 0, "event_id") orelse {
|
||||
std.debug.print("matrix: sendFile failed, resp: {s}\n", .{resp[0..@min(resp.len, 400)]});
|
||||
return error.SendFailed;
|
||||
};
|
||||
return ev.val;
|
||||
}
|
||||
|
||||
pub fn login(alloc: std.mem.Allocator, io: Io, user: []const u8, password: []const u8) !Session {
|
||||
const body = try std.fmt.allocPrint(alloc,
|
||||
\\{{"type":"m.login.password","identifier":{{"type":"m.id.user","user":"{s}"}},"password":"{s}","initial_device_display_name":"{s}"}}
|
||||
@@ -215,7 +331,15 @@ pub fn joinRoom(alloc: std.mem.Allocator, io: Io, token: []const u8, room: []con
|
||||
alloc.free(resp);
|
||||
}
|
||||
|
||||
pub const Event = struct { sender: []u8, body: []u8, ts: i64 = 0 }; // sender/body allocated
|
||||
pub const Event = struct {
|
||||
sender: []u8,
|
||||
body: []u8,
|
||||
ts: i64 = 0,
|
||||
/// mxc:// URI when the message carries an attachment (m.file/m.image/…).
|
||||
url: ?[]u8 = null, // allocated; caller frees
|
||||
mimetype: []u8 = "", // allocated; caller frees
|
||||
size: i64 = 0,
|
||||
};
|
||||
|
||||
/// One sync poll. Returns message events + next `since` token. Caller frees.
|
||||
/// (Sync first without a since token to establish one, then poll.)
|
||||
@@ -312,12 +436,34 @@ pub fn sync(alloc: std.mem.Allocator, io: Io, token: []const u8, since: []const
|
||||
else => 0,
|
||||
} else 0;
|
||||
|
||||
// Attachment fields (m.file / m.image / m.audio / m.video).
|
||||
var url_copy: ?[]u8 = null;
|
||||
if (content.get("url")) |u| switch (u) {
|
||||
.string => |v| url_copy = alloc.dupe(u8, v) catch null,
|
||||
else => {},
|
||||
};
|
||||
var mime: []u8 = "";
|
||||
var size: i64 = 0;
|
||||
if (content.get("info")) |inf| switch (inf) {
|
||||
.object => |io_| {
|
||||
if (io_.get("mimetype")) |m| switch (m) {
|
||||
.string => |v| mime = alloc.dupe(u8, v) catch "",
|
||||
else => {},
|
||||
};
|
||||
if (io_.get("size")) |s| switch (s) {
|
||||
.integer => |v| size = v,
|
||||
else => {},
|
||||
};
|
||||
},
|
||||
else => {},
|
||||
};
|
||||
|
||||
const sender_copy = alloc.dupe(u8, sender_raw) catch continue;
|
||||
const body_copy = alloc.dupe(u8, body_raw) catch {
|
||||
alloc.free(sender_copy);
|
||||
continue;
|
||||
};
|
||||
events.append(alloc, .{ .sender = sender_copy, .body = body_copy, .ts = ts }) catch {
|
||||
events.append(alloc, .{ .sender = sender_copy, .body = body_copy, .ts = ts, .url = url_copy, .mimetype = mime, .size = size }) catch {
|
||||
alloc.free(sender_copy);
|
||||
alloc.free(body_copy);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user