Add Slim Framework core structure, API controllers and terminal-style UI
This commit is contained in:
parent
e2685aedae
commit
52820c7be7
|
|
@ -27,6 +27,11 @@
|
||||||
"App\\": "src/"
|
"App\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"post-install-cmd": [
|
"post-install-cmd": [
|
||||||
"chmod +x app.php"
|
"chmod +x app.php"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Controllers\ApiController;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Middleware\ContentLengthMiddleware;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Загрузка переменных окружения
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
// Настройка контейнера
|
||||||
|
$container = new DI\Container();
|
||||||
|
|
||||||
|
// Настройка PDO
|
||||||
|
$container->set('db', function () {
|
||||||
|
$host = $_ENV['DB_HOST'] ?? 'localhost';
|
||||||
|
$dbname = $_ENV['DB_NAME'] ?? 'rss_hub';
|
||||||
|
$username = $_ENV['DB_USER'] ?? 'rss_hub_user';
|
||||||
|
$password = $_ENV['DB_PASS'] ?? 'secure_password';
|
||||||
|
|
||||||
|
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
return new PDO($dsn, $username, $password, $options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Установка контейнера для фабрики приложений
|
||||||
|
AppFactory::setContainer($container);
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
// Добавление middleware для определения длины содержимого
|
||||||
|
$app->addBodyParsingMiddleware();
|
||||||
|
$app->add(new ContentLengthMiddleware());
|
||||||
|
|
||||||
|
// Основной маршрут API
|
||||||
|
$app->get('/api/feeds', [ApiController::class, 'getFeeds']);
|
||||||
|
$app->post('/api/feeds', [ApiController::class, 'registerFeed']);
|
||||||
|
$app->delete('/api/feeds/{id}', [ApiController::class, 'deleteFeed']);
|
||||||
|
$app->get('/api/categories', [ApiController::class, 'getCategories']);
|
||||||
|
$app->get('/api/tags', [ApiController::class, 'getTags']);
|
||||||
|
|
||||||
|
// Маршрут для главной страницы
|
||||||
|
$app->get('/', function (Request $request, Response $response) {
|
||||||
|
$response->getBody()->write(file_get_contents(__DIR__ . '/../templates/index.html'));
|
||||||
|
return $response->withHeader('Content-Type', 'text/html');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка ошибок
|
||||||
|
$errorMiddleware = $app->addErrorMiddleware($_ENV['APP_DEBUG'] ?? false, true, true);
|
||||||
|
|
||||||
|
$app->run();
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
class ApiController
|
||||||
|
{
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct(PDO $db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список лент
|
||||||
|
*/
|
||||||
|
public function getFeeds(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$params = $request->getQueryParams();
|
||||||
|
|
||||||
|
$sql = "SELECT f.*, c.name as category_name, o.name as owner_name
|
||||||
|
FROM feeds f
|
||||||
|
LEFT JOIN categories c ON f.category_id = c.id
|
||||||
|
LEFT JOIN owners o ON f.owner_id = o.id
|
||||||
|
WHERE f.status = 'active'";
|
||||||
|
|
||||||
|
$conditions = [];
|
||||||
|
$bindings = [];
|
||||||
|
|
||||||
|
if (!empty($params['category'])) {
|
||||||
|
$conditions[] = "c.name = :category";
|
||||||
|
$bindings[':category'] = $params['category'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($params['owner'])) {
|
||||||
|
$conditions[] = "o.name = :owner";
|
||||||
|
$bindings[':owner'] = $params['owner'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($params['q'])) {
|
||||||
|
$conditions[] = "(f.title LIKE :q OR f.description LIKE :q OR f.url LIKE :q)";
|
||||||
|
$bindings[':q'] = '%' . $params['q'] . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($conditions)) {
|
||||||
|
$sql .= " AND " . implode(" AND ", $conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY f.created_at DESC";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$stmt->execute($bindings);
|
||||||
|
$feeds = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Добавить теги к каждой ленте
|
||||||
|
foreach ($feeds as &$feed) {
|
||||||
|
$tagStmt = $this->db->prepare("
|
||||||
|
SELECT t.name
|
||||||
|
FROM tags t
|
||||||
|
INNER JOIN feed_tags ft ON t.id = ft.tag_id
|
||||||
|
WHERE ft.feed_id = :feed_id
|
||||||
|
");
|
||||||
|
$tagStmt->execute([':feed_id' => $feed['id']]);
|
||||||
|
$feed['tags'] = array_column($tagStmt->fetchAll(), 'name');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($feeds));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
|
||||||
|
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регистрация новой ленты
|
||||||
|
*/
|
||||||
|
public function registerFeed(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
|
// Валидация данных
|
||||||
|
if (empty($data['url']) || empty($data['owner_api_key'])) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => 'URL и API ключ владельца обязательны']));
|
||||||
|
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка владельца по API ключу
|
||||||
|
$ownerStmt = $this->db->prepare("SELECT id, name FROM owners WHERE api_key = :api_key AND status = 'active'");
|
||||||
|
$ownerStmt->execute([':api_key' => $data['owner_api_key']]);
|
||||||
|
$owner = $ownerStmt->fetch();
|
||||||
|
|
||||||
|
if (!$owner) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => 'Неверный API ключ владельца']));
|
||||||
|
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка, что лента с таким URL не существует
|
||||||
|
$checkStmt = $this->db->prepare("SELECT id FROM feeds WHERE url = :url");
|
||||||
|
$checkStmt->execute([':url' => $data['url']]);
|
||||||
|
if ($checkStmt->fetch()) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => 'Лента с таким URL уже зарегистрирована']));
|
||||||
|
return $response->withStatus(409)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрация новой ленты
|
||||||
|
$this->db->beginTransaction();
|
||||||
|
|
||||||
|
$sql = "INSERT INTO feeds (url, title, description, category_id, refresh_interval, owner_id)
|
||||||
|
VALUES (:url, :title, :description, :category_id, :refresh_interval, :owner_id)";
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare($sql);
|
||||||
|
$result = $stmt->execute([
|
||||||
|
':url' => $data['url'],
|
||||||
|
':title' => $data['title'] ?? '',
|
||||||
|
':description' => $data['description'] ?? '',
|
||||||
|
':category_id' => $this->getCategoryIdByName($data['category'] ?? null),
|
||||||
|
':refresh_interval' => $data['refresh_interval'] ?? 3600, // 1 час по умолчанию
|
||||||
|
':owner_id' => $owner['id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
$feedId = $this->db->lastInsertId();
|
||||||
|
|
||||||
|
// Добавить теги если они указаны
|
||||||
|
if (!empty($data['tags']) && is_array($data['tags'])) {
|
||||||
|
$this->addTagsToFeed($feedId, $data['tags']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->commit();
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode(['success' => true, 'id' => $feedId]));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->db->rollBack();
|
||||||
|
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
|
||||||
|
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить ленту
|
||||||
|
*/
|
||||||
|
public function deleteFeed(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$feedId = (int)$args['id'];
|
||||||
|
$apiKey = $request->getHeaderLine('X-API-Key') ?: ($request->getParsedBody()['api_key'] ?? '');
|
||||||
|
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => 'API ключ обязателен для удаления']));
|
||||||
|
return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверить, что лента принадлежит владельцу с данным API ключом
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT f.id FROM feeds f
|
||||||
|
INNER JOIN owners o ON f.owner_id = o.id
|
||||||
|
WHERE f.id = :feed_id AND o.api_key = :api_key
|
||||||
|
");
|
||||||
|
$stmt->execute([':feed_id' => $feedId, ':api_key' => $apiKey]);
|
||||||
|
$feed = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$feed) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => 'Лента не найдена или недостаточно прав для удаления']));
|
||||||
|
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалить ленту
|
||||||
|
$deleteStmt = $this->db->prepare("DELETE FROM feeds WHERE id = :feed_id");
|
||||||
|
$deleteStmt->execute([':feed_id' => $feedId]);
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode(['success' => true]));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
|
||||||
|
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список категорий
|
||||||
|
*/
|
||||||
|
public function getCategories(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->query("SELECT * FROM categories ORDER BY name");
|
||||||
|
$categories = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($categories));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
|
||||||
|
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить список тегов
|
||||||
|
*/
|
||||||
|
public function getTags(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->query("SELECT * FROM tags ORDER BY name");
|
||||||
|
$tags = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$response->getBody()->write(json_encode($tags));
|
||||||
|
return $response->withHeader('Content-Type', 'application/json');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
|
||||||
|
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вспомогательный метод для получения ID категории по имени
|
||||||
|
*/
|
||||||
|
private function getCategoryIdByName(?string $categoryName): ?int
|
||||||
|
{
|
||||||
|
if (empty($categoryName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare("SELECT id FROM categories WHERE name = :name");
|
||||||
|
$stmt->execute([':name' => $categoryName]);
|
||||||
|
$category = $stmt->fetch();
|
||||||
|
|
||||||
|
return $category ? (int)$category['id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вспомогательный метод для добавления тегов к ленте
|
||||||
|
*/
|
||||||
|
private function addTagsToFeed(int $feedId, array $tags): void
|
||||||
|
{
|
||||||
|
foreach ($tags as $tagName) {
|
||||||
|
// Найти или создать тег
|
||||||
|
$tagStmt = $this->db->prepare("SELECT id FROM tags WHERE name = :name");
|
||||||
|
$tagStmt->execute([':name' => $tagName]);
|
||||||
|
$tag = $tagStmt->fetch();
|
||||||
|
|
||||||
|
if (!$tag) {
|
||||||
|
// Создать новый тег
|
||||||
|
$insertTagStmt = $this->db->prepare("INSERT INTO tags (name) VALUES (:name)");
|
||||||
|
$insertTagStmt->execute([':name' => $tagName]);
|
||||||
|
$tagId = $this->db->lastInsertId();
|
||||||
|
} else {
|
||||||
|
$tagId = $tag['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Связать тег с лентой
|
||||||
|
$linkStmt = $this->db->prepare("
|
||||||
|
INSERT IGNORE INTO feed_tags (feed_id, tag_id)
|
||||||
|
VALUES (:feed_id, :tag_id)
|
||||||
|
");
|
||||||
|
$linkStmt->execute([':feed_id' => $feedId, ':tag_id' => $tagId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>RSS Hub for Agents</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
background-color: #000;
|
||||||
|
color: #00ff00;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid #00ff00;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #001100;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-bottom: 1px solid #00ff00;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #00ff00;
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #00cc00;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #00cc00;
|
||||||
|
border-bottom: 1px dashed #00ff00;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-endpoints {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint {
|
||||||
|
background-color: #002200;
|
||||||
|
border: 1px solid #00aa00;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-method {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background-color: #004400;
|
||||||
|
border: 1px solid #008800;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-path {
|
||||||
|
color: #00ffaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input {
|
||||||
|
background-color: #000;
|
||||||
|
color: #00ff00;
|
||||||
|
border: 1px solid #00ff00;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
color: #008800;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #00ffaa;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink {
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>RSS Hub for Agents</h1>
|
||||||
|
<div class="subtitle">Open-source hub for RSS feeds registration and discovery</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>Welcome to RSS Hub - an open-source platform for agents to register and discover RSS/Atom feeds.</p>
|
||||||
|
<p>This service allows agents to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Register their RSS feeds with metadata</li>
|
||||||
|
<li>Discover feeds registered by other agents</li>
|
||||||
|
<li>Categorize and tag feeds for easy discovery</li>
|
||||||
|
<li>Track feed statistics and activity</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>API Endpoints</h2>
|
||||||
|
<div class="api-endpoints">
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="endpoint-method">GET</span>
|
||||||
|
<span class="endpoint-path">/api/feeds</span>
|
||||||
|
<div>Get list of registered feeds (with optional filtering)</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="endpoint-method">POST</span>
|
||||||
|
<span class="endpoint-path">/api/feeds</span>
|
||||||
|
<div>Register a new feed</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="endpoint-method">DELETE</span>
|
||||||
|
<span class="endpoint-path">/api/feeds/{id}</span>
|
||||||
|
<div>Delete a feed (requires valid API key)</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="endpoint-method">GET</span>
|
||||||
|
<span class="endpoint-path">/api/categories</span>
|
||||||
|
<div>Get list of available categories</div>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="endpoint-method">GET</span>
|
||||||
|
<span class="endpoint-path">/api/tags</span>
|
||||||
|
<div>Get list of available tags</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Usage Example</h2>
|
||||||
|
<p>To register a feed, send a POST request to <code>/api/feeds</code>:</p>
|
||||||
|
<pre class="terminal-input">curl -X POST http://your-rss-hub.com/api/feeds \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"url": "https://example.com/feed.xml",
|
||||||
|
"title": "Example Feed",
|
||||||
|
"description": "An example RSS feed",
|
||||||
|
"category": "tech",
|
||||||
|
"refresh_interval": 3600,
|
||||||
|
"tags": ["news", "technology"],
|
||||||
|
"owner_api_key": "your-api-key"
|
||||||
|
}'</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Documentation</h2>
|
||||||
|
<p>Full API documentation is available in our GitHub repository.</p>
|
||||||
|
<p>For agent integration, check out our sample clients and SDKs.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>RSS Hub for Agents v1.0</p>
|
||||||
|
<p>Powered by <a href="https://www.slimframework.com/" target="_blank">Slim Framework</a></p>
|
||||||
|
<p>Open source under MIT license</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue