PHP Classes

File: docs/proposals/OutboundFreqImplementation.md

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   docs/proposals/OutboundFreqImplementation.md   Download  
File: docs/proposals/OutboundFreqImplementation.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 10,640 bytes
 

Contents

Class file image Download

Outbound FREQ Implementation

> Draft proposal generated by AI; it may not have been reviewed for accuracy. > Created: 2026-03-15 11:38 PDT

Summary

BinktermPHP currently supports inbound FREQ only ? it can serve files to other nodes that request them via binkp M_GET, but it cannot itself request files from remote nodes. This proposal covers implementing the requesting (client) side of binkp FREQ so that users can request files from other FTN nodes by magic name or filename, with the fulfilled file delivered to their private message area as a netmail attachment.

Background

The existing FREQ review (docs/proposals/FileReqReview.md) identified the inbound M_GET path as the correct primary FREQ mechanism and noted that the requesting side was absent. This proposal implements that missing side.

The use case driving this work: a user sees a node in the nodelist that advertises files via magic names (e.g. NZINTFAQ at 3:770/220@Fidonet). They should be able to request it from the web UI and receive it in their inbox.

Scope

In scope

  • Database queue for outbound FREQ requests
  • Sending `M_GET` frames during a binkp originator session
  • Fulfillment and denial tracking per request
  • Hostname resolution for non-uplink nodes (nodelist ? binkp zone DNS fallback)
  • Delivery of fulfilled files to the requesting user's private file area
  • Netmail notification to the user on fulfillment
  • API endpoint to queue a request and trigger an immediate poll
  • UI dialog on the nodelist node view page (re-enabling the commented-out Request File button)

Out of scope (deferred)

  • `freq_status` lifecycle improvements for the inbound/netmail path (covered separately in FileReqReview.md)
  • Unifying the `freq_outbound` queue model (separate concern)
  • Automated tests for the new paths

Work Items

1. Database migration ? freq_requests table

Create migration vX.Y.Z_freq_requests.sql (check highest existing version before creating).

CREATE TABLE freq_requests (
    id                SERIAL        PRIMARY KEY,
    to_address        VARCHAR(50)   NOT NULL,
    filename          VARCHAR(255)  NOT NULL,
    password          VARCHAR(255),
    requested_by      INTEGER       REFERENCES users(id) ON DELETE SET NULL,
    requested_at      TIMESTAMPTZ   NOT NULL DEFAULT NOW(),
    status            VARCHAR(20)   NOT NULL DEFAULT 'pending',
    -- pending ? sent ? fulfilled | denied | failed
    sent_at           TIMESTAMPTZ,
    fulfilled_at      TIMESTAMPTZ,
    received_filename VARCHAR(255),
    received_path     TEXT,
    notes             TEXT
);
CREATE INDEX idx_freq_requests_pending ON freq_requests (to_address) WHERE status = 'pending';
CREATE INDEX idx_freq_requests_user    ON freq_requests (requested_by, requested_at DESC);

Notes: - to_address stores the normalized address without @domain suffix, matching the format BinkpSession uses for $remoteAddress after handshake parsing. - received_path holds the final path after the file is moved to the user's private area.

2. BinkpSession ? send M_GET and track fulfillment

File: src/Binkp/Protocol/BinkpSession.php

2a. New property

/ @var array<string,int> Mapping of lowercase filename => freq_requests.id for requests sent this session */
private array $freqSentRequests = [];

2b. New method sendFreqRequests()

Called from processSession() as originator, inserted before sendFiles():

sendFreqFiles()       ? existing (serving side)
sendHoldFiles()       ? existing (serving side)
sendFreqRequests()    ? NEW
sendFiles()           ? existing outbound packets

Logic: - Skip if not originator or remote address unknown - Query freq_requests WHERE to_address = $remoteAddr AND status = 'pending' ordered by requested_at ASC - For each row: send M_GET <filename> [password] 0 0, mark status = 'sent', populate $freqSentRequests - Password field: omit entirely if null (send just <filename> 0 0); include if set

2c. Update handleFileData() completion block

After the received file is renamed to its final inbound path, call markFreqRequestFulfilled($filename, $finalPath).

2d. New method markFreqRequestFulfilled()

  • Check `$freqSentRequests` for a case-insensitive match on `$filename`
  • If found, update the `freq_requests` row: `status = 'fulfilled'`, `fulfilled_at = NOW()`, `received_filename`, `received_path`

2e. Update handleSkipCommand()

Currently just logs. Add: check $freqSentRequests for a match on the skipped filename, mark status = 'denied', notes = 'Remote sent M_SKIP'.

3. BinkpClient ? hostname resolution and post-session delivery

File: src/Binkp/Protocol/BinkpClient.php

3a. Hostname resolution for non-uplink nodes

In connect(), replace the current throw-if-no-uplink-and-no-hostname with a call to a new private method resolveNodeHostname(string $address): ?array.

