fediglam/src/main/controllers.zig

318 lines
9.8 KiB
Zig

const std = @import("std");
const root = @import("root");
const builtin = @import("builtin");
const http = @import("http");
const api = @import("api");
const util = @import("util");
const query_utils = @import("./query.zig");
const json_utils = @import("./json.zig");
pub const auth = @import("./controllers/api/auth.zig");
pub const communities = @import("./controllers/api/communities.zig");
pub const invites = @import("./controllers/api/invites.zig");
pub const users = @import("./controllers/api/users.zig");
pub const follows = @import("./controllers/api/users/follows.zig");
pub const notes = @import("./controllers/api/notes.zig");
pub const streaming = @import("./controllers/api/streaming.zig");
pub const timelines = @import("./controllers/api/timelines.zig");
const web = @import("./controllers/web.zig");
const mdw = http.middleware;
const router = mdw.Router(&.{});
const not_found = struct {
pub fn handler(
_: @This(),
_: anytype,
res: anytype,
ctx: anytype,
) !void {
var headers = http.Fields.init(ctx.allocator);
defer headers.deinit();
var stream = try res.open(.not_found, &headers);
defer stream.close();
try stream.finish();
}
};
const base_handler = mdw.SplitUri(mdw.CatchErrors(not_found, mdw.DefaultErrorHandler));
fn InjectApiConn(comptime ApiSource: type) type {
return struct {
api_source: ApiSource,
fn getApiConn(self: @This(), alloc: std.mem.Allocator, req: anytype) !ApiSource.Conn {
const host = req.headers.get("Host") orelse return error.NoHost;
const auth_header = req.headers.get("Authorization");
const token = if (auth_header) |header| blk: {
const prefix = "bearer ";
if (header.len < prefix.len) break :blk null;
if (!std.ascii.eqlIgnoreCase(prefix, header[0..prefix.len])) break :blk null;
break :blk header[prefix.len..];
} else null;
if (token) |t| return try self.api_source.connectToken(host, t, alloc);
if (req.headers.getCookie("active_account") catch return error.BadRequest) |account| {
if (account.len + ("token.").len <= 64) {
var buf: [64]u8 = undefined;
const cookie_name = std.fmt.bufPrint(&buf, "token.{s}", .{account}) catch unreachable;
if (try req.headers.getCookie(cookie_name)) |token_hdr| {
return try self.api_source.connectToken(host, token_hdr, alloc);
}
} else return error.InvalidCookie;
}
return try self.api_source.connectUnauthorized(host, alloc);
}
fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void {
var api_conn = try self.getApiConn(ctx.allocator, req);
defer api_conn.close();
return next.handle(
req,
res,
mdw.injectContext(.{ .api_conn = &api_conn }),
{},
);
}
};
}
pub fn EndpointRequest(comptime Endpoint: type) type {
return struct {
pub const Args = if (@hasDecl(Endpoint, "Args")) Endpoint.Args else void;
pub const Body = if (@hasDecl(Endpoint, "Body")) Endpoint.Body else void;
pub const Query = if (@hasDecl(Endpoint, "Query")) Endpoint.Query else void;
allocator: std.mem.Allocator,
method: http.Method,
uri: []const u8,
headers: http.Fields,
args: Args,
body: Body,
query: Query,
const args_middleware = if (Args == void)
mdw.injectContext(.{ .args = {} })
else
mdw.ParsePathArgs(Args){};
const body_middleware = if (Body == void)
mdw.injectContext(.{ .body = {} })
else
mdw.ParseBody(Body){};
const query_middleware = if (Query == void)
mdw.injectContext(.{ .query = {} })
else
mdw.ParseQueryParams(Query){};
};
}
fn CallApiEndpoint(comptime Endpoint: type) type {
return struct {
fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void {
const request = EndpointRequest(Endpoint){
.allocator = ctx.allocator,
.method = req.method,
.uri = req.uri,
.headers = req.headers,
.args = ctx.args,
.body = ctx.body,
.query = ctx.query,
};
var response = Response{ .headers = http.Fields.init(ctx.allocator), .res = res };
defer response.headers.deinit();
return Endpoint.handler(request, &response, ctx.api_conn);
}
};
}
pub fn apiEndpoint(
comptime Endpoint: type,
api_source: anytype,
) return_type: {
const RequestType = EndpointRequest(Endpoint);
break :return_type mdw.Apply(std.meta.Tuple(.{
mdw.Route,
@TypeOf(RequestType.args_middleware),
@TypeOf(RequestType.query_middleware),
@TypeOf(RequestType.body_middleware),
// TODO: allocation strategy
InjectApiConn(@TypeOf(api_source)),
CallApiEndpoint(Endpoint),
}));
} {
const RequestType = EndpointRequest(Endpoint);
return mdw.apply(.{
mdw.Route{ .desc = .{ .path = Endpoint.path, .method = Endpoint.method } },
RequestType.args_middleware,
RequestType.query_middleware,
RequestType.body_middleware,
// TODO: allocation strategy
InjectApiConn(@TypeOf(api_source)){ .api_source = api_source },
CallApiEndpoint(Endpoint){},
});
}
pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void {
// TODO: hashmaps?
_ = .{ api_source, req, res, alloc };
unreachable;
//var response = Response{ .headers = http.Fields.init(alloc), .res = res };
//defer response.headers.deinit();
//const found = routeRequestInternal(api_source, req, &response, alloc);
//if (!found) response.status(.not_found) catch {};
}
const routes = .{
auth.login,
auth.verify_login,
communities.create,
communities.query,
invites.create,
users.create,
notes.create,
notes.get,
streaming.streaming,
timelines.global,
timelines.local,
timelines.home,
follows.create,
follows.delete,
follows.query_followers,
follows.query_following,
} ++ web.routes;
pub const AllocationStrategy = enum {
arena,
normal,
};
pub const Response = struct {
const Self = @This();
headers: http.Fields,
res: *http.Response,
opened: bool = false,
/// Write a response with no body, only a given status
pub fn status(self: *Self, status_code: http.Status) !void {
var stream = try self.open(status_code);
defer stream.close();
try stream.finish();
}
/// Write a request body as json
pub fn json(self: *Self, status_code: http.Status, response_body: anytype) !void {
try self.headers.put("Content-Type", "application/json");
var stream = try self.open(status_code);
defer stream.close();
const writer = stream.writer();
try std.json.stringify(response_body, json_options, writer);
try stream.finish();
}
pub fn open(self: *Self, status_code: http.Status) !http.Response.ResponseStream {
std.debug.assert(!self.opened);
self.opened = true;
return try self.res.open(status_code, &self.headers);
}
/// Prints the given error as json
pub fn err(self: *Self, status_code: http.Status, message: []const u8, details: anytype) !void {
return self.json(status_code, .{
.message = message,
.details = details,
});
}
/// Signals that the HTTP connection should be hijacked without writing a
/// response beforehand.
pub fn hijack(self: *Self) *http.Response {
std.debug.assert(!self.opened);
self.opened = true;
return self.res;
}
pub fn template(self: *Self, status_code: http.Status, comptime templ: []const u8, data: anytype) !void {
try self.headers.put("Content-Type", "text/html");
var stream = try self.open(status_code);
defer stream.close();
const writer = stream.writer();
try @import("template").execute(writer, templ, data);
try stream.finish();
}
};
const json_options = if (builtin.mode == .Debug)
.{
.whitespace = .{
.indent = .{ .Space = 2 },
.separator = true,
},
.string = .{ .String = .{} },
} else .{
.whitespace = .{
.indent = .None,
.separator = false,
},
.string = .{ .String = .{} },
};
pub const helpers = struct {
pub fn paginate(community: api.Community, path: []const u8, results: anytype, res: *Response, alloc: std.mem.Allocator) !void {
var link = std.ArrayList(u8).init(alloc);
const link_writer = link.writer();
defer link.deinit();
try writeLink(link_writer, community, path, results.next_page, "next");
try link_writer.writeByte(',');
try writeLink(link_writer, community, path, results.prev_page, "prev");
try res.headers.put("Link", link.items);
try res.json(.ok, results.items);
}
fn writeLink(
writer: anytype,
community: api.Community,
path: []const u8,
params: anytype,
rel: []const u8,
) !void {
// TODO: percent-encode
try std.fmt.format(
writer,
"<{s}://{s}/{s}?",
.{ @tagName(community.scheme), community.host, path },
);
try query_utils.formatQuery(params, writer);
try std.fmt.format(
writer,
">; rel=\"{s}\"",
.{rel},
);
}
};