artificer/lib/api/server.zig

1102 lines
38 KiB
Zig

// zig fmt: off
const Root = @import("root.zig");
const Store = @import("./store.zig");
const std = @import("std");
const json_utils = @import("./json_utils.zig");
const errors = @import("./errors.zig");
const stb_image = @import("../stb_image/root.zig");
const RateLimit = @import("./ratelimit.zig");
const FetchError = errors.FetchError;
const assert = std.debug.assert;
const json = std.json;
const Allocator = std.mem.Allocator;
const AuthToken = Root.AuthToken;
const log = std.log.scoped(.api);
const ServerURL = std.BoundedArray(u8, 256);
const Equipment = @import("./schemas/equipment.zig");
const Item = @import("./schemas/item.zig");
const Craft = @import("./schemas/craft.zig");
const Status = @import("./schemas/status.zig");
const Character = @import("./schemas/character.zig");
const Monster = @import("./schemas/monster.zig");
const Resource = @import("./schemas/resource.zig");
const Position = @import("./schemas/position.zig");
const Map = @import("./schemas/map.zig");
const GEOrder = @import("./schemas/ge_order.zig");
const MoveResult = @import("./schemas/move_result.zig");
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
const EquipResult = @import("./schemas/equip_result.zig");
const UnequipResult = EquipResult;
const GatherResult = SkillUsageResult;
const CraftResult = SkillUsageResult;
const Image = Store.Image;
const Server = @This();
const max_response_size = 1024 * 1024 * 16;
// TODO: Figure out a way to more accuretly pick a good 'self.fetch_buffer' size
fetch_buffer: []u8,
client: std.http.Client,
ratelimits: RateLimit.CategoryArray,
server_url: ServerURL,
server_uri: std.Uri,
token: ?AuthToken = null,
store: *Store,
pub fn init(allocator: Allocator, store: *Store) !Server {
const response_buffer = try allocator.alloc(u8, max_response_size);
errdefer allocator.free(response_buffer);
const now = std.time.milliTimestamp();
// Limits gotten from https://docs.artifactsmmo.com/api_guide/rate_limits
var ratelimits = RateLimit.CategoryArray.initFill(RateLimit{});
ratelimits.set(.account_creation, RateLimit.init(now, 50, null, null));
ratelimits.set(.token, RateLimit.init(now, 50, null, null));
ratelimits.set(.data, RateLimit.init(now, 7200, 200, 16));
ratelimits.set(.actions, RateLimit.init(now, 7200, 200, 5));
return Server{
.client = .{ .allocator = allocator },
.server_url = try ServerURL.fromSlice(Root.api_url),
.server_uri = try std.Uri.parse(Root.api_url),
.store = store,
.fetch_buffer = response_buffer,
.ratelimits = ratelimits
};
}
pub fn deinit(self: *Server) void {
const allocator = self.client.allocator;
self.client.deinit();
allocator.free(self.fetch_buffer);
}
pub fn setURL(self: *Server, url: []const u8) !void {
const server_url = try ServerURL.fromSlice(url);
self.server_uri = try std.Uri.parse(server_url);
self.server_url = server_url;
}
pub fn setToken(self: *Server, token: ?[]const u8) !void {
if (token) |t| {
self.token = try AuthToken.fromSlice(t);
} else {
self.token = null;
}
}
const FetchJsonOptions = struct {
const QueryValue = struct {
key: []const u8,
value: []const u8,
};
method: std.http.Method,
path: []const u8,
ratelimit: RateLimit.Category,
payload: ?[]const u8 = null,
query: ?[]const QueryValue = null,
page: ?u64 = null,
page_size: ?u64 = null,
/// When enabled, will to iterate over all available pages
paginated: bool = false,
};
pub const FetchJsonResult = struct {
status: std.http.Status,
body: ?json.Value = null,
};
fn formatQueryValues(buffer: *std.ArrayList(u8), pairs: []const FetchJsonOptions.QueryValue) ![]u8 {
buffer.clearRetainingCapacity();
for (0.., pairs) |i, pair| {
if (i > 0) {
try buffer.appendSlice("&");
}
try buffer.appendSlice(pair.key);
try buffer.appendSlice("=");
try buffer.appendSlice(pair.value);
}
return buffer.items;
}
fn maxIntBufferSize(comptime T: type) usize {
const max_int_size = std.fmt.count("{}", .{ std.math.maxInt(T) });
const min_int_size = std.fmt.count("{}", .{ std.math.minInt(T) });
return @max(max_int_size, min_int_size);
}
fn fetch(self: *Server, ratelimit: RateLimit.Category, options: std.http.Client.FetchOptions) !std.http.Client.FetchResult {
log.debug("+---- fetch -----", .{ });
log.debug("| endpoint {} {}", .{ options.method orelse .GET, options.location.uri });
if (options.payload) |payload| {
log.debug("| payload {s}", .{ payload });
}
const started_at = std.time.nanoTimestamp();
const result = try self.client.fetch(options);
const duration_ns = std.time.nanoTimestamp() - started_at;
var ratelimit_obj = self.ratelimits.getPtr(ratelimit);
ratelimit_obj.increment_counters();
log.debug("| duration {d:.3}s", .{ @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s });
log.debug("| status {}", .{ result.status });
if (ratelimit != .token) {
const body = switch (options.response_storage) {
.static => |body| body.items,
.dynamic => |body| body.items,
.ignore => null
};
const png_header = [_]u8{ 137, 80, 78, 71, 13, 10, 26, 10 };
if (body) |b| {
if (std.mem.startsWith(u8, b, &png_header)) {
log.debug("| response <PNG Image>", .{ });
} else {
log.debug("| response {s}", .{ b });
}
}
}
return result;
}
fn fetchJson(self: *Server, options: FetchJsonOptions) FetchError!FetchJsonResult {
var uri = self.server_uri;
uri.path = .{ .raw = options.path };
var fbs = std.heap.FixedBufferAllocator.init(self.fetch_buffer);
const allocator = fbs.allocator();
var result_status: std.http.Status = .ok;
var result_body: ?json.Value = null;
var current_page: u64 = options.page orelse 1;
var total_pages: u64 = 1;
var fetch_results = std.ArrayList(json.Value).init(allocator);
var authorization_header: std.http.Client.Request.Headers.Value = .default;
{
// + 7 for "Bearer "
var authorization_header_buffer: [Root.max_auth_token_size + 7]u8 = undefined;
if (self.token) |token_buff| {
const token = token_buff.slice();
authorization_header = .{
.override = std.fmt.bufPrint(&authorization_header_buffer, "Bearer {s}", .{token}) catch return FetchError.OutOfMemory
};
}
}
var query_buffer = std.ArrayList(u8).init(allocator);
const max_query_parametrs = 2 + @max(
0,
5, // self.getItems
);
while (true) : (current_page += 1) {
// NOTE: The limit on this array was hand picked. If the limit is hit, it should be increased.
var query = std.BoundedArray(FetchJsonOptions.QueryValue, max_query_parametrs).init(0) catch unreachable;
if (options.query) |additional_query| {
query.appendSlice(additional_query) catch return error.OutOfMemory;
}
const page_buffer_size = comptime maxIntBufferSize(@TypeOf(current_page));
var page_buffer: [page_buffer_size]u8 = undefined;
const page_size_buffer_size = comptime maxIntBufferSize(@TypeOf(options.page_size.?));
var page_size_buffer: [page_size_buffer_size]u8 = undefined;
if (options.paginated) {
const page_str = std.fmt.bufPrint(&page_buffer, "{}", .{ current_page }) catch unreachable;
query.append(.{ .key = "page", .value = page_str }) catch return error.OutOfMemory;
if (options.page_size) |page_size| {
const page_size_str = std.fmt.bufPrint(&page_size_buffer, "{}", .{ page_size }) catch unreachable;
query.append(.{ .key = "size", .value = page_size_str }) catch return error.OutOfMemory;
}
}
if (query.len > 0) {
uri.query = .{ .raw = try formatQueryValues(&query_buffer, query.slice()) };
}
var response_storage = std.ArrayList(u8).init(allocator);
var opts = std.http.Client.FetchOptions{
.method = options.method,
.location = .{ .uri = uri },
.payload = options.payload,
.response_storage = .{ .dynamic = &response_storage },
};
opts.headers.authorization = authorization_header;
const result = self.fetch(options.ratelimit, opts) catch return FetchError.RequestFailed;
const response_body = response_storage.items;
var ratelimit = self.ratelimits.getPtr(options.ratelimit);
ratelimit.increment_counters();
const status = @intFromEnum(result.status);
if (status == errors.NotAuthenticated.code.?) {
return FetchError.NotAuthenticated;
} else if (status == errors.ServerUnavailable.code.?) {
return FetchError.ServerUnavailable;
} else if (status == errors.InvalidPayload.code.?) {
return FetchError.InvalidPayload;
} else if (status == errors.TooManyRequests.code.?) {
return FetchError.TooManyRequests;
} else if (status == errors.FatalError.code.?) {
return FetchError.FatalError;
} else if (result.status != .ok) {
return FetchJsonResult{ .status = result.status };
}
const parsed = json.parseFromSliceLeaky(json.Value, allocator, response_body, .{ .allocate = .alloc_if_needed }) catch return FetchError.ParseFailed;
if (parsed != json.Value.object) {
return FetchError.ParseFailed;
}
result_status = result.status;
if (options.paginated) {
const total_i64 = json_utils.getInteger(parsed.object, "total") orelse return FetchError.ParseFailed;
if (total_i64 < 0) return FetchError.ParseFailed;
try fetch_results.ensureTotalCapacity(@intCast(total_i64));
const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return FetchError.ParseFailed;
if (total_pages_i64 < 0) return FetchError.ParseFailed;
total_pages = @intCast(total_pages_i64);
const page_results = json_utils.getArray(parsed.object, "data") orelse return FetchError.ParseFailed;
fetch_results.appendSlice(page_results.items) catch return FetchError.OutOfMemory;
if (current_page >= total_pages) break;
} else {
result_body = parsed.object.get("data");
break;
}
}
if (options.paginated) {
result_body = json.Value{ .array = fetch_results };
}
return FetchJsonResult{
.status = result_status,
.body = result_body
};
}
fn handleFetchError(
status: std.http.Status,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
) ?Error {
if (status != .ok) {
if (Error != FetchError) {
if (parseError == null) {
@compileError("`parseError` must be defined, if `Error` is not `FetchError`");
}
if (parseError.?(status)) |error_value| {
return error_value;
}
} else {
if (parseError != null) {
@compileError("`parseError` must be null");
}
}
}
return null;
}
fn fetchOptionalObject(
self: *Server,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Result: type,
parseObject: fn (store: *Store, obj: json.ObjectMap) anyerror!Result,
fetchOptions: FetchJsonOptions,
) Error!?Result {
const result = try self.fetchJson(fetchOptions);
if (handleFetchError(result.status, Error, parseError)) |error_value| {
return error_value;
}
if (result.status == .not_found) {
return null;
}
if (result.status != .ok) {
return FetchError.RequestFailed;
}
if (result.body == null) {
return FetchError.ParseFailed;
}
const body = json_utils.asObject(result.body.?) orelse return FetchError.ParseFailed;
return parseObject(self.store, body) catch return FetchError.ParseFailed;
}
fn fetchObject(
self: *Server,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Object: type,
parseObject: fn (store: *Store, obj: json.ObjectMap) anyerror!Object,
fetchOptions: FetchJsonOptions
) Error!Object {
const result = try self.fetchOptionalObject(Error, parseError, Object, parseObject, fetchOptions);
return result orelse return FetchError.RequestFailed;
}
fn fetchOptionalArray(
self: *Server,
allocator: Allocator,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Object: type,
parseObject: fn (store: *Store, obj: json.ObjectMap) anyerror!Object,
fetchOptions: FetchJsonOptions
) Error!?std.ArrayList(Object) {
const result = try self.fetchJson(fetchOptions);
if (handleFetchError(result.status, Error, parseError)) |error_value| {
return error_value;
}
if (result.status == .not_found) {
return null;
}
if (result.status != .ok) {
return FetchError.RequestFailed;
}
if (result.body == null) {
return FetchError.ParseFailed;
}
var array = std.ArrayList(Object).init(allocator);
errdefer array.deinit();
const result_data = json_utils.asArray(result.body.?) orelse return FetchError.ParseFailed;
for (result_data.items) |result_item| {
const item_obj = json_utils.asObject(result_item) orelse return FetchError.ParseFailed;
const parsed_item = parseObject(self.store, item_obj) catch return FetchError.ParseFailed;
array.append(parsed_item) catch return FetchError.OutOfMemory;
}
return array;
}
fn fetchArray(
self: *Server,
allocator: Allocator,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Object: type,
parseObject: fn (store: *Store, body: json.ObjectMap) anyerror!Object,
fetchOptions: FetchJsonOptions
) Error!std.ArrayList(Object) {
const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, fetchOptions);
return result orelse return FetchError.RequestFailed;
}
pub const PrefetchOptions = struct {
resources: bool = true,
maps: bool = true,
monsters: bool = true,
items: bool = true,
images: bool = false,
};
pub fn prefetch(self: *Server, allocator: std.mem.Allocator, opts: PrefetchOptions) !void {
// TODO: Create a version of `getResources`, `getMonsters`, `getItems`, etc..
// which don't need an allocator to be passed.
// This is for cases when you only care that everything will be saved into the store.
if (opts.resources) {
const resources = try self.getResources(allocator, .{});
defer resources.deinit();
}
if (opts.maps) {
const maps: std.ArrayList(Map) = try self.getMaps(allocator, .{});
defer maps.deinit();
}
if (opts.monsters) {
const monsters = try self.getMonsters(allocator, .{});
defer monsters.deinit();
}
if (opts.items) {
const items = try self.getItems(allocator, .{});
defer items.deinit();
}
if (opts.images) {
for (self.store.maps.items) |map| {
const skin: []const u8 = map.skin.slice();
if (self.store.images.getId(.map, skin) == null) {
_ = try self.getImage(.map, skin);
}
}
inline for (std.meta.fields(Character.Skin)) |field| {
const skin: Character.Skin = @enumFromInt(field.value);
const skin_name = skin.toString();
if (self.store.images.getId(.character, skin_name) == null) {
_ = try self.getImage(.character, skin_name);
}
}
}
}
pub fn prefetchCached(self: *Server, allocator: std.mem.Allocator, absolute_cache_path: []const u8, opts: PrefetchOptions) !void {
const status: Status = try self.getStatus();
const version = status.version.slice();
if (std.fs.openFileAbsolute(absolute_cache_path, .{})) |file| {
defer file.close();
if (self.store.load(allocator, version, file.reader())) {
return; // Saved store was loaded successfully
} else |_| {}
} else |_| {}
try self.prefetch(allocator, opts);
const file = try std.fs.createFileAbsolute(absolute_cache_path, .{});
defer file.close();
try self.store.save(version, file.writer());
}
// ------------------------- Endpoints ------------------------
// https://api.artifactsmmo.com/docs/#/operations/get_status__get
pub fn getStatus(self: *Server) FetchError!Status {
return try self.fetchObject(
FetchError,
null,
Status,
Status.parse,
.{ .method = .GET, .ratelimit = .data, .path = "/" }
);
}
// https://api.artifactsmmo.com/docs/#/operations/get_my_characters_my_characters_get
pub fn getMyCharacters(self: *Server, allocator: Allocator) FetchError!std.ArrayList(Store.Id) {
return try self.fetchArray(
allocator,
FetchError,
null,
Store.Id,
Character.parseAndAppend,
.{ .method = .GET, .path = "/my/characters", .ratelimit = .data }
);
}
// https://api.artifactsmmo.com/docs/#/operations/get_character_characters__name__get
pub fn getCharacter(self: *Server, name: []const u8) FetchError!?Store.Id {
const path_buff_size = comptime blk: {
var count = 0;
count += 12; // "/characters/"
count += Character.max_name_size;
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/characters/{s}", .{name}) catch return FetchError.InvalidPayload;
return try self.fetchOptionalObject(
FetchError,
null,
Store.Id,
Character.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
}
// https://api.artifactsmmo.com/docs/#/operations/create_character_characters_create_post
pub fn createCharacter(self: *Server, name: []const u8, skin: Character.Skin) errors.CreateCharacterError!Store.Id {
var payload_buffer: [128]u8 = undefined;
const payload = std.fmt.bufPrint(&payload_buffer, "{{ \"name\":\"{s}\",\"skin\":\"{s}\" }}", .{ name, skin.toString() }) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.CreateCharacterError,
errors.parseCreateCharacterError,
Store.Id,
Character.parseAndAppend,
.{ .method = .POST, .path = "/characters/create", .ratelimit = .actions, .payload = payload }
);
}
// https://api.artifactsmmo.com/docs/#/operations/delete_character_characters_delete_post
pub fn deleteCharacter(self: *Server, name: []const u8) errors.DeleteCharacterError!void {
var payload_buffer: [64]u8 = undefined;
const payload = std.fmt.bufPrint(&payload_buffer, "{{ \"name\":\"{s}\" }}", .{ name }) catch return FetchError.InvalidPayload;
_ = try self.fetchObject(
errors.DeleteCharacterError,
errors.parseDeleteCharacterError,
Character,
Character.parse,
.{ .method = .POST, .path = "/characters/delete", .ratelimit = .actions, .payload = payload }
);
if (self.store.characters.getId(name)) |id| {
_ = self.store.characters.remove(id);
}
}
// https://api.artifactsmmo.com/docs/#/operations/get_item_items__code__get
pub fn getItem(self: *Server, code: []const u8) FetchError!?Store.Id {
const path_buff_size = comptime blk: {
var count = 0;
count += 7; // "/items/"
count += Item.max_code_size;
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/items/{s}", .{code}) catch return FetchError.InvalidPayload;
return try self.fetchOptionalObject(
FetchError,
null,
Store.Id,
Item.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
}
pub const ItemOptions = struct {
craft_material: ?[]const u8 = null,
craft_skill: ?Craft.Skill = null,
min_level: ?u64 = null,
max_level: ?u64 = null,
name: ?[]const u8 = null,
type: ?Item.Type = null,
};
// https://api.artifactsmmo.com/docs/#/operations/get_all_items_items_get
pub fn getItems(self: *Server, allocator: Allocator, opts: ItemOptions) FetchError!std.ArrayList(Store.Id) {
const min_level_buffer_size = comptime maxIntBufferSize(@TypeOf(opts.min_level.?));
var min_level_buffer: [min_level_buffer_size]u8 = undefined;
const max_level_buffer_size = comptime maxIntBufferSize(@TypeOf(opts.max_level.?));
var max_level_buffer: [max_level_buffer_size]u8 = undefined;
var query = std.BoundedArray(FetchJsonOptions.QueryValue, 5).init(0) catch unreachable;
if (opts.craft_material) |craft_material| {
query.append(.{ .key = "craft_material", .value = craft_material }) catch unreachable;
}
if (opts.min_level) |min_level| {
const min_level_str = std.fmt.bufPrint(&min_level_buffer, "{}", .{ min_level }) catch unreachable;
query.append(.{ .key = "min_level", .value = min_level_str }) catch unreachable;
}
if (opts.max_level) |max_level| {
const max_level_str = std.fmt.bufPrint(&max_level_buffer, "{}", .{ max_level }) catch unreachable;
query.append(.{ .key = "max_level", .value = max_level_str }) catch unreachable;
}
if (opts.name) |name| {
query.append(.{ .key = "name", .value = name }) catch unreachable;
}
if (opts.type) |item_type| {
query.append(.{ .key = "type", .value = Item.Type.toString(item_type) }) catch unreachable;
}
return try self.fetchArray(
allocator,
FetchError,
null,
Store.Id,
Item.parseAndAppend,
.{
.method = .GET,
.path = "/items",
.ratelimit = .data,
.paginated = true,
.page_size = 100,
.query = query.slice()
}
);
}
// https://api.artifactsmmo.com/docs/#/operations/get_monster_monsters__code__get
pub fn getMonster(self: *Server, code: []const u8) FetchError!?Store.Id {
const path_buff_size = comptime blk: {
var count = 0;
count += 10; // "/monsters/"
count += Monster.max_code_size;
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/monsters/{s}", .{code}) catch return FetchError.InvalidPayload;
return try self.fetchOptionalObject(
FetchError,
null,
Store.Id,
Monster.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
}
pub const MonsterOptions = struct {
drop: ?[]const u8 = null,
max_level: ?u64 = null,
min_level: ?u64 = null,
};
// https://api.artifactsmmo.com/docs/#/operations/get_all_monsters_monsters_get
pub fn getMonsters(self: *Server, allocator: Allocator, opts: MonsterOptions) FetchError!std.ArrayList(Store.Id) {
const min_level_buffer_size = comptime maxIntBufferSize(@TypeOf(opts.min_level.?));
var min_level_buffer: [min_level_buffer_size]u8 = undefined;
const max_level_buffer_size = comptime maxIntBufferSize(@TypeOf(opts.max_level.?));
var max_level_buffer: [max_level_buffer_size]u8 = undefined;
var query = std.BoundedArray(FetchJsonOptions.QueryValue, 3).init(0) catch unreachable;
if (opts.drop) |drop| {
query.append(.{ .key = "drop", .value = drop }) catch unreachable;
}
if (opts.min_level) |min_level| {
const min_level_str = std.fmt.bufPrint(&min_level_buffer, "{}", .{ min_level }) catch unreachable;
query.append(.{ .key = "min_level", .value = min_level_str }) catch unreachable;
}
if (opts.max_level) |max_level| {
const max_level_str = std.fmt.bufPrint(&max_level_buffer, "{}", .{ max_level }) catch unreachable;
query.append(.{ .key = "max_level", .value = max_level_str }) catch unreachable;
}
return try self.fetchArray(
allocator,
FetchError,
null,
Store.Id,
Monster.parseAndAppend,
.{
.method = .GET,
.path = "/monsters",
.ratelimit = .data,
.paginated = true,
.page_size = 100,
.query = query.slice()
}
);
}
// https://api.artifactsmmo.com/docs/#/operations/get_resource_resources__code__get
pub fn getResource(self: *Server, code: []const u8) FetchError!?Store.Id {
const path_buff_size = comptime blk: {
var count = 0;
count += 11; // "/resources/"
count += Resource.max_code_size;
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/resources/{s}", .{code}) catch return FetchError.InvalidPayload;
return try self.fetchOptionalObject(
FetchError,
null,
Store.Id,
Resource.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
}
pub const ResourceOptions = struct {
drop: ?[]const u8 = null,
max_level: ?u64 = null,
min_level: ?u64 = null,
skill: ?Resource.Skill = null,
};
// https://api.artifactsmmo.com/docs/#/operations/get_all_resources_resources_get
pub fn getResources(self: *Server, allocator: Allocator, opts: ResourceOptions) FetchError!std.ArrayList(Store.Id) {
const min_level_buffer_size = comptime maxIntBufferSize(@TypeOf(opts.min_level.?));
var min_level_buffer: [min_level_buffer_size]u8 = undefined;
const max_level_buffer_size = comptime maxIntBufferSize(@TypeOf(opts.max_level.?));
var max_level_buffer: [max_level_buffer_size]u8 = undefined;
var query = std.BoundedArray(FetchJsonOptions.QueryValue, 4).init(0) catch unreachable;
if (opts.drop) |drop| {
query.append(.{ .key = "drop", .value = drop }) catch unreachable;
}
if (opts.min_level) |min_level| {
const min_level_str = std.fmt.bufPrint(&min_level_buffer, "{}", .{ min_level }) catch unreachable;
query.append(.{ .key = "min_level", .value = min_level_str }) catch unreachable;
}
if (opts.max_level) |max_level| {
const max_level_str = std.fmt.bufPrint(&max_level_buffer, "{}", .{ max_level }) catch unreachable;
query.append(.{ .key = "max_level", .value = max_level_str }) catch unreachable;
}
if (opts.skill) |skill| {
query.append(.{ .key = "skill", .value = skill.toString() }) catch unreachable;
}
return try self.fetchArray(
allocator,
FetchError,
null,
Store.Id,
Resource.parseAndAppend,
.{
.method = .GET,
.path = "/resources",
.ratelimit = .data,
.paginated = true,
.page_size = 100,
.query = query.slice()
}
);
}
// https://api.artifactsmmo.com/docs/#/operations/get_map_maps__x___y__get
pub fn getMap(self: *Server, position: Position) FetchError!?Map {
const path_buff_size = comptime blk: {
var count = 0;
count += 6; // "/maps/"
count += maxIntBufferSize(@TypeOf(position.x));
count += 1; // "/"
count += maxIntBufferSize(@TypeOf(position.y));
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/maps/{}/{}", .{ position.x, position.y }) catch return FetchError.InvalidPayload;
const result = try self.fetchOptionalObject(
FetchError,
null,
Position,
Map.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
if (result != null) {
return self.store.getMap(result.?).?.*;
} else {
return null;
}
}
pub const MapOptions = struct {
code: ?[]const u8 = null,
type: ?Map.Content.Type = null,
};
// https://api.artifactsmmo.com/docs/#/operations/get_all_maps_maps_get
pub fn getMaps(self: *Server, allocator: Allocator, opts: MapOptions) FetchError!std.ArrayList(Map) {
var query = std.BoundedArray(FetchJsonOptions.QueryValue, 2).init(0) catch unreachable;
if (opts.code) |code| {
query.append(.{ .key = "content_code", .value = code }) catch unreachable;
}
if (opts.type) |content_type| {
query.append(.{ .key = "content_type", .value = content_type.toString() }) catch unreachable;
}
return try self.fetchArray(
allocator,
FetchError,
null,
Map,
Map.parseAndAppendObject,
.{
.method = .GET,
.path = "/maps",
.ratelimit = .data,
.paginated = true,
.page_size = 100,
.query = query.slice()
}
);
}
// https://docs.artifactsmmo.com/resources/images
pub fn getImage(self: *Server, category: Image.Category, code: []const u8) FetchError!Store.Id {
const category_path = switch (category) {
.character => "characters",
.item => "items",
.monster => "monsters",
.map => "maps",
.resource => "resources",
.effect => "effects",
};
const path_buff_size = comptime blk: {
var count = 0;
count += 8; // "/images/"
count += 10; // For the longest path "characters"
count += 1; // "/"
count += Image.max_code_size;
count += 4; // ".png"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/images/{s}/{s}.png", .{ category_path, code }) catch return FetchError.InvalidPayload;
var uri = Root.images_uri;
uri.path = .{ .raw = path };
var fbs = std.heap.FixedBufferAllocator.init(self.fetch_buffer);
const allocator = fbs.allocator();
var response_storage = std.ArrayList(u8).init(allocator);
const opts = std.http.Client.FetchOptions{
.method = .GET,
.location = .{ .uri = uri },
.response_storage = .{ .dynamic = &response_storage },
};
const result = self.fetch(.data, opts) catch return FetchError.RequestFailed;
const response_body = response_storage.items;
if (result.status != .ok) {
return FetchError.RequestFailed;
}
const image = stb_image.load(response_body) catch return FetchError.FatalError;
defer image.deinit();
const image_id = self.store.images.append(category, code, image.width, image.height) catch return FetchError.OutOfMemory;
const stored_rgba = self.store.images.getRGBA(image_id).?;
@memcpy(stored_rgba, image.rgba);
return image_id;
}
// https://api.artifactsmmo.com/docs/#/operations/get_ge_sell_order_grandexchange_orders__id__get
pub fn getGEOrder(self: *Server, id: []const u8) FetchError!?Store.Id {
const path_buff_size = comptime blk: {
var count = 0;
count += 22; // "/grandexchange/orders/"
count += GEOrder.max_id_size;
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/grandexchange/orders/{s}", .{ id }) catch return FetchError.InvalidPayload;
return try self.fetchOptionalObject(
FetchError,
null,
Store.Id,
GEOrder.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
}
// https://api.artifactsmmo.com/docs/#/operations/action_move_my__name__action_move_post
pub fn move(self: *Server, character: []const u8, position: Position) errors.MoveError!MoveResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 12; // "/action/move"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/move", .{ character }) catch return FetchError.InvalidPayload;
var payload_buffer: [64]u8 = undefined;
const payload = std.fmt.bufPrint(&payload_buffer, "{{ \"x\":{}, \"y\":{} }}", .{ position.x, position.y }) catch return FetchError.InvalidPayload;
const result = try self.fetchObject(
errors.MoveError,
errors.parseMoveError,
MoveResult,
MoveResult.parseAndUpdate,
.{ .method = .POST, .path = path, .ratelimit = .actions, .payload = payload }
);
return result;
}
// https://api.artifactsmmo.com/docs/#/operations/action_gathering_my__name__action_gathering_post
pub fn gather(self: *Server, character: []const u8) errors.GatherError!GatherResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 17; // "/action/gathering"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/gathering", .{ character }) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.GatherError,
errors.parseGatherError,
GatherResult,
GatherResult.parseAndUpdate,
.{ .method = .POST, .path = path, .ratelimit = .actions }
);
}
// https://api.artifactsmmo.com/docs/#/operations/action_crafting_my__name__action_crafting_post
pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) errors.CraftError!CraftResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 16; // "/action/crafting"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/crafting", .{ character }) catch return FetchError.InvalidPayload;
var payload_buff: [256]u8 = undefined;
const payload = std.fmt.bufPrint(&payload_buff, "{{ \"code\":\"{s}\", \"quantity\":{} }}", .{ item, quantity }) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.CraftError,
errors.parseCraftError,
CraftResult,
CraftResult.parseAndUpdate,
.{ .method = .POST, .path = path, .payload = payload, .ratelimit = .actions }
);
}
pub fn equip(self: *Server, character: []const u8, slot: Equipment.SlotId, item: []const u8, quantity: u64) errors.EquipError!EquipResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 13; // "/action/equip"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(
&path_buff,
"/my/{s}/action/equip",.{ character }
) catch return FetchError.InvalidPayload;
var payload_buff: [256]u8 = undefined;
const payload = std.fmt.bufPrint(
&payload_buff,
"{{ \"slot\":\"{s}\", \"code\":\"{s}\", \"quantity\":{} }}", .{ slot.toString(), item, quantity }
) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.EquipError,
errors.parseEquipError,
EquipResult,
EquipResult.parseAndUpdate,
.{ .method = .POST, .path = path, .payload = payload, .ratelimit = .actions }
);
}
pub fn unequip(self: *Server, character: []const u8, slot: Equipment.SlotId, quantity: u64) errors.UnequipError!UnequipResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 15; // "/action/unequip"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(
&path_buff,
"/my/{s}/action/unequip",.{ character }
) catch return FetchError.InvalidPayload;
var payload_buff: [256]u8 = undefined;
const payload = std.fmt.bufPrint(
&payload_buff,
"{{ \"slot\":\"{s}\", \"quantity\":{} }}", .{ slot.toString(), quantity }
) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.UnequipError,
errors.parseUnequipError,
UnequipResult,
UnequipResult.parseAndUpdate,
.{ .method = .POST, .path = path, .payload = payload, .ratelimit = .actions }
);
}
// https://api.artifactsmmo.com/docs/#/operations/generate_token_token_post
pub fn generateToken(self: *Server, username: []const u8, password: []const u8) !AuthToken {
const base64_encoder = std.base64.standard.Encoder;
var credentials_buffer: [256]u8 = undefined;
const credentials = try std.fmt.bufPrint(&credentials_buffer, "{s}:{s}", .{username, password});
const max_encoded_size = comptime base64_encoder.calcSize(credentials_buffer.len);
const max_authorization_header_size = comptime blk: {
var sum: usize = 0;
sum += 6; // "Basic "
sum += max_encoded_size;
break :blk sum;
};
var authorization_header_buffer: [max_authorization_header_size]u8 = undefined;
@memcpy(authorization_header_buffer[0..6], "Basic ");
const encoded_credentials = base64_encoder.encode(authorization_header_buffer[6..], credentials);
const authorization_header = authorization_header_buffer[0..(6 + encoded_credentials.len)];
var fbs = std.heap.FixedBufferAllocator.init(self.fetch_buffer);
const allocator = fbs.allocator();
var uri = self.server_uri;
uri.path = .{ .raw = "/token" };
var response_storage = std.ArrayList(u8).init(allocator);
var opts = std.http.Client.FetchOptions{
.method = .POST,
.location = .{ .uri = uri },
.response_storage = .{ .dynamic = &response_storage },
};
opts.headers.authorization = .{ .override = authorization_header };
const result = self.fetch(.token, opts) catch return FetchError.RequestFailed;
const response_body = response_storage.items;
if (result.status != .ok) {
return FetchError.RequestFailed;
}
const parsed = try json.parseFromSliceLeaky(json.Value, allocator, response_body, .{ .allocate = .alloc_if_needed });
if (parsed != json.Value.object) {
return FetchError.ParseFailed;
}
const token = try json_utils.getStringRequired(parsed.object, "token");
return try AuthToken.fromSlice(token);
}