Resolution chain: 1. NodelistManager::getCrashRouteInfo($address) ? checks nodelist IBN/INA/IP flags 2. If null, find the uplink that would route this address via BinkpConfig::getUplinkForDestination(), then call CrashmailService::resolveViaBinkpZone() with that uplink's binkp_zone 3. If still null, throw with a message indicating the node could not be resolved

When connecting to a non-uplink node (no uplink config), pass an empty password (anonymous session).

3b. Post-session FREQ delivery

After $session->processSession() returns successfully, call deliverFulfilledFreqFiles($remoteAddress).

deliverFulfilledFreqFiles(): - Queries freq_requests WHERE to_address = $remoteAddress AND status = 'fulfilled' AND received_path IS NOT NULL AND fulfilled_at > NOW() - INTERVAL '10 minutes' - For each row, calls a delivery helper (see §4)

4. Delivery helper ? file to user private area + netmail notification

New class or method (e.g. FreqDeliveryService or inline in BinkpClient).

For each fulfilled request with a requested_by user:

  1. Get or create the user's private file area via `FileAreaManager::getOrCreatePrivateFileArea($userId)`
  2. Ensure a `requests/` subdirectory exists under the area's `storage_path`
  3. Move the file from `received_path` (inbound dir) to `{privateAreaStoragePath}/requests/{received_filename}`
  4. Insert a row into `files` (linked to the private area, `owner_id = $userId`, `status = 'approved'`)
  5. Call `MessageHandler::sendNetmail()` to send a local delivery notification from the sysop address to the user, with the file as an attachment ? subject: `File Request Fulfilled: {filename}` ? body includes the originating node and original magic name
  6. Update `freq_requests.received_path` to the new final path, add `fulfilled_at` if not already set

If requested_by is null (anonymous or user deleted), skip the netmail step and leave the file in inbound.

5. API endpoint ? queue a FREQ request

Route: POST /api/freq/request

Auth: logged-in users only

Payload:

{
  "node": "3:770/220@fidonet",
  "filename": "NZINTFAQ",
  "password": null
}

Logic: 1. Validate node and filename are present and non-empty 2. Normalize node: strip @domain suffix, validate FTN address format (zone:net/node) 3. Attempt to resolve hostname (nodelist + binkp zone) ? return 422 with a useful error if the node cannot be resolved, so the user knows before queuing 4. Insert freq_requests row with requested_by = $currentUserId 5. Call AdminDaemonClient::binkPoll($normalizedAddress) to trigger immediate session 6. Return { "id": <request_id>, "status": "pending", "node": "<normalized>", "filename": "<filename>" }

Status endpoint: GET /api/freq/request/{id} ? returns the current row so the UI can poll for completion.

6. UI ? Re-enable Request File dialog on node view

Files: templates/nodelist/node_view.twig (and shell variants if applicable)

The Request File button was commented out pending this implementation. Re-enable it and wire it to the new API endpoint. The dialog should: - Show the node address pre-filled - Accept a filename/magic name input - Accept an optional password field - Show a spinner while the poll is in progress - Poll GET /api/freq/request/{id} every few seconds to show status (pending ? fulfilled / denied) - On fulfillment, tell the user to check their inbox

Address Normalization

Throughout the implementation, to_address is stored and matched without the @domain suffix. BinkpSession already strips the domain during M_ADR handshake parsing (the $this->remoteAddress field). The API endpoint normalizes on input. This ensures freq_requests matching in sendFreqRequests() works correctly regardless of how the user typed the address.

Session Timing

M_GET frames are sent before outbound file sending. The remote should start responding with M_FILE frames while (or after) we send our outbound data. The existing EOB/inactivity wait loop handles receiving those files ? as long as the remote is sending data, the inactivity timeout won't fire. No changes to the EOB logic are needed for a first implementation.

Open Questions

  1. Multiple files from the same node: If a user queues several FREQs to the same node, all should go out in one session. The `sendFreqRequests()` method handles this naturally since it sends all pending rows in one pass.
  2. Retry on failure: If a poll session fails before M_GET is sent (`status` stays `pending`), the next poll to that node will retry automatically. If `status = 'sent'` and the session fails before M_FILE arrives, the status is stranded as `sent`. A cleanup sweep (e.g. in maintenance script) could reset requests stuck in `sent` for more than N minutes back to `pending`.
  3. Files for anonymous users: If `requested_by` is null, the received file stays in the inbound directory and won't be cleaned up. This edge case probably won't occur in practice since the API requires authentication.

Implementation Order

  1. Migration (`freq_requests` table)
  2. `BinkpSession` changes (sendFreqRequests + fulfillment/denial tracking)
  3. `BinkpClient` changes (hostname resolution + post-session delivery)
  4. Delivery helper (file ? private area + netmail)
  5. API endpoint
  6. UI dialog

Related Documents

  • `docs/proposals/FileReqReview.md` ? review of inbound FREQ and the netmail path; open issues with `freq_outbound` queue model and `freq_status` lifecycle
  • `docs/FileAreas.md` ? file area documentation