discord.zig/src/http.zig
2024-12-10 12:55:59 -05:00

410 lines
14 KiB
Zig

//! ISC License
//!
//! Copyright (c) 2024-2025 Yuzu
//!
//! Permission to use, copy, modify, and/or distribute this software for any
//! purpose with or without fee is hereby granted, provided that the above
//! copyright notice and this permission notice appear in all copies.
//!
//! THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
//! REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
//! AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
//! INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
//! LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
//! OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
//! PERFORMANCE OF THIS SOFTWARE.
const std = @import("std");
const mem = std.mem;
const http = std.http;
const json = std.json;
const zjson = @import("json.zig");
pub const BASE_URL = "https://discord.com/api/v10";
// zig fmt: off
pub const MakeRequestError = (
std.fmt.BufPrintError
|| http.Client.ConnectTcpError
|| http.Client.Request.WaitError
|| http.Client.Request.FinishError
|| http.Client.Request.Writer.Error
|| http.Client.Request.Reader.Error
|| std.Uri.ResolveInPlaceError
|| error{StreamTooLong}
);
// zig fmt: on
pub const FetchReq = struct {
allocator: mem.Allocator,
token: []const u8,
client: http.Client,
body: std.ArrayList(u8),
/// internal
extra_headers: std.ArrayList(http.Header),
query_params: std.StringArrayHashMap([]const u8),
pub fn init(allocator: mem.Allocator, token: []const u8) FetchReq {
const client = http.Client{ .allocator = allocator };
return FetchReq{
.allocator = allocator,
.client = client,
.token = token,
.body = std.ArrayList(u8).init(allocator),
.extra_headers = std.ArrayList(http.Header).init(allocator),
.query_params = std.StringArrayHashMap([]const u8).init(allocator),
};
}
pub fn deinit(self: *FetchReq) void {
self.client.deinit();
self.body.deinit();
}
pub fn addHeader(self: *FetchReq, name: []const u8, value: ?[]const u8) !void {
if (value) |some|
try self.extra_headers.append(http.Header{ .name = name, .value = some });
}
pub fn addQueryParam(self: *FetchReq, name: []const u8, value: anytype) !void {
if (value == null)
return;
var buf: [256]u8 = undefined;
try self.query_params.put(name, try std.fmt.bufPrint(&buf, "{any}", .{value}));
}
fn formatQueryParams(self: *FetchReq) ![]const u8 {
var query = std.ArrayListUnmanaged(u8){};
const writer = query.writer(self.allocator);
if (self.query_params.count() == 0)
return "";
_ = try writer.write("?");
var it = self.query_params.iterator();
while (it.next()) |kv| {
_ = try writer.write(kv.key_ptr.*);
_ = try writer.write("=");
_ = try writer.write(kv.value_ptr.*);
if (it.next()) |_| {
try writer.writeByte('&');
continue;
}
}
return query.toOwnedSlice(self.allocator);
}
pub fn get(self: *FetchReq, comptime T: type, path: []const u8) !zjson.Owned(T) {
const result = try self.makeRequest(.GET, path, null);
if (result.status != .ok)
return error.FailedRequest;
const output = try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
return output;
}
pub fn delete(self: *FetchReq, path: []const u8) !void {
const result = try self.makeRequest(.DELETE, path, null);
if (result.status != .no_content)
return error.FailedRequest;
}
pub fn patch(self: *FetchReq, comptime T: type, path: []const u8, object: anytype) !zjson.Owned(T) {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequest(.PATCH, path, try string.toOwnedSlice());
if (result.status != .ok)
return error.FailedRequest;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn patch2(self: *FetchReq, path: []const u8, object: anytype) !void {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequest(.PATCH, path, try string.toOwnedSlice());
if (result.status != .no_content)
return error.FailedRequest;
}
pub fn put(self: *FetchReq, comptime T: type, path: []const u8, object: anytype) !zjson.Owned(T) {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequest(.PUT, path, try string.toOwnedSlice());
if (result.status != .ok)
return error.FailedRequest;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn put2(self: *FetchReq, comptime T: type, path: []const u8, object: anytype) !?zjson.Owned(T) {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequest(.PUT, path, try string.toOwnedSlice());
if (result.status == .no_content)
return null;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn put3(self: *FetchReq, path: []const u8) !void {
const result = try self.makeRequest(.PUT, path, null);
if (result.status != .no_content)
return error.FailedRequest;
}
pub fn put4(self: *FetchReq, comptime T: type, path: []const u8) !zjson.Owned(T) {
const result = try self.makeRequest(.PUT, path, null);
if (result.status != .ok)
return error.FailedRequest;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn post(self: *FetchReq, comptime T: type, path: []const u8, object: anytype) !zjson.Owned(T) {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequest(.POST, path, try string.toOwnedSlice());
if (result.status != .ok)
return error.FailedRequest;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn post2(self: *FetchReq, comptime T: type, path: []const u8) !zjson.Owned(T) {
const result = try self.makeRequest(.POST, path, null);
if (result.status != .ok)
return error.FailedRequest;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn post3(
self: *FetchReq,
comptime T: type,
path: []const u8,
object: anytype,
files: []const FileData,
) !zjson.Owned(T) {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequestWithFiles(.POST, path, try string.toOwnedSlice(), files);
if (result.status != .ok)
return error.FailedRequest;
return try zjson.parse(T, self.allocator, try self.body.toOwnedSlice());
}
pub fn post4(self: *FetchReq, path: []const u8, object: anytype) !void {
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
errdefer string.deinit();
try json.stringify(object, .{}, string.writer());
const result = try self.makeRequest(.POST, path, try string.toOwnedSlice());
if (result.status != .no_content)
return error.FailedRequest;
}
pub fn post5(self: *FetchReq, path: []const u8) !void {
const result = try self.makeRequest(.POST, path, null);
if (result.status != .no_content)
return error.FailedRequest;
}
pub fn makeRequest(
self: *FetchReq,
method: http.Method,
path: []const u8,
to_post: ?[]const u8,
) MakeRequestError!http.Client.FetchResult {
var buf: [256]u8 = undefined;
const constructed = try std.fmt.bufPrint(&buf, "{s}{s}{s}", .{ BASE_URL, path, try self.formatQueryParams() });
try self.extra_headers.append(http.Header{ .name = "Accept", .value = "application/json" });
try self.extra_headers.append(http.Header{ .name = "Content-Type", .value = "application/json" });
try self.extra_headers.append(http.Header{ .name = "Authorization", .value = self.token });
var fetch_options = http.Client.FetchOptions{
.location = http.Client.FetchOptions.Location{ .url = constructed },
.method = method,
.extra_headers = try self.extra_headers.toOwnedSlice(),
.response_storage = .{ .dynamic = &self.body },
};
if (to_post != null) {
fetch_options.payload = to_post;
}
const res = try self.client.fetch(fetch_options);
return res;
}
pub fn makeRequestWithFiles(
self: *FetchReq,
method: http.Method,
path: []const u8,
to_post: []const u8,
files: []const FileData,
) !http.Client.FetchResult {
var form_fields = try std.ArrayList(FormField).initCapacity(self.allocator, files.len + 1);
errdefer form_fields.deinit();
for (files, 0..) |file, i|
form_fields.appendAssumeCapacity(.{
.name = try std.fmt.allocPrint(self.allocator, "files[{d}]", .{i}),
.filename = file.filename,
.value = file.value,
.content_type = .{ .override = try file.type.string() },
});
form_fields.appendAssumeCapacity(.{
.name = "payload_json",
.value = to_post,
.content_type = .{ .override = "application/json" },
});
var boundary: [64 + 3]u8 = undefined;
std.debug.assert((std.fmt.bufPrint(
&boundary,
"{x:0>16}-{x:0>16}-{x:0>16}-{x:0>16}",
.{ std.crypto.random.int(u64), std.crypto.random.int(u64), std.crypto.random.int(u64), std.crypto.random.int(u64) },
) catch unreachable).len == boundary.len);
const body = try createMultiPartFormDataBody(
self.allocator,
&boundary,
try form_fields.toOwnedSlice(),
);
const headers: std.http.Client.Request.Headers = .{
.content_type = .{ .override = try std.fmt.allocPrint(self.allocator, "multipart/form-data; boundary={s}", .{boundary}) },
.authorization = .{ .override = self.token },
};
var uri_buf: [256]u8 = undefined;
const uri = try std.Uri.parse(try std.fmt.bufPrint(&uri_buf, "{s}{s}{s}", .{ BASE_URL, path, try self.formatQueryParams() }));
var server_header_buffer: [16 * 1024]u8 = undefined;
var request = try self.client.open(method, uri, .{
.keep_alive = false,
.server_header_buffer = &server_header_buffer,
.headers = headers,
.extra_headers = try self.extra_headers.toOwnedSlice(),
});
defer request.deinit();
request.transfer_encoding = .{ .content_length = body.len };
try request.send();
try request.writeAll(body);
try request.finish();
try request.wait();
try request.reader().readAllArrayList(&self.body, 2 * 1024 * 1024);
if (request.response.status.class() == .success)
return .{ .status = request.response.status };
return error.FailedRequest; // TODO: make an Either type lol
}
};
pub const FileData = struct {
filename: []const u8,
value: []const u8,
type: union(enum) {
jpg,
jpeg,
png,
webp,
gif,
pub fn string(self: @This()) ![]const u8 {
var buf: [256]u8 = undefined;
return std.fmt.bufPrint(&buf, "image/{s}", .{@tagName(self)});
}
},
};
pub const FormField = struct {
name: []const u8,
filename: ?[]const u8 = null,
content_type: std.http.Client.Request.Headers.Value = .default,
value: []const u8,
};
fn createMultiPartFormDataBody(
allocator: std.mem.Allocator,
boundary: []const u8,
fields: []const FormField,
) error{OutOfMemory}![]const u8 {
var body: std.ArrayListUnmanaged(u8) = .{};
errdefer body.deinit(allocator);
const writer = body.writer(allocator);
for (fields) |field| {
try writer.print("--{s}\r\n", .{boundary});
if (field.filename) |filename| {
try writer.print("Content-Disposition: form-data; name=\"{s}\"; filename=\"{s}\"\r\n", .{ field.name, filename });
} else {
try writer.print("Content-Disposition: form-data; name=\"{s}\"\r\n", .{field.name});
}
switch (field.content_type) {
.default => {
if (field.filename != null) {
try writer.writeAll("Content-Type: application/octet-stream\r\n");
}
},
.omit => {},
.override => |content_type| {
try writer.print("Content-Type: {s}\r\n", .{content_type});
},
}
try writer.writeAll("\r\n");
try writer.writeAll(field.value);
try writer.writeAll("\r\n");
}
try writer.print("--{s}--\r\n", .{boundary});
return try body.toOwnedSlice(allocator);
}