payfrit-api/api/hub/vcgateway/visitor/feed.php
Mike cd373dd616 Add VC Gateway endpoints for invite links, visitor auth, DM, and rate limiting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:34:52 +00:00

128 lines
3.4 KiB
PHP

<?php
/**
* GET /api/hub/vcgateway/visitor/feed.php
*
* Read-only message feed for a visitor's allowed channels.
* Authenticated via X-Visitor-Token header (not user token).
*
* Query params:
* ChannelID int required Channel to read messages from
* Before int optional Cursor: return messages before this ID
* After int optional Cursor: return messages after this ID
* Limit int optional Max messages (default: 50, max: 100)
*
* Response:
* OK, Messages[], Channel (metadata), HasMore
*/
require_once __DIR__ . '/../helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
jsonResponse(['OK' => false, 'ERROR' => 'method_not_allowed'], 405);
}
$visitor = requireVisitorAuth();
$channelId = (int)($_GET['ChannelID'] ?? 0);
if ($channelId <= 0) {
jsonResponse(['OK' => false, 'ERROR' => 'channel_id_required'], 400);
}
// Verify visitor has access to this channel
if (!visitorCanReadChannel($visitor, $channelId)) {
jsonResponse(['OK' => false, 'ERROR' => 'channel_not_allowed'], 403);
}
// Verify channel exists and is not archived
$channel = queryOne(
"SELECT ID, Name, DisplayName, Purpose, ChannelType
FROM Hub_Channels WHERE ID = ? AND IsArchived = 0",
[$channelId]
);
if (!$channel) {
jsonResponse(['OK' => false, 'ERROR' => 'channel_not_found'], 404);
}
// Pagination
$before = (int)($_GET['Before'] ?? 0);
$after = (int)($_GET['After'] ?? 0);
$limit = min(100, max(1, (int)($_GET['Limit'] ?? 50)));
// Build query - read only non-deleted messages
$where = ['m.ChannelID = ?', 'm.IsDeleted = 0'];
$params = [$channelId];
if ($before > 0) {
$where[] = 'm.ID < ?';
$params[] = $before;
}
if ($after > 0) {
$where[] = 'm.ID > ?';
$params[] = $after;
}
$whereClause = implode(' AND ', $where);
// Fetch one extra to check HasMore
$fetchLimit = $limit + 1;
// Order: newest first for Before cursor, oldest first for After cursor
$order = ($after > 0) ? 'ASC' : 'DESC';
$rows = queryTimed(
"SELECT m.ID, m.SenderAddress, m.Content, m.ParentID,
m.IsEdited, m.CreatedAt, m.UpdatedAt
FROM Hub_Messages m
WHERE $whereClause
ORDER BY m.ID $order
LIMIT $fetchLimit",
$params
);
$hasMore = count($rows) > $limit;
if ($hasMore) {
array_pop($rows);
}
// If we fetched in ASC order, reverse for consistent newest-first
if ($after > 0) {
$rows = array_reverse($rows);
}
// Format messages (read-only: no edit/delete capabilities for visitors)
$messages = [];
foreach ($rows as $row) {
// Resolve sender display name
$senderName = $row['SenderAddress'];
// Try to get agent name
$agent = queryOne(
"SELECT AgentName FROM Sprinter_Agents WHERE FullAddress = ? LIMIT 1",
[$row['SenderAddress']]
);
if ($agent) {
$senderName = $agent['AgentName'];
}
$messages[] = [
'ID' => (int)$row['ID'],
'SenderAddress' => $row['SenderAddress'],
'SenderName' => $senderName,
'Content' => $row['Content'],
'ParentID' => $row['ParentID'] ? (int)$row['ParentID'] : null,
'IsEdited' => (bool)$row['IsEdited'],
'CreatedAt' => toISO8601($row['CreatedAt']),
];
}
jsonResponse([
'OK' => true,
'Channel' => [
'ID' => (int)$channel['ID'],
'Name' => $channel['Name'],
'DisplayName' => $channel['DisplayName'],
'Purpose' => $channel['Purpose'],
],
'Messages' => $messages,
'HasMore' => $hasMore,
]);