From 22f6f26a01719f1b96e78c49b1f4ef37e30fe895 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 23 Jan 2026 22:02:07 +0100 Subject: [PATCH] feat(cyberfeed): Add CyberFeed RSS aggregator packages for OpenWrt New packages: - secubox-app-cyberfeed: Core RSS aggregator service - Pure shell script, OpenWrt compatible - Cyberpunk emoji injection based on content keywords - Caching with configurable TTL - JSON and HTML output with neon/glitch effects - RSS-Bridge support for social media (Facebook, Twitter, YouTube) - luci-app-cyberfeed: LuCI dashboard with cyberpunk theme - Dashboard with stats, quick actions, recent items - Feed management with add/delete - RSS-Bridge templates for easy social media setup - Preview with category filtering - Settings page for service configuration Features: - Auto-emojification (security, tech, mystical themes) - Dark neon UI with scanlines and glitch effects - RSS-Bridge integration for Facebook/Twitter/YouTube - Category-based filtering - Auto-refresh via cron (5 min default) Co-Authored-By: Claude Opus 4.5 --- package/secubox/luci-app-cyberfeed/Makefile | 39 ++ .../luci-static/resources/cyberfeed/api.js | 148 +++++ .../resources/cyberfeed/dashboard.css | 566 ++++++++++++++++ .../resources/view/cyberfeed/feeds.js | 370 +++++++++++ .../resources/view/cyberfeed/overview.js | 251 +++++++ .../resources/view/cyberfeed/preview.js | 138 ++++ .../resources/view/cyberfeed/settings.js | 213 ++++++ .../root/usr/libexec/rpcd/luci.cyberfeed | 303 +++++++++ .../share/luci/menu.d/luci-app-cyberfeed.json | 45 ++ .../share/rpcd/acl.d/luci-app-cyberfeed.json | 17 + .../secubox/secubox-app-cyberfeed/Makefile | 74 +++ .../files/etc/config/cyberfeed | 15 + .../files/etc/cron.d/cyberfeed | 3 + .../files/etc/cyberfeed/feeds.conf | 36 + .../files/etc/init.d/cyberfeed | 38 ++ .../files/usr/bin/cyberfeed | 615 ++++++++++++++++++ .../files/usr/bin/rss-bridge-setup | 190 ++++++ .../files/usr/share/cyberfeed/template.html | 2 + 18 files changed, 3063 insertions(+) create mode 100644 package/secubox/luci-app-cyberfeed/Makefile create mode 100644 package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/api.js create mode 100644 package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/dashboard.css create mode 100644 package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/feeds.js create mode 100644 package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/overview.js create mode 100644 package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/preview.js create mode 100644 package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/settings.js create mode 100644 package/secubox/luci-app-cyberfeed/root/usr/libexec/rpcd/luci.cyberfeed create mode 100644 package/secubox/luci-app-cyberfeed/root/usr/share/luci/menu.d/luci-app-cyberfeed.json create mode 100644 package/secubox/luci-app-cyberfeed/root/usr/share/rpcd/acl.d/luci-app-cyberfeed.json create mode 100644 package/secubox/secubox-app-cyberfeed/Makefile create mode 100644 package/secubox/secubox-app-cyberfeed/files/etc/config/cyberfeed create mode 100644 package/secubox/secubox-app-cyberfeed/files/etc/cron.d/cyberfeed create mode 100644 package/secubox/secubox-app-cyberfeed/files/etc/cyberfeed/feeds.conf create mode 100644 package/secubox/secubox-app-cyberfeed/files/etc/init.d/cyberfeed create mode 100644 package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed create mode 100644 package/secubox/secubox-app-cyberfeed/files/usr/bin/rss-bridge-setup create mode 100644 package/secubox/secubox-app-cyberfeed/files/usr/share/cyberfeed/template.html diff --git a/package/secubox/luci-app-cyberfeed/Makefile b/package/secubox/luci-app-cyberfeed/Makefile new file mode 100644 index 00000000..ee1ebc07 --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/Makefile @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: MIT +# LuCI App for CyberFeed +# Copyright (C) 2025 CyberMind.fr + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI CyberFeed Dashboard +LUCI_DESCRIPTION:=Cyberpunk-themed RSS feed aggregator dashboard with social media support +LUCI_DEPENDS:=+secubox-app-cyberfeed +luci-base +luci-compat +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-cyberfeed +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-cyberfeed/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.cyberfeed $(1)/usr/libexec/rpcd/luci.cyberfeed + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-cyberfeed.json $(1)/usr/share/luci/menu.d/luci-app-cyberfeed.json + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-cyberfeed.json $(1)/usr/share/rpcd/acl.d/luci-app-cyberfeed.json + + $(INSTALL_DIR) $(1)/www/luci-static/resources/cyberfeed + $(INSTALL_DATA) ./htdocs/luci-static/resources/cyberfeed/api.js $(1)/www/luci-static/resources/cyberfeed/api.js + $(INSTALL_DATA) ./htdocs/luci-static/resources/cyberfeed/dashboard.css $(1)/www/luci-static/resources/cyberfeed/dashboard.css + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/cyberfeed + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/cyberfeed/*.js $(1)/www/luci-static/resources/view/cyberfeed/ +endef + +$(eval $(call BuildPackage,luci-app-cyberfeed)) diff --git a/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/api.js b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/api.js new file mode 100644 index 00000000..cbd79890 --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/api.js @@ -0,0 +1,148 @@ +'use strict'; +'require rpc'; +'require baseclass'; + +/** + * CyberFeed API Module + * RPCD interface for CyberFeed RSS Aggregator + */ + +var callGetStatus = rpc.declare({ + object: 'luci.cyberfeed', + method: 'get_status', + expect: { } +}); + +var callGetFeeds = rpc.declare({ + object: 'luci.cyberfeed', + method: 'get_feeds', + expect: { } +}); + +var callGetItems = rpc.declare({ + object: 'luci.cyberfeed', + method: 'get_items', + expect: { } +}); + +var callAddFeed = rpc.declare({ + object: 'luci.cyberfeed', + method: 'add_feed', + params: ['name', 'url', 'type', 'category'], + expect: { } +}); + +var callDeleteFeed = rpc.declare({ + object: 'luci.cyberfeed', + method: 'delete_feed', + params: ['name'], + expect: { } +}); + +var callSyncFeeds = rpc.declare({ + object: 'luci.cyberfeed', + method: 'sync_feeds', + expect: { } +}); + +var callGetConfig = rpc.declare({ + object: 'luci.cyberfeed', + method: 'get_config', + expect: { } +}); + +var callSaveConfig = rpc.declare({ + object: 'luci.cyberfeed', + method: 'save_config', + params: ['enabled', 'refresh_interval', 'max_items', 'cache_ttl', 'rssbridge_enabled', 'rssbridge_port'], + expect: { } +}); + +var callRssBridgeStatus = rpc.declare({ + object: 'luci.cyberfeed', + method: 'rssbridge_status', + expect: { } +}); + +var callRssBridgeInstall = rpc.declare({ + object: 'luci.cyberfeed', + method: 'rssbridge_install', + expect: { } +}); + +var callRssBridgeControl = rpc.declare({ + object: 'luci.cyberfeed', + method: 'rssbridge_control', + params: ['action'], + expect: { } +}); + +return baseclass.extend({ + getStatus: function() { + return callGetStatus(); + }, + + getFeeds: function() { + return callGetFeeds(); + }, + + getItems: function() { + return callGetItems(); + }, + + addFeed: function(name, url, type, category) { + return callAddFeed(name, url, type || 'rss', category || 'custom'); + }, + + deleteFeed: function(name) { + return callDeleteFeed(name); + }, + + syncFeeds: function() { + return callSyncFeeds(); + }, + + getConfig: function() { + return callGetConfig(); + }, + + saveConfig: function(config) { + return callSaveConfig( + config.enabled, + config.refresh_interval, + config.max_items, + config.cache_ttl, + config.rssbridge_enabled, + config.rssbridge_port + ); + }, + + getRssBridgeStatus: function() { + return callRssBridgeStatus(); + }, + + installRssBridge: function() { + return callRssBridgeInstall(); + }, + + controlRssBridge: function(action) { + return callRssBridgeControl(action); + }, + + getDashboardData: function() { + var self = this; + return Promise.all([ + self.getStatus(), + self.getFeeds(), + self.getItems(), + self.getRssBridgeStatus() + ]).then(function(results) { + return { + status: results[0] || {}, + feeds: results[1] || [], + items: results[2] || [], + rssbridge: results[3] || {} + }; + }); + } +}); diff --git a/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/dashboard.css b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/dashboard.css new file mode 100644 index 00000000..80484570 --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/cyberfeed/dashboard.css @@ -0,0 +1,566 @@ +/** + * CyberFeed Dashboard CSS + * Cyberpunk theme with neon effects + */ + +:root { + --cf-neon-cyan: #0ff; + --cf-neon-magenta: #f0f; + --cf-neon-yellow: #ff0; + --cf-neon-green: #0f0; + --cf-dark-bg: #0a0a0f; + --cf-darker-bg: #050508; + --cf-card-bg: rgba(10, 10, 15, 0.9); + --cf-border: rgba(0, 255, 255, 0.2); + --cf-text: #e0e0e0; + --cf-text-dim: #606080; + --cf-success: #22c55e; + --cf-warning: #f59e0b; + --cf-danger: #ef4444; +} + +.cyberfeed-dashboard { + font-family: 'Segoe UI', system-ui, sans-serif; + padding: 20px; + background: linear-gradient(135deg, #0a0a0f 0%, #12121a 100%); + min-height: 100vh; + color: var(--cf-text); +} + +/* Header */ +.cf-header { + text-align: center; + padding: 24px; + margin-bottom: 24px; + background: linear-gradient(180deg, rgba(0,255,255,0.1) 0%, transparent 100%); + border: 1px solid var(--cf-border); + border-radius: 12px; + position: relative; + overflow: hidden; +} + +.cf-header::before { + content: ''; + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0,255,255,0.02) 2px, + rgba(0,255,255,0.02) 4px + ); + pointer-events: none; +} + +.cf-header h1 { + font-size: 2.5rem; + font-weight: 900; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--cf-neon-cyan); + text-shadow: + 0 0 10px var(--cf-neon-cyan), + 0 0 20px var(--cf-neon-cyan), + 0 0 40px var(--cf-neon-cyan); + margin: 0 0 8px 0; + animation: cf-flicker 3s infinite; +} + +@keyframes cf-flicker { + 0%, 100% { opacity: 1; } + 92% { opacity: 1; } + 93% { opacity: 0.8; } + 94% { opacity: 1; } +} + +.cf-header .subtitle { + font-size: 0.9rem; + color: var(--cf-neon-magenta); + letter-spacing: 0.3em; + text-transform: uppercase; +} + +/* Stats Grid */ +.cf-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.cf-stat-card { + background: var(--cf-card-bg); + border: 1px solid var(--cf-border); + border-radius: 12px; + padding: 20px; + text-align: center; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.cf-stat-card::after { + content: ''; + position: absolute; + bottom: 0; left: 0; + width: 100%; height: 3px; + background: var(--cf-neon-cyan); + box-shadow: 0 0 10px var(--cf-neon-cyan); +} + +.cf-stat-card:hover { + transform: translateY(-4px); + border-color: var(--cf-neon-cyan); + box-shadow: 0 0 20px rgba(0,255,255,0.2); +} + +.cf-stat-card .icon { + font-size: 2rem; + margin-bottom: 8px; +} + +.cf-stat-card .value { + font-size: 2rem; + font-weight: 700; + color: var(--cf-neon-cyan); + text-shadow: 0 0 10px var(--cf-neon-cyan); +} + +.cf-stat-card .label { + font-size: 0.75rem; + color: var(--cf-text-dim); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-top: 4px; +} + +/* Cards */ +.cf-card { + background: var(--cf-card-bg); + border: 1px solid var(--cf-border); + border-radius: 12px; + margin-bottom: 20px; + overflow: hidden; +} + +.cf-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + background: linear-gradient(90deg, rgba(0,255,255,0.1) 0%, transparent 100%); + border-bottom: 1px solid var(--cf-border); +} + +.cf-card-title { + display: flex; + align-items: center; + gap: 10px; + font-size: 1.1rem; + font-weight: 600; + color: var(--cf-neon-cyan); +} + +.cf-card-title-icon { + font-size: 1.3rem; +} + +.cf-card-body { + padding: 20px; +} + +.cf-card-body.no-padding { + padding: 0; +} + +/* Buttons */ +.cf-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border: 1px solid var(--cf-neon-cyan); + border-radius: 6px; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + background: transparent; + color: var(--cf-neon-cyan); +} + +.cf-btn:hover { + background: var(--cf-neon-cyan); + color: var(--cf-dark-bg); + box-shadow: 0 0 15px var(--cf-neon-cyan); +} + +.cf-btn-primary { + background: var(--cf-neon-cyan); + color: var(--cf-dark-bg); +} + +.cf-btn-primary:hover { + background: var(--cf-neon-magenta); + border-color: var(--cf-neon-magenta); + box-shadow: 0 0 15px var(--cf-neon-magenta); +} + +.cf-btn-secondary { + border-color: var(--cf-neon-magenta); + color: var(--cf-neon-magenta); +} + +.cf-btn-secondary:hover { + background: var(--cf-neon-magenta); + color: var(--cf-dark-bg); +} + +.cf-btn-danger { + border-color: var(--cf-danger); + color: var(--cf-danger); +} + +.cf-btn-danger:hover { + background: var(--cf-danger); + color: white; +} + +.cf-btn-sm { + padding: 6px 12px; + font-size: 0.8rem; +} + +/* Form Elements */ +.cf-form-group { + margin-bottom: 16px; +} + +.cf-form-label { + display: block; + font-size: 0.85rem; + font-weight: 600; + color: var(--cf-text); + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.cf-form-input { + width: 100%; + padding: 10px 14px; + background: rgba(0,0,0,0.3); + border: 1px solid var(--cf-border); + border-radius: 6px; + color: var(--cf-text); + font-size: 0.95rem; + transition: all 0.3s ease; +} + +.cf-form-input:focus { + outline: none; + border-color: var(--cf-neon-cyan); + box-shadow: 0 0 10px rgba(0,255,255,0.2); +} + +.cf-form-input::placeholder { + color: var(--cf-text-dim); +} + +select.cf-form-input { + cursor: pointer; +} + +/* Table */ +.cf-table { + width: 100%; + border-collapse: collapse; +} + +.cf-table th, +.cf-table td { + padding: 14px 16px; + text-align: left; + border-bottom: 1px solid var(--cf-border); +} + +.cf-table th { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--cf-neon-magenta); + background: rgba(255,0,255,0.05); +} + +.cf-table tr:hover { + background: rgba(0,255,255,0.05); +} + +/* Badges */ +.cf-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.cf-badge-success { + background: rgba(34,197,94,0.2); + color: var(--cf-success); + border: 1px solid var(--cf-success); +} + +.cf-badge-warning { + background: rgba(245,158,11,0.2); + color: var(--cf-warning); + border: 1px solid var(--cf-warning); +} + +.cf-badge-danger { + background: rgba(239,68,68,0.2); + color: var(--cf-danger); + border: 1px solid var(--cf-danger); +} + +.cf-badge-info { + background: rgba(0,255,255,0.2); + color: var(--cf-neon-cyan); + border: 1px solid var(--cf-neon-cyan); +} + +.cf-badge-category { + background: rgba(255,0,255,0.2); + color: var(--cf-neon-magenta); + border: 1px solid var(--cf-neon-magenta); +} + +/* Feed Items */ +.cf-feed-item { + background: linear-gradient(135deg, rgba(0,255,255,0.03) 0%, rgba(255,0,255,0.01) 100%); + border: 1px solid var(--cf-border); + border-left: 3px solid var(--cf-neon-cyan); + padding: 16px; + margin-bottom: 12px; + border-radius: 8px; + transition: all 0.3s ease; +} + +.cf-feed-item:hover { + border-color: var(--cf-neon-magenta); + box-shadow: 0 0 15px rgba(0,255,255,0.1); +} + +.cf-feed-item .meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + flex-wrap: wrap; + gap: 8px; +} + +.cf-feed-item .timestamp { + font-size: 0.7rem; + color: var(--cf-neon-magenta); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.cf-feed-item .title { + font-size: 1rem; + font-weight: 600; + color: var(--cf-neon-cyan); + margin-bottom: 6px; +} + +.cf-feed-item .title a { + color: inherit; + text-decoration: none; +} + +.cf-feed-item .title a:hover { + text-shadow: 0 0 10px var(--cf-neon-cyan); +} + +.cf-feed-item .description { + font-size: 0.85rem; + color: var(--cf-text); + opacity: 0.85; + line-height: 1.5; +} + +/* Empty State */ +.cf-empty { + text-align: center; + padding: 48px 24px; + color: var(--cf-text-dim); +} + +.cf-empty-icon { + font-size: 4rem; + margin-bottom: 16px; + opacity: 0.5; +} + +.cf-empty-text { + font-size: 1.1rem; + margin-bottom: 8px; +} + +.cf-empty-hint { + font-size: 0.85rem; + opacity: 0.7; +} + +/* Quick Actions */ +.cf-quick-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +/* Toast */ +.cf-toast { + position: fixed; + bottom: 24px; + right: 24px; + display: flex; + align-items: center; + gap: 10px; + padding: 14px 20px; + background: var(--cf-card-bg); + border: 1px solid var(--cf-neon-cyan); + border-radius: 8px; + color: var(--cf-text); + font-size: 0.9rem; + z-index: 9999; + animation: cf-slideIn 0.3s ease; + box-shadow: 0 0 20px rgba(0,255,255,0.3); +} + +.cf-toast.success { + border-color: var(--cf-success); + box-shadow: 0 0 20px rgba(34,197,94,0.3); +} + +.cf-toast.error { + border-color: var(--cf-danger); + box-shadow: 0 0 20px rgba(239,68,68,0.3); +} + +@keyframes cf-slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Grid Helpers */ +.cf-grid { + display: grid; + gap: 16px; +} + +.cf-grid-2 { + grid-template-columns: repeat(2, 1fr); +} + +.cf-grid-3 { + grid-template-columns: repeat(3, 1fr); +} + +@media (max-width: 768px) { + .cf-grid-2, + .cf-grid-3 { + grid-template-columns: 1fr; + } + + .cf-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .cf-header h1 { + font-size: 1.8rem; + } +} + +/* Preview iframe */ +.cf-preview-frame { + width: 100%; + height: 600px; + border: 1px solid var(--cf-border); + border-radius: 8px; + background: var(--cf-dark-bg); +} + +/* RSS-Bridge section */ +.cf-rssbridge-card { + border-color: var(--cf-neon-magenta); +} + +.cf-rssbridge-card .cf-card-header { + background: linear-gradient(90deg, rgba(255,0,255,0.1) 0%, transparent 100%); +} + +.cf-rssbridge-card .cf-card-title { + color: var(--cf-neon-magenta); +} + +/* Glitch effect for titles */ +.cf-glitch { + position: relative; +} + +.cf-glitch::before, +.cf-glitch::after { + content: attr(data-text); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.8; +} + +.cf-glitch::before { + color: var(--cf-neon-cyan); + animation: cf-glitch-1 2s infinite linear alternate-reverse; + clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%); +} + +.cf-glitch::after { + color: var(--cf-neon-magenta); + animation: cf-glitch-2 3s infinite linear alternate-reverse; + clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%); +} + +@keyframes cf-glitch-1 { + 0% { transform: translateX(0); } + 20% { transform: translateX(-2px); } + 40% { transform: translateX(2px); } + 60% { transform: translateX(-1px); } + 80% { transform: translateX(1px); } + 100% { transform: translateX(0); } +} + +@keyframes cf-glitch-2 { + 0% { transform: translateX(0); } + 20% { transform: translateX(2px); } + 40% { transform: translateX(-2px); } + 60% { transform: translateX(1px); } + 80% { transform: translateX(-1px); } + 100% { transform: translateX(0); } +} diff --git a/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/feeds.js b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/feeds.js new file mode 100644 index 00000000..05b8cfe2 --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/feeds.js @@ -0,0 +1,370 @@ +'use strict'; +'require view'; +'require dom'; +'require ui'; +'require cyberfeed.api as api'; + +return view.extend({ + title: _('Feed Sources'), + + load: function() { + var cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = L.resource('cyberfeed/dashboard.css'); + document.head.appendChild(cssLink); + + return api.getFeeds(); + }, + + render: function(feeds) { + var self = this; + feeds = Array.isArray(feeds) ? feeds : []; + + var content = []; + + // Page Header + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCE1'), + 'Feed Sources' + ]), + E('a', { + 'href': L.url('admin/services/cyberfeed/overview'), + 'class': 'cf-btn cf-btn-sm cf-btn-secondary' + }, ['\u2190', ' Back']) + ]) + ])); + + // Add Feed Card + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\u2795'), + 'Add New Feed' + ]) + ]), + E('div', { 'class': 'cf-card-body' }, [ + E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 16px;' }, [ + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Feed Name'), + E('input', { + 'type': 'text', + 'id': 'new-name', + 'class': 'cf-form-input', + 'placeholder': 'my_feed (alphanumeric, no spaces)' + }) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Feed URL'), + E('input', { + 'type': 'url', + 'id': 'new-url', + 'class': 'cf-form-input', + 'placeholder': 'https://example.com/feed.xml' + }) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Type'), + E('select', { 'id': 'new-type', 'class': 'cf-form-input' }, [ + E('option', { 'value': 'rss' }, 'RSS/Atom'), + E('option', { 'value': 'rss-bridge' }, 'RSS-Bridge') + ]) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Category'), + E('select', { 'id': 'new-category', 'class': 'cf-form-input' }, [ + E('option', { 'value': 'custom' }, 'Custom'), + E('option', { 'value': 'security' }, 'Security'), + E('option', { 'value': 'tech' }, 'Tech'), + E('option', { 'value': 'social' }, 'Social'), + E('option', { 'value': 'news' }, 'News') + ]) + ]) + ]), + E('div', { 'style': 'margin-top: 16px;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-primary', + 'click': function() { self.handleAddFeed(); } + }, ['\u2795', ' Add Feed']) + ]) + ]) + ])); + + // RSS-Bridge Templates + content.push(E('div', { 'class': 'cf-card cf-rssbridge-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83C\uDF09'), + 'RSS-Bridge Templates' + ]) + ]), + E('div', { 'class': 'cf-card-body' }, [ + E('p', { 'style': 'margin-bottom: 16px; color: var(--cf-text-dim);' }, + 'Quick-add social media feeds (requires RSS-Bridge to be running)'), + E('div', { 'class': 'cf-grid cf-grid-3', 'style': 'gap: 12px;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-sm', + 'click': function() { self.showBridgeModal('Facebook'); } + }, ['\uD83D\uDCD8', ' Facebook']), + E('button', { + 'class': 'cf-btn cf-btn-sm', + 'click': function() { self.showBridgeModal('Twitter'); } + }, ['\uD83D\uDC26', ' Twitter/X']), + E('button', { + 'class': 'cf-btn cf-btn-sm', + 'click': function() { self.showBridgeModal('Youtube'); } + }, ['\uD83D\uDCFA', ' YouTube']), + E('button', { + 'class': 'cf-btn cf-btn-sm', + 'click': function() { self.showBridgeModal('Mastodon'); } + }, ['\uD83D\uDC18', ' Mastodon']), + E('button', { + 'class': 'cf-btn cf-btn-sm', + 'click': function() { self.showBridgeModal('Reddit'); } + }, ['\uD83E\uDD16', ' Reddit']), + E('button', { + 'class': 'cf-btn cf-btn-sm', + 'click': function() { self.showBridgeModal('Instagram'); } + }, ['\uD83D\uDCF7', ' Instagram']) + ]) + ]) + ])); + + // Feeds List + var feedsContent; + if (feeds.length === 0) { + feedsContent = E('div', { 'class': 'cf-empty' }, [ + E('div', { 'class': 'cf-empty-icon' }, '\uD83D\uDCE1'), + E('div', { 'class': 'cf-empty-text' }, 'No feeds configured'), + E('div', { 'class': 'cf-empty-hint' }, 'Add a feed above to get started') + ]); + } else { + feedsContent = E('table', { 'class': 'cf-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Name'), + E('th', {}, 'URL'), + E('th', {}, 'Type'), + E('th', {}, 'Category'), + E('th', { 'style': 'width: 100px; text-align: right;' }, 'Actions') + ]) + ]), + E('tbody', {}, feeds.map(function(feed) { + return E('tr', {}, [ + E('td', { 'style': 'font-weight: 600;' }, feed.name), + E('td', {}, [ + E('span', { 'style': 'font-size: 0.85rem; color: var(--cf-text-dim); word-break: break-all;' }, + feed.url.length > 50 ? feed.url.substring(0, 50) + '...' : feed.url) + ]), + E('td', {}, [ + E('span', { 'class': 'cf-badge cf-badge-info' }, feed.type || 'rss') + ]), + E('td', {}, [ + E('span', { 'class': 'cf-badge cf-badge-category' }, feed.category || 'custom') + ]), + E('td', { 'style': 'text-align: right;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-sm cf-btn-danger', + 'click': function() { self.handleDeleteFeed(feed.name); } + }, 'Delete') + ]) + ]); + })) + ]); + } + + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCCB'), + 'Configured Feeds (' + feeds.length + ')' + ]) + ]), + E('div', { 'class': 'cf-card-body no-padding' }, [feedsContent]) + ])); + + return E('div', { 'class': 'cyberfeed-dashboard' }, content); + }, + + handleAddFeed: function() { + var self = this; + var name = document.getElementById('new-name').value.trim(); + var url = document.getElementById('new-url').value.trim(); + var type = document.getElementById('new-type').value; + var category = document.getElementById('new-category').value; + + if (!name) { + self.showToast('Please enter a feed name', 'error'); + return; + } + + if (!/^[a-zA-Z0-9_-]+$/.test(name)) { + self.showToast('Name must be alphanumeric (underscores/dashes allowed)', 'error'); + return; + } + + if (!url) { + self.showToast('Please enter a feed URL', 'error'); + return; + } + + return api.addFeed(name, url, type, category).then(function(res) { + if (res && res.success) { + self.showToast('Feed added successfully', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + handleDeleteFeed: function(name) { + var self = this; + + ui.showModal('Delete Feed', [ + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('p', {}, 'Are you sure you want to delete this feed?'), + E('div', { + 'style': 'margin-top: 12px; padding: 12px; background: rgba(0,255,255,0.1); border-radius: 8px; font-family: monospace;' + }, name) + ]), + E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-secondary', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'cf-btn cf-btn-danger', + 'click': function() { + ui.hideModal(); + api.deleteFeed(name).then(function(res) { + if (res && res.success) { + self.showToast('Feed deleted', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + } + }, 'Delete') + ]) + ]); + }, + + showBridgeModal: function(bridge) { + var self = this; + var fields = { + 'Facebook': { param: 'u', label: 'Page/User Name', placeholder: 'CyberMindFR' }, + 'Twitter': { param: 'u', label: 'Username', placeholder: 'TheHackersNews' }, + 'Youtube': { param: 'c', label: 'Channel ID', placeholder: 'UCxxxxxxx' }, + 'Mastodon': { param: 'username', label: 'Username', placeholder: 'user', extra: 'instance', extraLabel: 'Instance', extraPlaceholder: 'mastodon.social' }, + 'Reddit': { param: 'r', label: 'Subreddit', placeholder: 'netsec' }, + 'Instagram': { param: 'u', label: 'Username', placeholder: 'username' } + }; + + var config = fields[bridge] || { param: 'u', label: 'Username', placeholder: 'username' }; + + var modalContent = [ + E('div', { 'style': 'margin-bottom: 16px;' }, [ + E('p', { 'style': 'color: var(--cf-text-dim);' }, + 'Add a ' + bridge + ' feed via RSS-Bridge') + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Feed Name'), + E('input', { + 'type': 'text', + 'id': 'bridge-name', + 'class': 'cf-form-input', + 'placeholder': bridge.toLowerCase() + '_feed' + }) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, config.label), + E('input', { + 'type': 'text', + 'id': 'bridge-param', + 'class': 'cf-form-input', + 'placeholder': config.placeholder + }) + ]) + ]; + + if (config.extra) { + modalContent.push(E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, config.extraLabel), + E('input', { + 'type': 'text', + 'id': 'bridge-extra', + 'class': 'cf-form-input', + 'placeholder': config.extraPlaceholder + }) + ])); + } + + modalContent.push(E('div', { 'style': 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-secondary', + 'click': ui.hideModal + }, 'Cancel'), + E('button', { + 'class': 'cf-btn cf-btn-primary', + 'click': function() { + var name = document.getElementById('bridge-name').value.trim(); + var param = document.getElementById('bridge-param').value.trim(); + var extra = config.extra ? document.getElementById('bridge-extra').value.trim() : ''; + + if (!name || !param) { + self.showToast('Please fill in all fields', 'error'); + return; + } + + var url = 'http://localhost:3000/?action=display&bridge=' + bridge; + url += '&' + config.param + '=' + encodeURIComponent(param); + if (config.extra && extra) { + url += '&' + config.extra + '=' + encodeURIComponent(extra); + } + url += '&format=Atom'; + + ui.hideModal(); + api.addFeed(name, url, 'rss-bridge', 'social').then(function(res) { + if (res && res.success) { + self.showToast('Feed added successfully', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + } + }, 'Add Feed') + ])); + + ui.showModal(bridge + ' Feed', modalContent); + }, + + showToast: function(message, type) { + var existing = document.querySelector('.cf-toast'); + if (existing) existing.remove(); + + var iconMap = { + 'success': '\u2705', + 'error': '\u274C', + 'warning': '\u26A0\uFE0F', + 'info': '\u2139\uFE0F' + }; + + var toast = E('div', { 'class': 'cf-toast ' + (type || '') }, [ + E('span', {}, iconMap[type] || '\u2139\uFE0F'), + message + ]); + document.body.appendChild(toast); + + setTimeout(function() { + toast.remove(); + }, 4000); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/overview.js b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/overview.js new file mode 100644 index 00000000..bab524cb --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/overview.js @@ -0,0 +1,251 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require cyberfeed.api as api'; + +return view.extend({ + title: _('CyberFeed Dashboard'), + pollRegistered: false, + + load: function() { + var cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = L.resource('cyberfeed/dashboard.css'); + document.head.appendChild(cssLink); + + return api.getDashboardData(); + }, + + render: function(data) { + var self = this; + var status = data.status || {}; + var feeds = Array.isArray(data.feeds) ? data.feeds : []; + var items = Array.isArray(data.items) ? data.items : []; + var rssbridge = data.rssbridge || {}; + + var lastSync = status.last_sync ? new Date(status.last_sync * 1000).toLocaleString() : 'Never'; + + var content = []; + + // Header + content.push(E('div', { 'class': 'cf-header' }, [ + E('h1', {}, '\u26A1 CYBERFEED \u26A1'), + E('p', { 'class': 'subtitle' }, 'NEURAL RSS MATRIX INTERFACE') + ])); + + // Stats Grid + content.push(E('div', { 'class': 'cf-stats-grid' }, [ + E('div', { 'class': 'cf-stat-card' }, [ + E('div', { 'class': 'icon' }, '\uD83D\uDCE1'), + E('div', { 'class': 'value' }, String(status.feed_count || 0)), + E('div', { 'class': 'label' }, 'Feed Sources') + ]), + E('div', { 'class': 'cf-stat-card' }, [ + E('div', { 'class': 'icon' }, '\uD83D\uDCCB'), + E('div', { 'class': 'value' }, String(status.item_count || 0)), + E('div', { 'class': 'label' }, 'Total Items') + ]), + E('div', { 'class': 'cf-stat-card' }, [ + E('div', { 'class': 'icon' }, '\u23F0'), + E('div', { 'class': 'value' }, lastSync.split(' ')[1] || '--:--'), + E('div', { 'class': 'label' }, 'Last Sync') + ]), + E('div', { 'class': 'cf-stat-card' }, [ + E('div', { 'class': 'icon' }, status.enabled ? '\u2705' : '\u26D4'), + E('div', { 'class': 'value' }, status.enabled ? 'ON' : 'OFF'), + E('div', { 'class': 'label' }, 'Service Status') + ]) + ])); + + // Quick Actions Card + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\u26A1'), + 'Quick Actions' + ]) + ]), + E('div', { 'class': 'cf-card-body' }, [ + E('div', { 'class': 'cf-quick-actions' }, [ + E('button', { + 'class': 'cf-btn cf-btn-primary', + 'click': function() { self.handleSync(); } + }, ['\uD83D\uDD04', ' Sync Now']), + E('a', { + 'href': L.url('admin/services/cyberfeed/feeds'), + 'class': 'cf-btn cf-btn-secondary' + }, ['\u2795', ' Add Feed']), + E('a', { + 'href': '/cyberfeed/', + 'target': '_blank', + 'class': 'cf-btn' + }, ['\uD83C\uDF10', ' Open Web View']), + E('a', { + 'href': L.url('admin/services/cyberfeed/preview'), + 'class': 'cf-btn' + }, ['\uD83D\uDC41', ' Preview']) + ]) + ]) + ])); + + // RSS-Bridge Card + var rssbridgeStatus = rssbridge.running ? 'Running' : (rssbridge.installed ? 'Stopped' : 'Not Installed'); + var rssbridgeBadgeClass = rssbridge.running ? 'cf-badge-success' : (rssbridge.installed ? 'cf-badge-warning' : 'cf-badge-danger'); + + content.push(E('div', { 'class': 'cf-card cf-rssbridge-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83C\uDF09'), + 'RSS-Bridge (Social Media)' + ]), + E('span', { 'class': 'cf-badge ' + rssbridgeBadgeClass }, rssbridgeStatus) + ]), + E('div', { 'class': 'cf-card-body' }, [ + E('p', { 'style': 'margin-bottom: 16px; color: var(--cf-text-dim);' }, + 'RSS-Bridge converts Facebook, Twitter, YouTube and other platforms to RSS feeds.'), + E('div', { 'class': 'cf-quick-actions' }, + rssbridge.installed ? [ + E('button', { + 'class': 'cf-btn ' + (rssbridge.running ? 'cf-btn-danger' : 'cf-btn-primary'), + 'click': function() { self.handleRssBridge(rssbridge.running ? 'stop' : 'start'); } + }, rssbridge.running ? ['\u23F9', ' Stop'] : ['\u25B6', ' Start']), + rssbridge.running ? E('a', { + 'href': 'http://' + window.location.hostname + ':' + (rssbridge.port || 3000), + 'target': '_blank', + 'class': 'cf-btn' + }, ['\uD83C\uDF10', ' Open Bridge UI']) : null + ].filter(Boolean) : [ + E('button', { + 'class': 'cf-btn cf-btn-primary', + 'click': function() { self.handleRssBridgeInstall(); } + }, ['\uD83D\uDCE5', ' Install RSS-Bridge']) + ] + ) + ]) + ])); + + // Recent Items Card + var recentItems = items.slice(0, 5); + var itemsContent; + + if (recentItems.length === 0) { + itemsContent = E('div', { 'class': 'cf-empty' }, [ + E('div', { 'class': 'cf-empty-icon' }, '\uD83D\uDD2E'), + E('div', { 'class': 'cf-empty-text' }, 'No feed items yet'), + E('div', { 'class': 'cf-empty-hint' }, 'Add feeds and sync to see content') + ]); + } else { + itemsContent = E('div', {}, recentItems.map(function(item) { + return E('div', { 'class': 'cf-feed-item' }, [ + E('div', { 'class': 'meta' }, [ + E('span', { 'class': 'timestamp' }, '\u23F0 ' + (item.date || 'Unknown')), + E('div', {}, [ + E('span', { 'class': 'cf-badge cf-badge-info' }, item.source || 'RSS'), + item.category ? E('span', { 'class': 'cf-badge cf-badge-category', 'style': 'margin-left: 6px;' }, item.category) : null + ].filter(Boolean)) + ]), + E('div', { 'class': 'title' }, [ + item.link ? E('a', { 'href': item.link, 'target': '_blank' }, item.title || 'Untitled') : (item.title || 'Untitled') + ]), + item.desc ? E('div', { 'class': 'description' }, item.desc) : null + ].filter(Boolean)); + })); + } + + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCCB'), + 'Recent Items (' + items.length + ')' + ]), + E('a', { + 'href': L.url('admin/services/cyberfeed/preview'), + 'class': 'cf-btn cf-btn-sm' + }, 'View All') + ]), + E('div', { 'class': 'cf-card-body' }, [itemsContent]) + ])); + + var view = E('div', { 'class': 'cyberfeed-dashboard' }, content); + + if (!this.pollRegistered) { + this.pollRegistered = true; + poll.add(function() { + return api.getDashboardData().then(function(newData) { + var container = document.querySelector('.cyberfeed-dashboard'); + if (container) { + var newView = self.render(newData); + container.parentNode.replaceChild(newView, container); + } + }); + }, 60); + } + + return view; + }, + + handleSync: function() { + var self = this; + this.showToast('Syncing feeds...', 'info'); + + return api.syncFeeds().then(function(res) { + if (res && res.success) { + self.showToast('Sync started', 'success'); + } else { + self.showToast('Sync failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + handleRssBridge: function(action) { + var self = this; + return api.controlRssBridge(action).then(function(res) { + if (res && res.success) { + self.showToast('RSS-Bridge ' + action + 'ed', 'success'); + window.location.reload(); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + handleRssBridgeInstall: function() { + var self = this; + this.showToast('Installing RSS-Bridge...', 'info'); + + return api.installRssBridge().then(function(res) { + if (res && res.success) { + self.showToast('Installation started', 'success'); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + showToast: function(message, type) { + var existing = document.querySelector('.cf-toast'); + if (existing) existing.remove(); + + var iconMap = { + 'success': '\u2705', + 'error': '\u274C', + 'warning': '\u26A0\uFE0F', + 'info': '\u2139\uFE0F' + }; + + var toast = E('div', { 'class': 'cf-toast ' + (type || '') }, [ + E('span', {}, iconMap[type] || '\u2139\uFE0F'), + message + ]); + document.body.appendChild(toast); + + setTimeout(function() { + toast.remove(); + }, 4000); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/preview.js b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/preview.js new file mode 100644 index 00000000..efb65824 --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/preview.js @@ -0,0 +1,138 @@ +'use strict'; +'require view'; +'require dom'; +'require cyberfeed.api as api'; + +return view.extend({ + title: _('Feed Preview'), + + load: function() { + var cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = L.resource('cyberfeed/dashboard.css'); + document.head.appendChild(cssLink); + + return api.getItems(); + }, + + render: function(items) { + var self = this; + items = Array.isArray(items) ? items : []; + + var categories = ['all']; + items.forEach(function(item) { + if (item.category && categories.indexOf(item.category) === -1) { + categories.push(item.category); + } + }); + + var content = []; + + // Header + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDC41'), + 'Feed Preview' + ]), + E('div', { 'style': 'display: flex; gap: 12px;' }, [ + E('a', { + 'href': L.url('admin/services/cyberfeed/overview'), + 'class': 'cf-btn cf-btn-sm cf-btn-secondary' + }, ['\u2190', ' Back']), + E('a', { + 'href': '/cyberfeed/', + 'target': '_blank', + 'class': 'cf-btn cf-btn-sm' + }, ['\uD83C\uDF10', ' Full View']) + ]) + ]) + ])); + + // Filter buttons + content.push(E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px;' }, + categories.map(function(cat) { + return E('button', { + 'class': 'cf-btn cf-btn-sm' + (cat === 'all' ? ' cf-btn-primary' : ''), + 'data-category': cat, + 'click': function(ev) { + self.filterItems(cat, ev.target); + } + }, cat.toUpperCase()); + }) + )); + + // Items container + var itemsContainer; + if (items.length === 0) { + itemsContainer = E('div', { 'class': 'cf-empty' }, [ + E('div', { 'class': 'cf-empty-icon' }, '\uD83D\uDD2E'), + E('div', { 'class': 'cf-empty-text' }, 'No feed items'), + E('div', { 'class': 'cf-empty-hint' }, 'Sync your feeds to see content here') + ]); + } else { + itemsContainer = E('div', { 'id': 'feed-items-container' }, + items.map(function(item) { + return E('div', { + 'class': 'cf-feed-item', + 'data-category': item.category || 'custom' + }, [ + E('div', { 'class': 'meta' }, [ + E('span', { 'class': 'timestamp' }, '\u23F0 ' + (item.date || 'Unknown')), + E('div', {}, [ + E('span', { 'class': 'cf-badge cf-badge-info' }, item.source || 'RSS'), + item.category ? E('span', { + 'class': 'cf-badge cf-badge-category', + 'style': 'margin-left: 6px;' + }, item.category) : null + ].filter(Boolean)) + ]), + E('div', { 'class': 'title' }, [ + item.link ? E('a', { + 'href': item.link, + 'target': '_blank', + 'rel': 'noopener' + }, item.title || 'Untitled') : (item.title || 'Untitled') + ]), + item.desc ? E('div', { 'class': 'description' }, item.desc) : null + ].filter(Boolean)); + }) + ); + } + + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCCB'), + 'Feed Items (' + items.length + ')' + ]) + ]), + E('div', { 'class': 'cf-card-body' }, [itemsContainer]) + ])); + + return E('div', { 'class': 'cyberfeed-dashboard' }, content); + }, + + filterItems: function(category, button) { + // Update button styles + document.querySelectorAll('[data-category]').forEach(function(btn) { + if (btn.tagName === 'BUTTON') { + btn.classList.remove('cf-btn-primary'); + } + }); + button.classList.add('cf-btn-primary'); + + // Filter items + document.querySelectorAll('.cf-feed-item').forEach(function(item) { + if (category === 'all' || item.dataset.category === category) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/settings.js b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/settings.js new file mode 100644 index 00000000..4399e1cc --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/htdocs/luci-static/resources/view/cyberfeed/settings.js @@ -0,0 +1,213 @@ +'use strict'; +'require view'; +'require dom'; +'require cyberfeed.api as api'; + +return view.extend({ + title: _('CyberFeed Settings'), + + load: function() { + var cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = L.resource('cyberfeed/dashboard.css'); + document.head.appendChild(cssLink); + + return Promise.all([ + api.getConfig(), + api.getRssBridgeStatus() + ]); + }, + + render: function(data) { + var self = this; + var config = data[0] || {}; + var rssbridge = data[1] || {}; + + var content = []; + + // Header + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\u2699\uFE0F'), + 'Settings' + ]), + E('a', { + 'href': L.url('admin/services/cyberfeed/overview'), + 'class': 'cf-btn cf-btn-sm cf-btn-secondary' + }, ['\u2190', ' Back']) + ]) + ])); + + // General Settings + content.push(E('div', { 'class': 'cf-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83D\uDCCB'), + 'General Settings' + ]) + ]), + E('div', { 'class': 'cf-card-body' }, [ + E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 20px;' }, [ + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Service Enabled'), + E('select', { 'id': 'cfg-enabled', 'class': 'cf-form-input' }, [ + E('option', { 'value': '1', 'selected': config.enabled == 1 }, 'Enabled'), + E('option', { 'value': '0', 'selected': config.enabled == 0 }, 'Disabled') + ]) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Refresh Interval (minutes)'), + E('input', { + 'type': 'number', + 'id': 'cfg-refresh', + 'class': 'cf-form-input', + 'value': config.refresh_interval || 5, + 'min': 1, + 'max': 60 + }) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Max Items Per Feed'), + E('input', { + 'type': 'number', + 'id': 'cfg-maxitems', + 'class': 'cf-form-input', + 'value': config.max_items || 20, + 'min': 5, + 'max': 100 + }) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'Cache TTL (seconds)'), + E('input', { + 'type': 'number', + 'id': 'cfg-cachettl', + 'class': 'cf-form-input', + 'value': config.cache_ttl || 300, + 'min': 60, + 'max': 3600 + }) + ]) + ]) + ]) + ])); + + // RSS-Bridge Settings + content.push(E('div', { 'class': 'cf-card cf-rssbridge-card' }, [ + E('div', { 'class': 'cf-card-header' }, [ + E('div', { 'class': 'cf-card-title' }, [ + E('span', { 'class': 'cf-card-title-icon' }, '\uD83C\uDF09'), + 'RSS-Bridge Settings' + ]), + E('span', { + 'class': 'cf-badge ' + (rssbridge.running ? 'cf-badge-success' : 'cf-badge-warning') + }, rssbridge.running ? 'Running' : (rssbridge.installed ? 'Stopped' : 'Not Installed')) + ]), + E('div', { 'class': 'cf-card-body' }, [ + E('p', { 'style': 'margin-bottom: 16px; color: var(--cf-text-dim);' }, + 'RSS-Bridge allows you to subscribe to Facebook, Twitter, YouTube and many other platforms.'), + E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 20px;' }, [ + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'RSS-Bridge Enabled'), + E('select', { 'id': 'cfg-rssbridge-enabled', 'class': 'cf-form-input' }, [ + E('option', { 'value': '1', 'selected': config.rssbridge_enabled == 1 }, 'Enabled'), + E('option', { 'value': '0', 'selected': config.rssbridge_enabled == 0 }, 'Disabled') + ]) + ]), + E('div', { 'class': 'cf-form-group' }, [ + E('label', { 'class': 'cf-form-label' }, 'RSS-Bridge Port'), + E('input', { + 'type': 'number', + 'id': 'cfg-rssbridge-port', + 'class': 'cf-form-input', + 'value': config.rssbridge_port || 3000, + 'min': 1024, + 'max': 65535 + }) + ]) + ]), + !rssbridge.installed ? E('div', { 'style': 'margin-top: 16px;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-primary', + 'click': function() { self.handleInstallRssBridge(); } + }, ['\uD83D\uDCE5', ' Install RSS-Bridge']) + ]) : null + ].filter(Boolean)) + ])); + + // Save Button + content.push(E('div', { 'style': 'margin-top: 20px; display: flex; gap: 12px;' }, [ + E('button', { + 'class': 'cf-btn cf-btn-primary', + 'click': function() { self.handleSaveConfig(); } + }, ['\uD83D\uDCBE', ' Save Settings']), + E('button', { + 'class': 'cf-btn cf-btn-secondary', + 'click': function() { window.location.reload(); } + }, ['\u21BA', ' Reset']) + ])); + + return E('div', { 'class': 'cyberfeed-dashboard' }, content); + }, + + handleSaveConfig: function() { + var self = this; + + var config = { + enabled: parseInt(document.getElementById('cfg-enabled').value, 10), + refresh_interval: parseInt(document.getElementById('cfg-refresh').value, 10), + max_items: parseInt(document.getElementById('cfg-maxitems').value, 10), + cache_ttl: parseInt(document.getElementById('cfg-cachettl').value, 10), + rssbridge_enabled: parseInt(document.getElementById('cfg-rssbridge-enabled').value, 10), + rssbridge_port: parseInt(document.getElementById('cfg-rssbridge-port').value, 10) + }; + + return api.saveConfig(config).then(function(res) { + if (res && res.success) { + self.showToast('Settings saved', 'success'); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + handleInstallRssBridge: function() { + var self = this; + this.showToast('Installing RSS-Bridge...', 'info'); + + return api.installRssBridge().then(function(res) { + if (res && res.success) { + self.showToast('Installation started', 'success'); + } else { + self.showToast('Failed: ' + (res.error || 'Unknown error'), 'error'); + } + }); + }, + + showToast: function(message, type) { + var existing = document.querySelector('.cf-toast'); + if (existing) existing.remove(); + + var iconMap = { + 'success': '\u2705', + 'error': '\u274C', + 'warning': '\u26A0\uFE0F', + 'info': '\u2139\uFE0F' + }; + + var toast = E('div', { 'class': 'cf-toast ' + (type || '') }, [ + E('span', {}, iconMap[type] || '\u2139\uFE0F'), + message + ]); + document.body.appendChild(toast); + + setTimeout(function() { + toast.remove(); + }, 4000); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-cyberfeed/root/usr/libexec/rpcd/luci.cyberfeed b/package/secubox/luci-app-cyberfeed/root/usr/libexec/rpcd/luci.cyberfeed new file mode 100644 index 00000000..611b9114 --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/root/usr/libexec/rpcd/luci.cyberfeed @@ -0,0 +1,303 @@ +#!/bin/sh +# RPCD backend for CyberFeed LuCI App + +. /lib/functions.sh + +CYBERFEED_BIN="/usr/bin/cyberfeed" +RSSBRIDGE_BIN="/usr/bin/rss-bridge-setup" +OUTPUT_DIR="/tmp/cyberfeed/output" +CONFIG_FILE="/etc/cyberfeed/feeds.conf" + +json_init() { + JSON_OUTPUT="" +} + +json_add_string() { + local key="$1" val="$2" + val=$(echo "$val" | sed 's/"/\\"/g; s/\\/\\\\/g') + [ -n "$JSON_OUTPUT" ] && JSON_OUTPUT="$JSON_OUTPUT," + JSON_OUTPUT="$JSON_OUTPUT\"$key\":\"$val\"" +} + +json_add_int() { + local key="$1" val="$2" + [ -n "$JSON_OUTPUT" ] && JSON_OUTPUT="$JSON_OUTPUT," + JSON_OUTPUT="$JSON_OUTPUT\"$key\":$val" +} + +json_add_bool() { + local key="$1" val="$2" + [ -n "$JSON_OUTPUT" ] && JSON_OUTPUT="$JSON_OUTPUT," + if [ "$val" = "1" ] || [ "$val" = "true" ]; then + JSON_OUTPUT="$JSON_OUTPUT\"$key\":true" + else + JSON_OUTPUT="$JSON_OUTPUT\"$key\":false" + fi +} + +json_dump() { + echo "{$JSON_OUTPUT}" +} + +# Get service status +get_status() { + local enabled=$(uci -q get cyberfeed.main.enabled || echo 0) + local feed_count=0 + local item_count=0 + local last_sync=0 + local rssbridge_enabled=$(uci -q get cyberfeed.rssbridge.enabled || echo 0) + local rssbridge_running="false" + + if [ -f "${OUTPUT_DIR}/feeds.json" ]; then + item_count=$(grep -c '"title"' "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0) + last_sync=$(stat -c %Y "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0) + fi + + if [ -f "$CONFIG_FILE" ]; then + feed_count=$(grep -v "^#" "$CONFIG_FILE" 2>/dev/null | grep -c "|" || echo 0) + fi + + if [ "$rssbridge_enabled" = "1" ]; then + if pgrep -f "rss-bridge\|php.*3000" >/dev/null 2>&1; then + rssbridge_running="true" + fi + fi + + cat << EOF +{ + "enabled": $enabled, + "feed_count": $feed_count, + "item_count": $item_count, + "last_sync": $last_sync, + "rssbridge_enabled": $rssbridge_enabled, + "rssbridge_running": $rssbridge_running +} +EOF +} + +# Get feeds list +get_feeds() { + if [ -x "$CYBERFEED_BIN" ]; then + $CYBERFEED_BIN list + else + echo "[]" + fi +} + +# Get feed items +get_items() { + if [ -f "${OUTPUT_DIR}/feeds.json" ]; then + cat "${OUTPUT_DIR}/feeds.json" + else + echo "[]" + fi +} + +# Add a feed +add_feed() { + local name="$1" + local url="$2" + local type="${3:-rss}" + local category="${4:-custom}" + + if [ -z "$name" ] || [ -z "$url" ]; then + echo '{"success":false,"error":"Name and URL are required"}' + return + fi + + # Validate name (alphanumeric and underscore only) + if ! echo "$name" | grep -qE '^[a-zA-Z0-9_-]+$'; then + echo '{"success":false,"error":"Invalid name format"}' + return + fi + + # Check for duplicates + if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then + echo '{"success":false,"error":"Feed already exists"}' + return + fi + + echo "${name}|${url}|${type}|${category}" >> "$CONFIG_FILE" + echo '{"success":true}' +} + +# Delete a feed +delete_feed() { + local name="$1" + + if [ -z "$name" ]; then + echo '{"success":false,"error":"Name is required"}' + return + fi + + if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then + sed -i "/^${name}|/d" "$CONFIG_FILE" + rm -f "/tmp/cyberfeed/cache/${name}.xml" + echo '{"success":true}' + else + echo '{"success":false,"error":"Feed not found"}' + fi +} + +# Sync feeds +sync_feeds() { + if [ -x "$CYBERFEED_BIN" ]; then + $CYBERFEED_BIN sync >/dev/null 2>&1 & + echo '{"success":true,"message":"Sync started"}' + else + echo '{"success":false,"error":"CyberFeed not installed"}' + fi +} + +# Get config +get_config() { + local enabled=$(uci -q get cyberfeed.main.enabled || echo 0) + local refresh=$(uci -q get cyberfeed.main.refresh_interval || echo 5) + local max_items=$(uci -q get cyberfeed.main.max_items || echo 20) + local cache_ttl=$(uci -q get cyberfeed.main.cache_ttl || echo 300) + local rssbridge_enabled=$(uci -q get cyberfeed.rssbridge.enabled || echo 0) + local rssbridge_port=$(uci -q get cyberfeed.rssbridge.port || echo 3000) + + cat << EOF +{ + "enabled": $enabled, + "refresh_interval": $refresh, + "max_items": $max_items, + "cache_ttl": $cache_ttl, + "rssbridge_enabled": $rssbridge_enabled, + "rssbridge_port": $rssbridge_port +} +EOF +} + +# Save config +save_config() { + local enabled="$1" + local refresh="$2" + local max_items="$3" + local cache_ttl="$4" + local rssbridge_enabled="$5" + local rssbridge_port="$6" + + [ -n "$enabled" ] && uci set cyberfeed.main.enabled="$enabled" + [ -n "$refresh" ] && uci set cyberfeed.main.refresh_interval="$refresh" + [ -n "$max_items" ] && uci set cyberfeed.main.max_items="$max_items" + [ -n "$cache_ttl" ] && uci set cyberfeed.main.cache_ttl="$cache_ttl" + [ -n "$rssbridge_enabled" ] && uci set cyberfeed.rssbridge.enabled="$rssbridge_enabled" + [ -n "$rssbridge_port" ] && uci set cyberfeed.rssbridge.port="$rssbridge_port" + + uci commit cyberfeed + + # Restart service if needed + /etc/init.d/cyberfeed reload 2>/dev/null + + echo '{"success":true}' +} + +# RSS-Bridge status +rssbridge_status() { + if [ -x "$RSSBRIDGE_BIN" ]; then + $RSSBRIDGE_BIN status + else + echo '{"installed":false,"enabled":false,"running":false}' + fi +} + +# RSS-Bridge install +rssbridge_install() { + if [ -x "$RSSBRIDGE_BIN" ]; then + $RSSBRIDGE_BIN install >/dev/null 2>&1 & + echo '{"success":true,"message":"Installation started"}' + else + echo '{"success":false,"error":"Setup script not found"}' + fi +} + +# RSS-Bridge start/stop +rssbridge_control() { + local action="$1" + + if [ -x "$RSSBRIDGE_BIN" ]; then + $RSSBRIDGE_BIN "$action" >/dev/null 2>&1 + echo '{"success":true}' + else + echo '{"success":false,"error":"Setup script not found"}' + fi +} + +# RPCD interface +case "$1" in + list) + cat << 'EOF' +{ + "get_status": {}, + "get_feeds": {}, + "get_items": {}, + "add_feed": {"name":"str","url":"str","type":"str","category":"str"}, + "delete_feed": {"name":"str"}, + "sync_feeds": {}, + "get_config": {}, + "save_config": {"enabled":"int","refresh_interval":"int","max_items":"int","cache_ttl":"int","rssbridge_enabled":"int","rssbridge_port":"int"}, + "rssbridge_status": {}, + "rssbridge_install": {}, + "rssbridge_control": {"action":"str"} +} +EOF + ;; + call) + case "$2" in + get_status) + get_status + ;; + get_feeds) + get_feeds + ;; + get_items) + get_items + ;; + add_feed) + read -r input + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + url=$(echo "$input" | jsonfilter -e '@.url' 2>/dev/null) + type=$(echo "$input" | jsonfilter -e '@.type' 2>/dev/null) + category=$(echo "$input" | jsonfilter -e '@.category' 2>/dev/null) + add_feed "$name" "$url" "$type" "$category" + ;; + delete_feed) + read -r input + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + delete_feed "$name" + ;; + sync_feeds) + sync_feeds + ;; + get_config) + get_config + ;; + save_config) + read -r input + enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null) + refresh=$(echo "$input" | jsonfilter -e '@.refresh_interval' 2>/dev/null) + max_items=$(echo "$input" | jsonfilter -e '@.max_items' 2>/dev/null) + cache_ttl=$(echo "$input" | jsonfilter -e '@.cache_ttl' 2>/dev/null) + rssbridge_enabled=$(echo "$input" | jsonfilter -e '@.rssbridge_enabled' 2>/dev/null) + rssbridge_port=$(echo "$input" | jsonfilter -e '@.rssbridge_port' 2>/dev/null) + save_config "$enabled" "$refresh" "$max_items" "$cache_ttl" "$rssbridge_enabled" "$rssbridge_port" + ;; + rssbridge_status) + rssbridge_status + ;; + rssbridge_install) + rssbridge_install + ;; + rssbridge_control) + read -r input + action=$(echo "$input" | jsonfilter -e '@.action' 2>/dev/null) + rssbridge_control "$action" + ;; + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-cyberfeed/root/usr/share/luci/menu.d/luci-app-cyberfeed.json b/package/secubox/luci-app-cyberfeed/root/usr/share/luci/menu.d/luci-app-cyberfeed.json new file mode 100644 index 00000000..fa77b98d --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/root/usr/share/luci/menu.d/luci-app-cyberfeed.json @@ -0,0 +1,45 @@ +{ + "admin/services/cyberfeed": { + "title": "CyberFeed", + "order": 85, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-cyberfeed"], + "uci": {"cyberfeed": true} + } + }, + "admin/services/cyberfeed/overview": { + "title": "Dashboard", + "order": 10, + "action": { + "type": "view", + "path": "cyberfeed/overview" + } + }, + "admin/services/cyberfeed/feeds": { + "title": "Feeds", + "order": 20, + "action": { + "type": "view", + "path": "cyberfeed/feeds" + } + }, + "admin/services/cyberfeed/preview": { + "title": "Preview", + "order": 30, + "action": { + "type": "view", + "path": "cyberfeed/preview" + } + }, + "admin/services/cyberfeed/settings": { + "title": "Settings", + "order": 40, + "action": { + "type": "view", + "path": "cyberfeed/settings" + } + } +} diff --git a/package/secubox/luci-app-cyberfeed/root/usr/share/rpcd/acl.d/luci-app-cyberfeed.json b/package/secubox/luci-app-cyberfeed/root/usr/share/rpcd/acl.d/luci-app-cyberfeed.json new file mode 100644 index 00000000..7eefd3df --- /dev/null +++ b/package/secubox/luci-app-cyberfeed/root/usr/share/rpcd/acl.d/luci-app-cyberfeed.json @@ -0,0 +1,17 @@ +{ + "luci-app-cyberfeed": { + "description": "Grant access to CyberFeed RSS Aggregator", + "read": { + "ubus": { + "luci.cyberfeed": ["get_status", "get_feeds", "get_items", "get_config", "rssbridge_status"] + }, + "uci": ["cyberfeed"] + }, + "write": { + "ubus": { + "luci.cyberfeed": ["add_feed", "delete_feed", "sync_feeds", "save_config", "rssbridge_install", "rssbridge_control"] + }, + "uci": ["cyberfeed"] + } + } +} diff --git a/package/secubox/secubox-app-cyberfeed/Makefile b/package/secubox/secubox-app-cyberfeed/Makefile new file mode 100644 index 00000000..53f16b27 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/Makefile @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: MIT +# SecuBox CyberFeed - Cyberpunk RSS Aggregator +# Copyright (C) 2025 CyberMind.fr + +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-cyberfeed +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=CyberMind +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-cyberfeed + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=CyberFeed - Cyberpunk RSS Aggregator + DEPENDS:=+wget-ssl +jsonfilter +coreutils-stat + PKGARCH:=all +endef + +define Package/secubox-app-cyberfeed/description + Cyberpunk-themed RSS feed aggregator for OpenWrt/SecuBox. + Features emoji injection, neon styling, and RSS-Bridge support + for social media feeds (Facebook, Twitter, Mastodon). +endef + +define Package/secubox-app-cyberfeed/conffiles +/etc/config/cyberfeed +/etc/cyberfeed/feeds.conf +endef + +define Build/Compile +endef + +define Package/secubox-app-cyberfeed/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/cyberfeed $(1)/etc/config/cyberfeed + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/cyberfeed $(1)/etc/init.d/cyberfeed + + $(INSTALL_DIR) $(1)/etc/cron.d + $(INSTALL_DATA) ./files/etc/cron.d/cyberfeed $(1)/etc/cron.d/cyberfeed + + $(INSTALL_DIR) $(1)/etc/cyberfeed + $(INSTALL_DATA) ./files/etc/cyberfeed/feeds.conf $(1)/etc/cyberfeed/feeds.conf + + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) ./files/usr/bin/cyberfeed $(1)/usr/bin/cyberfeed + $(INSTALL_BIN) ./files/usr/bin/rss-bridge-setup $(1)/usr/bin/rss-bridge-setup + + $(INSTALL_DIR) $(1)/usr/share/cyberfeed + $(INSTALL_DATA) ./files/usr/share/cyberfeed/template.html $(1)/usr/share/cyberfeed/template.html + + $(INSTALL_DIR) $(1)/www/cyberfeed +endef + +define Package/secubox-app-cyberfeed/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + # Create output directories + mkdir -p /tmp/cyberfeed/cache /tmp/cyberfeed/output + # Create symlink for web access + [ -L /www/cyberfeed/index.html ] || ln -sf /tmp/cyberfeed/output/index.html /www/cyberfeed/index.html 2>/dev/null + # Enable cron + /etc/init.d/cron restart 2>/dev/null +} +exit 0 +endef + +$(eval $(call BuildPackage,secubox-app-cyberfeed)) diff --git a/package/secubox/secubox-app-cyberfeed/files/etc/config/cyberfeed b/package/secubox/secubox-app-cyberfeed/files/etc/config/cyberfeed new file mode 100644 index 00000000..f86600f1 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/etc/config/cyberfeed @@ -0,0 +1,15 @@ +config cyberfeed 'main' + option enabled '1' + option refresh_interval '5' + option max_items '20' + option cache_ttl '300' + option output_dir '/tmp/cyberfeed/output' + option theme 'cyberpunk' + +config rssbridge 'rssbridge' + option enabled '0' + option port '3000' + option container 'rss-bridge' + +# Feed sources - managed via /etc/cyberfeed/feeds.conf +# or through LuCI interface diff --git a/package/secubox/secubox-app-cyberfeed/files/etc/cron.d/cyberfeed b/package/secubox/secubox-app-cyberfeed/files/etc/cron.d/cyberfeed new file mode 100644 index 00000000..dcecfc53 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/etc/cron.d/cyberfeed @@ -0,0 +1,3 @@ +# CyberFeed auto-refresh +# Runs every 5 minutes by default +*/5 * * * * root [ -x /usr/bin/cyberfeed ] && /usr/bin/cyberfeed sync >/dev/null 2>&1 diff --git a/package/secubox/secubox-app-cyberfeed/files/etc/cyberfeed/feeds.conf b/package/secubox/secubox-app-cyberfeed/files/etc/cyberfeed/feeds.conf new file mode 100644 index 00000000..cf2a2968 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/etc/cyberfeed/feeds.conf @@ -0,0 +1,36 @@ +# ╔═══════════════════════════════════════════════════════════════════╗ +# ║ ⚡ CYBERFEED - Feed Configuration ⚡ ║ +# ╚═══════════════════════════════════════════════════════════════════╝ +# +# Format: NAME|URL|TYPE|CATEGORY +# TYPE: rss, atom, rss-bridge +# CATEGORY: security, tech, social, news, custom +# +# Lines starting with # are comments + +# === Security Feeds === +crowdsec|https://www.crowdsec.net/feed|rss|security +#threatpost|https://threatpost.com/feed/|rss|security +#krebs|https://krebsonsecurity.com/feed/|rss|security +#sans|https://isc.sans.edu/rssfeed.xml|rss|security + +# === Tech News === +hackernews|https://news.ycombinator.com/rss|rss|tech +#openwrt|https://openwrt.org/feed.php|rss|tech +#lwn|https://lwn.net/headlines/rss|rss|tech + +# === Social Media (via RSS-Bridge) === +# Requires RSS-Bridge to be enabled and running +# Configure in LuCI: Services > CyberFeed > RSS-Bridge +# +# Facebook Page: +#cybermind_fb|http://localhost:3000/?action=display&bridge=Facebook&u=CyberMindFR&format=Atom|rss-bridge|social +# +# Twitter/X: +#hackernews_x|http://localhost:3000/?action=display&bridge=Twitter&u=TheHackersNews&format=Atom|rss-bridge|social +# +# Mastodon: +#infosec_masto|http://localhost:3000/?action=display&bridge=Mastodon&instance=infosec.exchange&username=user&format=Atom|rss-bridge|social +# +# YouTube Channel: +#channel_yt|http://localhost:3000/?action=display&bridge=Youtube&c=CHANNEL_ID&format=Atom|rss-bridge|social diff --git a/package/secubox/secubox-app-cyberfeed/files/etc/init.d/cyberfeed b/package/secubox/secubox-app-cyberfeed/files/etc/init.d/cyberfeed new file mode 100644 index 00000000..5fd9cb25 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/etc/init.d/cyberfeed @@ -0,0 +1,38 @@ +#!/bin/sh /etc/rc.common +# CyberFeed init script + +START=95 +STOP=10 +USE_PROCD=1 + +PROG=/usr/bin/cyberfeed + +start_service() { + local enabled + config_load cyberfeed + config_get enabled main enabled 0 + + [ "$enabled" = "1" ] || return 0 + + # Create directories + mkdir -p /tmp/cyberfeed/cache /tmp/cyberfeed/output + + # Create web symlink + [ -L /www/cyberfeed/index.html ] || ln -sf /tmp/cyberfeed/output/index.html /www/cyberfeed/index.html 2>/dev/null + + # Run initial sync + $PROG sync & +} + +stop_service() { + # Nothing to stop - runs via cron + : +} + +reload_service() { + $PROG sync & +} + +service_triggers() { + procd_add_reload_trigger "cyberfeed" +} diff --git a/package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed b/package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed new file mode 100644 index 00000000..4c9b7221 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed @@ -0,0 +1,615 @@ +#!/bin/sh +# ╔═══════════════════════════════════════════════════════════════════╗ +# ║ ⚡ CYBERFEED v0.1 - RSS Aggregator for OpenWrt/SecuBox ⚡ ║ +# ║ Cyberpunk Social Feed Analyzer with Emoji Enhancement ║ +# ║ Author: CyberMind.FR | License: MIT ║ +# ╚═══════════════════════════════════════════════════════════════════╝ + +. /lib/functions.sh + +# === CONFIGURATION === +CYBERFEED_DIR="/tmp/cyberfeed" +CACHE_DIR="${CYBERFEED_DIR}/cache" +OUTPUT_DIR="${CYBERFEED_DIR}/output" +CONFIG_FILE="/etc/cyberfeed/feeds.conf" +TEMPLATE_FILE="/usr/share/cyberfeed/template.html" +MAX_ITEMS=20 +CACHE_TTL=300 + +# Load UCI config +load_config() { + config_load cyberfeed + config_get MAX_ITEMS main max_items 20 + config_get CACHE_TTL main cache_ttl 300 + config_get OUTPUT_DIR main output_dir "/tmp/cyberfeed/output" +} + +# === CYBERPUNK EMOJI MAPPING === +cyberpunk_emojify() { + local text="$1" + + # Security/Hacking themes + echo "$text" | sed -E ' + s/(hack|breach|exploit|vulnerab)/\xf0\x9f\x94\x93\1/gi + s/(secur|protect|defense|firewall)/\xf0\x9f\x9b\xa1\xef\xb8\x8f\1/gi + s/(cyber|digital|virtual)/\xe2\x9a\xa1\1/gi + s/(encrypt|crypto|cipher)/\xf0\x9f\x94\x90\1/gi + s/(malware|virus|trojan)/\xe2\x98\xa0\xef\xb8\x8f\1/gi + s/(alert|warning|danger)/\xe2\x9a\xa0\xef\xb8\x8f\1/gi + s/(attack|threat|risk)/\xf0\x9f\x92\x80\1/gi + s/(network|connect|link)/\xf0\x9f\x8c\x90\1/gi + s/(server|cloud|data)/\xf0\x9f\x92\xbe\1/gi + s/(code|program|script)/\xf0\x9f\x92\xbb\1/gi + s/(linux|opensource|github)/\xf0\x9f\x90\xa7\1/gi + s/(robot|automat|ai|machine)/\xf0\x9f\xa4\x96\1/gi + s/(update|upgrade|patch)/\xf0\x9f\x93\xa1\1/gi + s/(launch|deploy|release)/\xf0\x9f\x9a\x80\1/gi + s/(new|annonce|breaking)/\xe2\x9c\xa8\1/gi + s/(success|win|achieve)/\xf0\x9f\x8f\x86\1/gi + s/(fail|error|bug)/\xf0\x9f\x90\x9b\1/gi + s/(magic|mystiq|oracle)/\xf0\x9f\x94\xae\1/gi + s/(energy|power|force)/\xe2\x9a\xa1\1/gi + ' +} + +# === RSS FETCHER === +fetch_feed() { + local url="$1" + local name="$2" + local cache_file="${CACHE_DIR}/${name}.xml" + + # Check cache freshness + if [ -f "$cache_file" ]; then + local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0) + local now=$(date +%s) + local age=$((now - file_time)) + if [ "$age" -lt "$CACHE_TTL" ]; then + cat "$cache_file" + return 0 + fi + fi + + # Fetch with wget (OpenWrt standard) + wget -q -T 15 -O "$cache_file" "$url" 2>/dev/null + + if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then + cat "$cache_file" + else + rm -f "$cache_file" + return 1 + fi +} + +# === RSS PARSER (Pure AWK for OpenWrt) === +parse_rss() { + local xml="$1" + local source="$2" + local category="$3" + + echo "$xml" | awk -v source="$source" -v category="$category" -v max="$MAX_ITEMS" ' + BEGIN { + RS="|" + item_count=0 + } + { + title="" + link="" + date="" + desc="" + + # Extract title + if (match($0, /]*>([^<]+)<\/title>/, arr)) title=arr[1] + else if (match($0, /]*><\/title>/, arr)) title=arr[1] + + # Extract link + if (match($0, /]*>([^<]+)<\/link>/, arr)) link=arr[1] + else if (match($0, /]*href="([^"]+)"/, arr)) link=arr[1] + + # Extract date + if (match($0, /([^<]+)([^<]+)([^<]+)]*>([^<]+)]*>([^<]+)]*>]+>/, "", desc) + desc = substr(desc, 1, 280) + + printf "{\"title\":\"%s\",\"link\":\"%s\",\"date\":\"%s\",\"desc\":\"%s\",\"source\":\"%s\",\"category\":\"%s\"},", title, link, date, desc, source, category + item_count++ + } + } + ' +} + +# === HTML GENERATOR === +generate_html() { + local json_file="$1" + local output_file="${OUTPUT_DIR}/index.html" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local feed_count=$(grep -c '"title"' "$json_file" 2>/dev/null || echo 0) + + cat > "$output_file" << 'HTMLEOF' + + + + + +⚡ CYBERFEED ⚡ Neural RSS Matrix + + + +
+

⚡ CYBERFEED ⚡

+

NEURAL RSS MATRIX INTERFACE

+
+ FEEDS: -- + SYNC: --:--:-- + STATUS: ONLINE +
+
+
+ + + + + +
+
+
+
🔮
+

Awaiting Neural Feed Connection...

+
+
+
+

⚡ CYBERFEED v0.1 | Powered by CyberMind.FR | SecuBox Module ⚡

+

░▒▓ JACK IN TO THE MATRIX ▓▒░

+
+ + + +HTMLEOF + + echo "$output_file" +} + +# === STATUS === +get_status() { + local enabled=$(uci -q get cyberfeed.main.enabled || echo 0) + local feed_count=0 + local last_sync="never" + local rssbridge_enabled=$(uci -q get cyberfeed.rssbridge.enabled || echo 0) + local rssbridge_status="stopped" + + if [ -f "${OUTPUT_DIR}/feeds.json" ]; then + feed_count=$(grep -c '"title"' "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0) + last_sync=$(stat -c %Y "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0) + fi + + if [ "$rssbridge_enabled" = "1" ]; then + if pgrep -f "rss-bridge" >/dev/null 2>&1; then + rssbridge_status="running" + fi + fi + + cat << EOF +{ + "enabled": $enabled, + "feed_count": $feed_count, + "last_sync": $last_sync, + "cache_ttl": $CACHE_TTL, + "max_items": $MAX_ITEMS, + "rssbridge_enabled": $rssbridge_enabled, + "rssbridge_status": "$rssbridge_status" +} +EOF +} + +# === SYNC FEEDS === +sync_feeds() { + load_config + + mkdir -p "$CACHE_DIR" "$OUTPUT_DIR" + + if [ ! -f "$CONFIG_FILE" ]; then + echo '[]' > "${OUTPUT_DIR}/feeds.json" + generate_html "${OUTPUT_DIR}/feeds.json" + return 0 + fi + + local json_items="[" + local feed_count=0 + + while IFS='|' read -r name url type category || [ -n "$name" ]; do + # Skip comments and empty lines + case "$name" in + ''|\#*) continue ;; + esac + + [ -z "$category" ] && category="custom" + + echo "📡 Fetching: $name" >&2 + + raw_xml=$(fetch_feed "$url" "$name") + if [ -n "$raw_xml" ]; then + parsed=$(parse_rss "$raw_xml" "$name" "$category") + if [ -n "$parsed" ]; then + emojified=$(cyberpunk_emojify "$parsed") + json_items="${json_items}${emojified}" + feed_count=$((feed_count + 1)) + fi + fi + done < "$CONFIG_FILE" + + # Remove trailing comma, close array + json_items=$(echo "$json_items" | sed 's/,$//') + json_items="${json_items}]" + + echo "$json_items" > "${OUTPUT_DIR}/feeds.json" + generate_html "${OUTPUT_DIR}/feeds.json" >/dev/null + + # Create symlink for web access + [ -L /www/cyberfeed/index.html ] || ln -sf "${OUTPUT_DIR}/index.html" /www/cyberfeed/index.html 2>/dev/null + [ -L /www/cyberfeed/feeds.json ] || ln -sf "${OUTPUT_DIR}/feeds.json" /www/cyberfeed/feeds.json 2>/dev/null + + echo "" + echo "╔═══════════════════════════════════════════════════════════╗" + echo "║ ⚡ CYBERFEED SYNC COMPLETE ⚡ ║" + echo "╠═══════════════════════════════════════════════════════════╣" + printf "║ 📊 Feeds processed: %-36s ║\n" "$feed_count" + echo "║ 📁 Output: /www/cyberfeed/ ║" + echo "╚═══════════════════════════════════════════════════════════╝" +} + +# === LIST FEEDS === +list_feeds() { + if [ ! -f "$CONFIG_FILE" ]; then + echo "[]" + return + fi + + echo "[" + local first=1 + while IFS='|' read -r name url type category || [ -n "$name" ]; do + case "$name" in + ''|\#*) continue ;; + esac + [ "$first" = "1" ] || echo "," + first=0 + printf '{"name":"%s","url":"%s","type":"%s","category":"%s"}' \ + "$name" "$url" "${type:-rss}" "${category:-custom}" + done < "$CONFIG_FILE" + echo "]" +} + +# === ADD FEED === +add_feed() { + local name="$1" + local url="$2" + local type="${3:-rss}" + local category="${4:-custom}" + + [ -z "$name" ] || [ -z "$url" ] && { + echo '{"success":false,"error":"Name and URL required"}' + return 1 + } + + # Check if feed already exists + if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then + echo '{"success":false,"error":"Feed already exists"}' + return 1 + fi + + echo "${name}|${url}|${type}|${category}" >> "$CONFIG_FILE" + echo '{"success":true}' +} + +# === DELETE FEED === +delete_feed() { + local name="$1" + + [ -z "$name" ] && { + echo '{"success":false,"error":"Name required"}' + return 1 + } + + if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then + sed -i "/^${name}|/d" "$CONFIG_FILE" + rm -f "${CACHE_DIR}/${name}.xml" + echo '{"success":true}' + else + echo '{"success":false,"error":"Feed not found"}' + return 1 + fi +} + +# === MAIN === +case "$1" in + sync) + sync_feeds + ;; + status) + get_status + ;; + list) + list_feeds + ;; + add) + add_feed "$2" "$3" "$4" "$5" + ;; + delete) + delete_feed "$2" + ;; + *) + echo "Usage: $0 {sync|status|list|add|delete}" + echo "" + echo "Commands:" + echo " sync Fetch and process all feeds" + echo " status Show service status (JSON)" + echo " list List configured feeds (JSON)" + echo " add NAME URL [TYPE] [CATEGORY]" + echo " Add a new feed" + echo " delete NAME Remove a feed" + ;; +esac diff --git a/package/secubox/secubox-app-cyberfeed/files/usr/bin/rss-bridge-setup b/package/secubox/secubox-app-cyberfeed/files/usr/bin/rss-bridge-setup new file mode 100644 index 00000000..a2234380 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/usr/bin/rss-bridge-setup @@ -0,0 +1,190 @@ +#!/bin/sh +# ╔═══════════════════════════════════════════════════════════════════╗ +# ║ RSS-Bridge Setup for CyberFeed ║ +# ║ Enables Facebook, Twitter, YouTube feeds via RSS-Bridge ║ +# ╚═══════════════════════════════════════════════════════════════════╝ + +RSSBRIDGE_DIR="/srv/rss-bridge" +RSSBRIDGE_PORT=$(uci -q get cyberfeed.rssbridge.port || echo 3000) + +print_banner() { + echo "" + echo "╔═══════════════════════════════════════════════════════════╗" + echo "║ 🌉 RSS-BRIDGE SETUP FOR CYBERFEED 🌉 ║" + echo "╚═══════════════════════════════════════════════════════════╝" + echo "" +} + +check_deps() { + echo "📦 Checking dependencies..." + + local missing="" + + command -v php >/dev/null 2>&1 || missing="$missing php8" + command -v php-cgi >/dev/null 2>&1 || missing="$missing php8-cgi" + + if [ -n "$missing" ]; then + echo "⚠️ Missing packages:$missing" + echo "" + echo "Install with:" + echo " opkg update" + echo " opkg install$missing php8-mod-curl php8-mod-json php8-mod-mbstring php8-mod-simplexml" + return 1 + fi + + echo "✅ Dependencies OK" + return 0 +} + +install_rssbridge() { + print_banner + + if ! check_deps; then + return 1 + fi + + echo "" + echo "📥 Downloading RSS-Bridge..." + + mkdir -p "$RSSBRIDGE_DIR" + + # Download latest release + wget -q -O /tmp/rss-bridge.zip \ + "https://github.com/RSS-Bridge/rss-bridge/releases/latest/download/rss-bridge.zip" || { + echo "❌ Download failed" + return 1 + } + + echo "📦 Extracting..." + unzip -q -o /tmp/rss-bridge.zip -d "$RSSBRIDGE_DIR" + rm -f /tmp/rss-bridge.zip + + # Configure whitelist (enable common bridges) + cat > "${RSSBRIDGE_DIR}/whitelist.txt" << 'EOF' +Facebook +Twitter +Youtube +Mastodon +Reddit +Instagram +Telegram +Bandcamp +SoundCloud +* +EOF + + # Create init script + cat > /etc/init.d/rss-bridge << 'INITEOF' +#!/bin/sh /etc/rc.common +START=96 +STOP=10 +USE_PROCD=1 + +RSSBRIDGE_DIR="/srv/rss-bridge" + +start_service() { + local enabled port + config_load cyberfeed + config_get enabled rssbridge enabled 0 + config_get port rssbridge port 3000 + + [ "$enabled" = "1" ] || return 0 + + procd_open_instance + procd_set_param command php-cgi -b 127.0.0.1:9000 -d cgi.fix_pathinfo=1 + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance + + # Also start simple HTTP server for RSS-Bridge + procd_open_instance rss-bridge-http + procd_set_param command php -S 0.0.0.0:${port} -t ${RSSBRIDGE_DIR} + procd_set_param respawn + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} +INITEOF + chmod +x /etc/init.d/rss-bridge + + echo "" + echo "╔═══════════════════════════════════════════════════════════╗" + echo "║ ✅ RSS-BRIDGE INSTALLED ║" + echo "╠═══════════════════════════════════════════════════════════╣" + echo "║ 📁 Location: /srv/rss-bridge ║" + printf "║ 🌐 URL: http://your-router:%s ║\n" "$RSSBRIDGE_PORT" + echo "║ ║" + echo "║ To enable: ║" + echo "║ uci set cyberfeed.rssbridge.enabled=1 ║" + echo "║ uci commit cyberfeed ║" + echo "║ /etc/init.d/rss-bridge start ║" + echo "╚═══════════════════════════════════════════════════════════╝" +} + +start_rssbridge() { + local enabled=$(uci -q get cyberfeed.rssbridge.enabled || echo 0) + + if [ "$enabled" != "1" ]; then + echo "⚠️ RSS-Bridge is disabled" + echo "Enable with: uci set cyberfeed.rssbridge.enabled=1 && uci commit" + return 1 + fi + + if [ ! -d "$RSSBRIDGE_DIR" ]; then + echo "❌ RSS-Bridge not installed. Run: rss-bridge-setup install" + return 1 + fi + + /etc/init.d/rss-bridge start + echo "✅ RSS-Bridge started on port $RSSBRIDGE_PORT" +} + +stop_rssbridge() { + /etc/init.d/rss-bridge stop 2>/dev/null + echo "⏹️ RSS-Bridge stopped" +} + +status_rssbridge() { + local enabled=$(uci -q get cyberfeed.rssbridge.enabled || echo 0) + local running="false" + + if pgrep -f "rss-bridge" >/dev/null 2>&1 || pgrep -f "php.*$RSSBRIDGE_PORT" >/dev/null 2>&1; then + running="true" + fi + + cat << EOF +{ + "installed": $([ -d "$RSSBRIDGE_DIR" ] && echo "true" || echo "false"), + "enabled": $enabled, + "running": $running, + "port": $RSSBRIDGE_PORT, + "path": "$RSSBRIDGE_DIR" +} +EOF +} + +case "$1" in + install) + install_rssbridge + ;; + start) + start_rssbridge + ;; + stop) + stop_rssbridge + ;; + status) + status_rssbridge + ;; + *) + echo "Usage: $0 {install|start|stop|status}" + echo "" + echo "RSS-Bridge converts social media pages to RSS feeds:" + echo " - Facebook pages/groups" + echo " - Twitter/X accounts" + echo " - YouTube channels" + echo " - Mastodon accounts" + echo " - And many more..." + ;; +esac diff --git a/package/secubox/secubox-app-cyberfeed/files/usr/share/cyberfeed/template.html b/package/secubox/secubox-app-cyberfeed/files/usr/share/cyberfeed/template.html new file mode 100644 index 00000000..8173f378 --- /dev/null +++ b/package/secubox/secubox-app-cyberfeed/files/usr/share/cyberfeed/template.html @@ -0,0 +1,2 @@ + +