410 lines
14 KiB
Zig
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);
|
|
}
|