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:
pierreandLetta Code committed 2026-09-03 23:51:09 +03:00
1 parent dcf6d8df75
commit e212e3e120
2 files changed
+308 -6

No files matched your search

+160 -4
View File
@@ -39,6 +39,115 @@ fn runSend(alloc: std.mem.Allocator, io: std.Io, text: []const u8) !void {
alloc.free(ev_id);
}
/// Infer msgtype + mimetype from filename extension.
const FileType = struct { msgtype: []const u8, mimetype: []const u8 };
fn fileTypeOf(path: []const u8) FileType {
const ext = if (std.mem.lastIndexOfScalar(u8, path, '.')) |i| path[i + 1 ..] else "";
const map = .{
.{ ".png", "m.image", "image/png" },
.{ ".jpg", "m.image", "image/jpeg" },
.{ ".jpeg", "m.image", "image/jpeg" },
.{ ".webp", "m.image", "image/webp" },
.{ ".gif", "m.image", "image/gif" },
.{ ".svg", "m.image", "image/svg+xml" },
.{ ".mp3", "m.audio", "audio/mpeg" },
.{ ".ogg", "m.audio", "audio/ogg" },
.{ ".wav", "m.audio", "audio/wav" },
.{ ".mp4", "m.video", "video/mp4" },
.{ ".webm", "m.video", "video/webm" },
.{ ".pdf", "m.file", "application/pdf" },
.{ ".txt", "m.file", "text/plain" },
.{ ".md", "m.file", "text/markdown" },
.{ ".json", "m.file", "application/json" },
.{ ".zip", "m.file", "application/zip" },
.{ ".gz", "m.file", "application/gzip" },
.{ ".log", "m.file", "text/plain" },
};
inline for (map) |e| {
if (std.ascii.eqlIgnoreCase(ext, e[0][1..])) return .{ .msgtype = e[1], .mimetype = e[2] };
}
return .{ .msgtype = "m.file", .mimetype = "application/octet-stream" };
}
/// Basename of a path.
fn basenameOf(path: []const u8) []const u8 {
if (std.mem.lastIndexOfScalar(u8, path, '/')) |i| return path[i + 1 ..];
return path;
}
/// One-shot file send: `matrix_harness send-file /path/to/file`.
/// Uploads to the homeserver media store, then sends as m.image/m.file.
fn runSendFile(alloc: std.mem.Allocator, io: std.Io, path: []const u8) !void {
letta.initConversationsDir();
const user = env_or("MATRIX_USER", "@chaos-prime:chaosmith.systems");
const password = env_or("MATRIX_PASSWORD", "");
const room = env_or("MATRIX_ROOM", "");
if (password.len == 0 or room.len == 0) {
std.debug.print("matrix_harness send-file: MATRIX_PASSWORD and MATRIX_ROOM required\n", .{});
return error.NoCredentials;
}
const dir = std.Io.Dir.cwd();
const st = dir.statFile(io, path, .{}) catch {
std.debug.print("matrix_harness send-file: cannot stat {s}\n", .{path});
return error.NoFile;
};
const size: u64 = switch (st.kind) {
.file => st.size,
else => return error.NotAFile,
};
const ft = fileTypeOf(path);
const name = basenameOf(path);
std.debug.print("matrix_harness: uploading {s} ({d} bytes, {s})...\n", .{ name, size, ft.mimetype });
const session = try matrix.login(alloc, io, user, password);
defer alloc.free(session.token);
defer alloc.free(session.user_id);
const mxc = try matrix.uploadFile(alloc, io, session.token, path, name, ft.mimetype);
defer alloc.free(mxc);
const ev_id = try matrix.sendFile(alloc, io, session.token, room, ft.msgtype, mxc, name, ft.mimetype, size);
std.debug.print("matrix_harness: sent {s} ({s}) as {s}\n", .{ ev_id, name, ft.msgtype });
alloc.free(ev_id);
}
/// Where incoming peer attachments are saved. The harness passes this path
/// to the agent in the forwarded message.
const FILES_DIR = "/tmp/matrix-files";
/// Sanitize a filename for local saving (keep it recognizable, drop path
/// separators and control chars).
fn sanitize(alloc: std.mem.Allocator, name: []const u8) ![]u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(alloc);
for (name) |c| {
switch (c) {
'/', '\\', 0...0x1f, 0x7f => try out.append(alloc, '_'),
' ' => try out.append(alloc, '_'),
else => try out.append(alloc, c),
}
}
if (out.items.len == 0) try out.appendSlice(alloc, "attachment");
return out.toOwnedSlice(alloc);
}
/// Download an incoming attachment to FILES_DIR. Returns local path
/// (allocated; caller frees).
fn saveAttachment(alloc: std.mem.Allocator, io: std.Io, token: []const u8, ev_ts: i64, filename: []const u8, mxc: []const u8) ![]u8 {
std.Io.Dir.cwd().createDirPath(io, FILES_DIR) catch {};
const safe = try sanitize(alloc, filename);
defer alloc.free(safe);
const path = try std.fmt.allocPrint(alloc, "{s}/{d}-{s}", .{ FILES_DIR, if (ev_ts > 0) ev_ts else 0, safe });
errdefer alloc.free(path);
const data = try matrix.downloadFile(alloc, io, token, mxc);
defer alloc.free(data);
const f = try std.Io.Dir.createFileAbsolute(io, path, .{ .truncate = true });
defer f.close(io);
var wbuf: [16 * 1024]u8 = undefined;
var w = f.writer(io, &wbuf);
try w.interface.writeAll(data);
try w.interface.flush();
return path;
}
/// Persistent seen-state: last homeserver event timestamp we already
/// forwarded. Survives restarts so backlog never re-sends read messages.
const STATE_FILE = "/tmp/matrix-harness.state";
@@ -80,6 +189,13 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("usage: matrix_harness send \"message\"\n", .{});
return error.MissingMessage;
}
if (first_arg != null and std.mem.eql(u8, first_arg.?, "send-file")) {
if (args_it.next()) |p| {
return runSendFile(alloc, io, p);
}
std.debug.print("usage: matrix_harness send-file /path/to/file\n", .{});
return error.MissingFile;
}
}
letta.initConversationsDir();
@@ -131,8 +247,20 @@ pub fn main(init: std.process.Init) !void {
for (first.events) |ev| {
defer alloc.free(ev.sender);
defer alloc.free(ev.body);
defer if (ev.url) |u| alloc.free(u);
defer if (ev.mimetype.len > 0) alloc.free(ev.mimetype);
if (ev.ts > new_max) new_max = ev.ts;
if (!std.mem.eql(u8, ev.sender, peer)) continue;
// Attachments in backlog are stale — note them but don't download.
if (ev.url != null) {
if (ev.ts > 0 and now_ms - ev.ts > 30 * 60 * 1000) continue;
if (ev.ts > 0 and ev.ts <= seen_ts) continue;
try backlog.appendSlice(alloc, ev.sender);
try backlog.appendSlice(alloc, ": [sent a file: ");
try backlog.appendSlice(alloc, ev.body);
try backlog.appendSlice(alloc, " — not downloaded, ask if needed]\n");
continue;
}
if (ev.body.len == 0) continue;
if (ev.ts > 0 and now_ms - ev.ts > 30 * 60 * 1000) continue; // stale
if (ev.ts > 0 and ev.ts <= seen_ts) continue; // already seen/answered
@@ -175,15 +303,13 @@ pub fn main(init: std.process.Init) !void {
for (result.events) |ev| {
defer alloc.free(ev.sender);
defer alloc.free(ev.body);
defer if (ev.url) |u| alloc.free(u);
defer if (ev.mimetype.len > 0) alloc.free(ev.mimetype);
defer if (ev.ts > 0) saveSeenTs(io, ev.ts);
// Only peer messages (skip own sends and server noise).
if (!std.mem.eql(u8, ev.sender, peer)) continue;
if (ev.body.len == 0) continue;
std.debug.print("matrix_harness: [{s}] {s}\n", .{ ev.sender, ev.body });
// Prefix with origin + event UTC timestamp so the agent can tell
// Matrix-origin messages from CLI ones (and when they were sent).
var ts_buf: [16]u8 = undefined;
const ts_str: []const u8 = blk: {
if (ev.ts <= 0) break :blk "";
@@ -192,6 +318,36 @@ pub fn main(init: std.process.Init) !void {
const sod = epoch.getDaySeconds();
break :blk std.fmt.bufPrint(&ts_buf, " {d:0>2}:{d:0>2}Z", .{ sod.getHoursIntoDay(), sod.getMinutesIntoHour() }) catch "";
};
// Attachment: download locally and forward the path to the agent.
if (ev.url) |mxc| {
const local = saveAttachment(alloc, io, session.token, ev.ts, ev.body, mxc) catch |e| blk: {
std.debug.print("matrix_harness: attachment download failed: {s}\n", .{@errorName(e)});
break :blk null;
};
defer if (local) |p| alloc.free(p);
var size_buf: [24]u8 = undefined;
const size_str = std.fmt.bufPrint(&size_buf, "{d}", .{ev.size}) catch "?";
const note = if (local) |p|
try std.fmt.allocPrint(alloc, "[matrix{s}] sent a file: \"{s}\" ({s}, {s} bytes) — saved to {s}", .{ ts_str, ev.body, ev.mimetype, size_str, p })
else
try std.fmt.allocPrint(alloc, "[matrix{s}] sent a file: \"{s}\" ({s}, {s} bytes) — download failed, ask them to resend", .{ ts_str, ev.body, ev.mimetype, size_str });
defer alloc.free(note);
const reply = letta.infer(io, alloc, agent, note) catch |e| {
std.debug.print("matrix_harness: letta failed: {s}\n", .{@errorName(e)});
continue;
};
defer alloc.free(reply);
if (matrix.sendText(alloc, io, session.token, room_id, reply)) |ev_id| alloc.free(ev_id) else |e| {
std.debug.print("matrix_harness: send failed: {s}\n", .{@errorName(e)});
}
continue;
}
std.debug.print("matrix_harness: [{s}] {s}\n", .{ ev.sender, ev.body });
// Prefix with origin + event UTC timestamp so the agent can tell
// Matrix-origin messages from CLI ones (and when they were sent).
const prefixed = try std.fmt.allocPrint(alloc, "[matrix{s}] {s}", .{ ts_str, ev.body });
defer alloc.free(prefixed);