╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1411 try request.respond("", .{ .status = .no_content, .keep_alive = false }); │ │1412 } │ │1413 │ │1414 +// assign the posted email, or the email of the user it names, to the thread the │ │1415 +// url names, then reload it │ │1416 +fn handleAssign( │ │1417 + io: std.Io, │ │1418 + request: *std.http.Server.Request, │ │1419 + allocator: std.mem.Allocator, │ │1420 + base: []const u8, │ │1421 + host: Host, │ │1422 +) !void { │ │1423 + const parts = commentBaseParts(host, base) orelse return respondRemoveNotFound(request); │ │1424 + if (parts.comment_id != null) return respondRemoveNotFound(request); │ │1425 + │ │1426 + var arena = std.heap.ArenaAllocator.init(allocator); │ │1427 + defer arena.deinit(); │ │1428 + const actor = (try authorizeWrite(io, allocator, &arena, request, host, parts.repo_base, .read, parts.thread_kind)) orelse return; │ │1429 + const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │1430 + defer request_repo.deinit(allocator); │ │1431 + const source = request_repo.source; │ │1432 + if (!try source.canModify(io, allocator, actor, parts.thread_kind, &parts.thread_id)) return respondForbidden(request); │ │1433 + │ │1434 + const text = blk: { │ │1435 + const body = try readFormBody(request, allocator); │ │1436 + defer allocator.free(body); │ │1437 + break :blk (try parseFormField(arena.allocator(), body, "assignee")) orelse ""; │ │1438 + }; │ │1439 + const failure: ?ui.Session.FormFeedback.AssignmentFailure = blk: { │ │1440 + const lookup = switch (host) { │ │1441 + .local => try ui.assigneeEmail(null, &arena, text), │ │1442 + .server => |server| server: { │ │1443 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ │1444 + defer admin_repo.deinit(io, allocator); │ │1445 + break :server try ui.assigneeEmail(try evt.currentMoment(evt.admin_repo_opts, &admin_repo), &arena, text); │ │1446 + }, │ │1447 + }; │ │1448 + const email = switch (lookup) { │ │1449 + .email => |email| email, │ │1450 + .failure => |value| break :blk value, │ │1451 + }; │ │1452 + const thread_id_hex = std.fmt.bytesToHex(parts.thread_id, .lower); │ │1453 + switch (source.repo_kind) { │ │1454 + inline else => |repo_kind| { │ │1455 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │1456 + defer any_repo.deinit(io, allocator); │ │1457 + switch (any_repo) { │ │1458 + inline else => |*repo| _ = evt.Assignment.create(host.event(), repo_kind, repo.self_repo_opts, io, allocator, repo, parts.thread_kind, &thread_id_hex, email, actor.author) catch |err| switch (err) { │ │1459 + error.AlreadyAssigned => break :blk .already_assigned, │ │1460 + error.InvalidFields => break :blk .not_an_email, │ │1461 + error.NotFound => return respondRemoveNotFound(request), │ │1462 + else => |e| return e, │ │1463 + }, │ │1464 + } │ │1465 + }, │ │1466 + } │ │1467 + break :blk null; │ │1468 + }; │ │1469 + if (failure) |value| return respondThreadFormFailure(request, allocator, host, base, .{ .assignment = .{ .failure = value, .text = text } }); │ │1470 + try request.respond("", .{ .status = .see_other, .keep_alive = false, .extra_headers = &.{.{ .name = "location", .value = base }} }); │ │1471 +} │ │1472 + │ │1473 +// unassign the email the url's last segment carries from the thread before it, │ │1474 +// then reload the thread │ │1475 +fn handleUnassign( │ │1476 + io: std.Io, │ │1477 + request: *std.http.Server.Request, │ │1478 + allocator: std.mem.Allocator, │ │1479 + base: []const u8, │ │1480 + host: Host, │ │1481 +) !void { │ │1482 + const infix = "/assignee:"; │ │1483 + const at = std.mem.lastIndexOf(u8, base, infix) orelse return respondRemoveNotFound(request); │ │1484 + const thread_url = base[0..at]; │ │1485 + const parts = commentBaseParts(host, thread_url) orelse return respondRemoveNotFound(request); │ │1486 + if (parts.comment_id != null) return respondRemoveNotFound(request); │ │1487 + │ │1488 + var arena = std.heap.ArenaAllocator.init(allocator); │ │1489 + defer arena.deinit(); │ │1490 + const email = std.Uri.percentDecodeInPlace(try arena.allocator().dupe(u8, base[at + infix.len ..])); │ │1491 + const actor = (try authorizeWrite(io, allocator, &arena, request, host, parts.repo_base, .read, parts.thread_kind)) orelse return; │ │1492 + const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │1493 + defer request_repo.deinit(allocator); │ │1494 + const source = request_repo.source; │ │1495 + if (!try source.canModify(io, allocator, actor, parts.thread_kind, &parts.thread_id)) return respondForbidden(request); │ │1496 + │ │1497 + switch (source.repo_kind) { │ │1498 + inline else => |repo_kind| { │ │1499 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │1500 + defer any_repo.deinit(io, allocator); │ │1501 + switch (any_repo) { │ │1502 + inline else => |*repo| evt.removeInThread(host.event(), repo_kind, repo.self_repo_opts, io, allocator, repo, parts.thread_kind, &parts.thread_id, .assign, &evt.Assignment.idOf(&parts.thread_id, email), actor.author) catch |err| switch (err) {│ │1503 + error.EventNotFound => return respondRemoveNotFound(request), │ │1504 + else => |e| return e, │ │1505 + }, │ │1506 + } │ │1507 + }, │ │1508 + } │ │1509 + try request.respond("", .{ .status = .see_other, .keep_alive = false, .extra_headers = &.{.{ .name = "location", .value = thread_url }} }); │ │1510 +} │ │1511 + │ │1512 // the name the upload declares for its file, percent-encoded so it survives a │ │1513 // header │ │1514 fn attachmentName(allocator: std.mem.Allocator, request: *std.http.Server.Request) !?[]u8 { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