523 lines
22 KiB
Zig
523 lines
22 KiB
Zig
//! 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
|
|
|
|
/// 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}"}}
|
|
, .{ 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).
|
|
/// Escape text for embedding inside a JSON string literal.
|
|
fn jsonEscape(alloc: std.mem.Allocator, out: *std.ArrayList(u8), text: []const u8) !void {
|
|
for (text) |c| {
|
|
switch (c) {
|
|
'"' => try out.appendSlice(alloc, "\\\""),
|
|
'\\' => try out.appendSlice(alloc, "\\\\"),
|
|
'\n' => try out.appendSlice(alloc, "\\n"),
|
|
'\r', '\t' => try out.append(alloc, ' '),
|
|
// Any other control char is invalid raw in a JSON string —
|
|
// escape as \u00XX so the server never rejects the send.
|
|
// (Ranges disjoint from \t \n \r handled above.)
|
|
0x00...0x08, 0x0b, 0x0c, 0x0e...0x1f => {
|
|
var buf: [6]u8 = undefined;
|
|
const s = std.fmt.bufPrint(&buf, "\\u{x:0>4}", .{c}) catch unreachable;
|
|
try out.appendSlice(alloc, s);
|
|
},
|
|
else => try out.append(alloc, c),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Minimal markdown → HTML for formatted_body: **bold**, *italic*,
|
|
/// `code`, ``` fences, paragraphs. Element only renders styling via
|
|
/// org.matrix.custom.html — plain m.text bodies show raw asterisks.
|
|
fn markdownToHtml(alloc: std.mem.Allocator, md: []const u8) ![]u8 {
|
|
var out: std.ArrayList(u8) = .empty;
|
|
errdefer out.deinit(alloc);
|
|
var in_code = false;
|
|
var i: usize = 0;
|
|
while (i < md.len) {
|
|
// Fenced code blocks
|
|
if (std.mem.startsWith(u8, md[i..], "```")) {
|
|
try out.appendSlice(alloc, if (in_code) "</pre>" else "<pre>");
|
|
in_code = !in_code;
|
|
i += 3;
|
|
// Skip to end of line (language tag when opening).
|
|
if (!in_code) {
|
|
while (i < md.len and md[i] != '\n') i += 1;
|
|
if (i < md.len) i += 1;
|
|
} else {
|
|
if (i < md.len and md[i] == '\n') i += 1;
|
|
}
|
|
continue;
|
|
}
|
|
if (in_code) {
|
|
try out.append(alloc, md[i]);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
switch (md[i]) {
|
|
'\n' => {
|
|
try out.appendSlice(alloc, "<br>");
|
|
i += 1;
|
|
},
|
|
'<' => {
|
|
try out.appendSlice(alloc, "<");
|
|
i += 1;
|
|
},
|
|
'>' => {
|
|
try out.appendSlice(alloc, ">");
|
|
i += 1;
|
|
},
|
|
'&' => {
|
|
try out.appendSlice(alloc, "&");
|
|
i += 1;
|
|
},
|
|
'`' => {
|
|
// Inline code: `...`
|
|
if (std.mem.indexOfScalarPos(u8, md, i + 1, '`')) |end| {
|
|
try out.appendSlice(alloc, "<code>");
|
|
try out.appendSlice(alloc, md[i + 1 .. end]);
|
|
try out.appendSlice(alloc, "</code>");
|
|
i = end + 1;
|
|
} else {
|
|
try out.append(alloc, '`');
|
|
i += 1;
|
|
}
|
|
},
|
|
'*' => {
|
|
if (std.mem.startsWith(u8, md[i..], "**")) {
|
|
if (std.mem.indexOfPos(u8, md, i + 2, "**")) |end| {
|
|
try out.appendSlice(alloc, "<strong>");
|
|
try out.appendSlice(alloc, md[i + 2 .. end]);
|
|
try out.appendSlice(alloc, "</strong>");
|
|
i = end + 2;
|
|
} else {
|
|
try out.appendSlice(alloc, "**");
|
|
i += 2;
|
|
}
|
|
} else if (std.mem.indexOfScalarPos(u8, md, i + 1, '*')) |end| {
|
|
try out.appendSlice(alloc, "<em>");
|
|
try out.appendSlice(alloc, md[i + 1 .. end]);
|
|
try out.appendSlice(alloc, "</em>");
|
|
i = end + 1;
|
|
} else {
|
|
try out.append(alloc, '*');
|
|
i += 1;
|
|
}
|
|
},
|
|
else => {
|
|
try out.append(alloc, md[i]);
|
|
i += 1;
|
|
},
|
|
}
|
|
}
|
|
if (in_code) try out.appendSlice(alloc, "</pre>");
|
|
return out.toOwnedSlice(alloc);
|
|
}
|
|
|
|
|
|
/// Typing notification. timeout_ms: how long the indicator lasts server-side
|
|
/// (refresh periodically for long turns). typing=false clears it immediately.
|
|
pub fn setTyping(alloc: std.mem.Allocator, io: Io, token: []const u8, user_id: []const u8, room: []const u8, typing: bool, timeout_ms: i64) void {
|
|
var url_buf: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer url_buf.deinit(alloc);
|
|
const user_esc = std.fmt.allocPrint(alloc, "{s}", .{user_id}) catch return;
|
|
defer alloc.free(user_esc);
|
|
// user ids contain chars that are fine unescaped in a path segment for
|
|
// conduwuit, but encode the ':' minimally
|
|
var user_path: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer user_path.deinit(alloc);
|
|
for (user_esc) |ch| {
|
|
if (ch == ':') user_path.appendSlice(alloc, "%3A") catch return else user_path.append(alloc, ch) catch return;
|
|
}
|
|
const url = std.fmt.allocPrint(alloc, "{s}/_matrix/client/v3/rooms/{s}/typing/{s}", .{ HOMESERVER, room, user_path.items }) catch return;
|
|
defer alloc.free(url);
|
|
var payload: std.ArrayListUnmanaged(u8) = .empty;
|
|
defer payload.deinit(alloc);
|
|
const body = if (typing)
|
|
std.fmt.allocPrint(alloc, "{{\"typing\":true,\"timeout\":{d}}}", .{timeout_ms}) catch return
|
|
else
|
|
alloc.dupe(u8, "{\"typing\":false}") catch return;
|
|
defer alloc.free(body);
|
|
_ = httpRaw(alloc, io, "PUT", url, token, "application/json", body) catch null;
|
|
}
|
|
|
|
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);
|
|
|
|
const html = markdownToHtml(alloc, body) catch |e| switch (e) {
|
|
error.OutOfMemory => return e,
|
|
};
|
|
defer alloc.free(html);
|
|
|
|
var payload: std.ArrayList(u8) = .empty;
|
|
defer payload.deinit(alloc);
|
|
try payload.appendSlice(alloc, "{\"msgtype\":\"m.text\",\"body\":\"");
|
|
try jsonEscape(alloc, &payload, body);
|
|
try payload.appendSlice(alloc, "\",\"format\":\"org.matrix.custom.html\",\"formatted_body\":\"");
|
|
try jsonEscape(alloc, &payload, html);
|
|
try payload.append(alloc, '"');
|
|
try payload.append(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 {
|
|
// Diagnose silently-rejected sends: dump first 400 bytes of the
|
|
// server response (errcode/message) and payload head to stderr.
|
|
std.debug.print("matrix: send failed, resp: {s}\n", .{resp[0..@min(resp.len, 400)]});
|
|
std.debug.print("matrix: payload head: {s}\n", .{payload.items[0..@min(payload.items.len, 400)]});
|
|
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,
|
|
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,
|
|
/// Original attachment filename (distinct from body, which is the caption).
|
|
filename: []u8 = &.{}, // allocated; caller frees
|
|
};
|
|
|
|
/// One sync poll. Returns message events + next `since` token. Caller frees.
|
|
/// (Sync first without a since token to establish one, then poll.)
|
|
///
|
|
/// Proper JSON parsing (std.json): the previous string-window scraper
|
|
/// misattributed senders and duplicated events when multiple messages shared
|
|
/// a response — sender/body could be scraped from different events.
|
|
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);
|
|
|
|
var events: std.ArrayList(Event) = .empty;
|
|
errdefer events.deinit(alloc);
|
|
|
|
var arena_state = std.heap.ArenaAllocator.init(alloc);
|
|
defer arena_state.deinit();
|
|
const jalloc = arena_state.allocator();
|
|
|
|
var parsed = std.json.parseFromSlice(std.json.Value, jalloc, resp, .{}) catch return error.BadSyncJson;
|
|
defer parsed.deinit();
|
|
|
|
const root = switch (parsed.value) {
|
|
.object => |o| o,
|
|
else => return error.BadSyncJson,
|
|
};
|
|
|
|
var next: []u8 = try alloc.dupe(u8, since);
|
|
if (root.get("next_batch")) |nb| switch (nb) {
|
|
.string => |s| {
|
|
alloc.free(next);
|
|
next = try alloc.dupe(u8, s);
|
|
},
|
|
else => {},
|
|
};
|
|
|
|
// Walk rooms.join.*.timeline.events[] — every room, in order.
|
|
const join = if (root.get("rooms")) |rooms| switch (rooms) {
|
|
.object => |o| o.get("join") orelse return .{ .events = try events.toOwnedSlice(alloc), .next = next },
|
|
else => return .{ .events = try events.toOwnedSlice(alloc), .next = next },
|
|
} else return .{ .events = try events.toOwnedSlice(alloc), .next = next };
|
|
|
|
const join_obj = switch (join) {
|
|
.object => |o| o,
|
|
else => return .{ .events = try events.toOwnedSlice(alloc), .next = next },
|
|
};
|
|
|
|
var room_it = join_obj.iterator();
|
|
while (room_it.next()) |room_entry| {
|
|
const room = switch (room_entry.value_ptr.*) {
|
|
.object => |o| o,
|
|
else => continue,
|
|
};
|
|
const timeline = if (room.get("timeline")) |t| switch (t) {
|
|
.object => |o| o,
|
|
else => continue,
|
|
} else continue;
|
|
const evs = if (timeline.get("events")) |e| switch (e) {
|
|
.array => |a| a,
|
|
else => continue,
|
|
} else continue;
|
|
|
|
for (evs.items) |ev| {
|
|
const obj = switch (ev) {
|
|
.object => |o| o,
|
|
else => continue,
|
|
};
|
|
// Only m.room.message events.
|
|
const ty = if (obj.get("type")) |t| switch (t) {
|
|
.string => |s| s,
|
|
else => continue,
|
|
} else continue;
|
|
if (!std.mem.eql(u8, ty, "m.room.message")) continue;
|
|
|
|
const sender_raw = if (obj.get("sender")) |s| switch (s) {
|
|
.string => |v| v,
|
|
else => continue,
|
|
} else continue;
|
|
|
|
const content = if (obj.get("content")) |c| switch (c) {
|
|
.object => |o| o,
|
|
else => continue,
|
|
} else continue;
|
|
const body_raw = if (content.get("body")) |b| switch (b) {
|
|
.string => |v| v,
|
|
else => continue,
|
|
} else continue;
|
|
const ts: i64 = if (obj.get("origin_server_ts")) |t| switch (t) {
|
|
.integer => |v| v,
|
|
else => 0,
|
|
} else 0;
|
|
|
|
// Attachment fields (m.file / m.image / m.audio / m.video).
|
|
var url_copy: ?[]u8 = null;
|
|
var fname_copy: []u8 = "";
|
|
if (content.get("filename")) |f| switch (f) {
|
|
.string => |v| fname_copy = alloc.dupe(u8, v) catch "",
|
|
else => {},
|
|
};
|
|
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, .url = url_copy, .filename = fname_copy, .mimetype = mime, .size = size }) catch {
|
|
alloc.free(sender_copy);
|
|
alloc.free(body_copy);
|
|
};
|
|
}
|
|
}
|
|
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;
|
|
}
|