//! Matrix client (scaffold) — conduwuit @ chaosmith.systems. //! //! HTTP via curl subprocess behind a narrow seam (native TLS swap later). //! Sync loop + send + login. No olm/megolm: rooms must be unencrypted //! (trusted_private_chat without encryption). const std = @import("std"); const Io = std.Io; const letta = @import("letta.zig"); pub const HOMESERVER = "https://matrix.chaosmith.systems"; pub const DEVICE_NAME = "inferon-harness"; // ------------------------------------------------------------- transport --- /// HTTP via curl subprocess. Returns body (allocated; caller frees). fn http(alloc: std.mem.Allocator, io: Io, method: []const u8, url: []const u8, token: ?[]const u8, json_body: ?[]const u8) ![]u8 { var argv: std.ArrayList([]const u8) = .empty; defer argv.deinit(alloc); try argv.appendSlice(alloc, &.{ "curl", "-sS", "-m", "25", "-X", method }); if (token) |t| { // NOTE: no defer-free — h must outlive the spawn below (argv holds it). // Leaks one small string per call; fine for the harness, revisit with // the native-TLS rewrite. const h = try std.fmt.allocPrint(alloc, "Authorization: Bearer {s}", .{t}); try argv.appendSlice(alloc, &.{ "-H", h }); } if (json_body) |b| { try argv.appendSlice(alloc, &.{ "-H", "Content-Type: application/json", "-d", b }); } try argv.append(alloc, 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); } // ------------------------------------------------------------- api calls --- pub const Session = struct { token: []u8, user_id: []u8 }; // caller frees both 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}"}} , .{ user, password, DEVICE_NAME }); defer alloc.free(body); const resp = try http(alloc, io, "POST", HOMESERVER ++ "/_matrix/client/v3/login", null, body); defer alloc.free(resp); const tok = letta.jsonStr(alloc, resp, 0, "access_token") orelse return error.LoginFailed; errdefer alloc.free(tok.val); const uid = letta.jsonStr(alloc, resp, 0, "user_id") orelse return error.LoginFailed; return .{ .token = tok.val, .user_id = uid.val }; } /// Send a text message. Returns event id (allocated; caller frees). pub fn sendText(alloc: std.mem.Allocator, io: Io, token: []const u8, room: []const u8, body: []const u8) ![]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/infr-{d}", .{ HOMESERVER, room, ts }); defer alloc.free(url); var esc: std.ArrayList(u8) = .empty; defer esc.deinit(alloc); for (body) |c| { switch (c) { '"' => try esc.appendSlice(alloc, "\\\""), '\\' => try esc.appendSlice(alloc, "\\\\"), '\n' => try esc.appendSlice(alloc, "\\n"), '\r', '\t' => try esc.appendSlice(alloc, " "), else => try esc.append(alloc, c), } } const payload = try std.fmt.allocPrint(alloc, "{{\"msgtype\":\"m.text\",\"body\":\"{s}\"}}", .{esc.items}); defer alloc.free(payload); const resp = try http(alloc, io, "PUT", url, token, payload); defer alloc.free(resp); const ev = letta.jsonStr(alloc, resp, 0, "event_id") orelse return error.SendFailed; return ev.val; } /// Join a room by id (accepts invites). pub fn joinRoom(alloc: std.mem.Allocator, io: Io, token: []const u8, room: []const u8) !void { const url = try std.fmt.allocPrint(alloc, "{s}/_matrix/client/v3/rooms/{s}/join", .{ HOMESERVER, room }); defer alloc.free(url); const resp = try http(alloc, io, "POST", url, token, "{}"); alloc.free(resp); } pub const Event = struct { sender: []u8, body: []u8 }; // both allocated /// One sync poll. Returns message events + next `since` token. Caller frees. /// (Sync first without a since token to establish one, then poll.) pub fn sync(alloc: std.mem.Allocator, io: Io, token: []const u8, since: []const u8, timeout_ms: u32) !struct { events: []Event, next: []u8 } { const url = if (since.len > 0) try std.fmt.allocPrint(alloc, "{s}/_matrix/client/v3/sync?timeout={d}&since={s}", .{ HOMESERVER, timeout_ms, since }) else try std.fmt.allocPrint(alloc, "{s}/_matrix/client/v3/sync?timeout=0", .{HOMESERVER}); defer alloc.free(url); const resp = try http(alloc, io, "GET", url, token, null); defer alloc.free(resp); const next = if (letta.jsonStr(alloc, resp, 0, "next_batch")) |nb| nb.val else try alloc.dupe(u8, since); var events: std.ArrayList(Event) = .empty; errdefer events.deinit(alloc); var i: usize = 0; while (std.mem.indexOfPos(u8, resp, i, "\"m.room.message\"")) |pos| { defer i = pos + 15; const win_end = @min(resp.len, pos + 1500); const win = resp[pos..win_end]; const sender = letta.jsonStr(alloc, win, 0, "sender") orelse continue; const body = letta.jsonStr(alloc, win, 0, "body") orelse { alloc.free(sender.val); continue; }; events.append(alloc, .{ .sender = sender.val, .body = body.val }) catch { alloc.free(sender.val); alloc.free(body.val); }; } return .{ .events = try events.toOwnedSlice(alloc), .next = next }; } /// Create a direct chat room with `peer`; returns room id (allocated). pub fn createDirect(alloc: std.mem.Allocator, io: Io, token: []const u8, peer: []const u8) ![]u8 { const body = try std.fmt.allocPrint(alloc, \\{{"preset":"trusted_private_chat","invite":["{s}"],"is_direct":true}} , .{peer}); defer alloc.free(body); const resp = try http(alloc, io, "POST", HOMESERVER ++ "/_matrix/client/v3/createRoom", token, body); defer alloc.free(resp); const room = letta.jsonStr(alloc, resp, 0, "room_id") orelse { std.debug.print("matrix: createRoom failed, resp: {s}\n", .{resp}); return error.CreateFailed; }; return room.val; }