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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-23 22:02:07 +01:00
parent 45c9a4b7dc
commit 22f6f26a01
18 changed files with 3063 additions and 0 deletions

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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] || {}
};
});
}
});

View File

@ -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); }
}

View File

@ -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
});

View File

@ -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
});

View File

@ -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
});

View File

@ -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
});

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"]
}
}
}

View File

@ -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 <contact@cybermind.fr>
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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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>|</entry>"
item_count=0
}
{
title=""
link=""
date=""
desc=""
# Extract title
if (match($0, /<title[^>]*>([^<]+)<\/title>/, arr)) title=arr[1]
else if (match($0, /<title[^>]*><!\[CDATA\[([^\]]+)\]\]><\/title>/, arr)) title=arr[1]
# Extract link
if (match($0, /<link[^>]*>([^<]+)<\/link>/, arr)) link=arr[1]
else if (match($0, /<link[^>]*href="([^"]+)"/, arr)) link=arr[1]
# Extract date
if (match($0, /<pubDate>([^<]+)</, arr)) date=arr[1]
else if (match($0, /<published>([^<]+)</, arr)) date=arr[1]
else if (match($0, /<updated>([^<]+)</, arr)) date=arr[1]
# Extract description
if (match($0, /<description[^>]*>([^<]+)</, arr)) desc=arr[1]
else if (match($0, /<summary[^>]*>([^<]+)</, arr)) desc=arr[1]
else if (match($0, /<description[^>]*><!\[CDATA\[(.{1,500})/, arr)) desc=arr[1]
if (title != "" && item_count < max) {
# Escape JSON special chars
gsub(/"/, "\\\"", title)
gsub(/"/, "\\\"", desc)
gsub(/[\r\n\t]/, " ", title)
gsub(/[\r\n\t]/, " ", desc)
gsub(/<[^>]+>/, "", 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'
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ CYBERFEED ⚡ Neural RSS Matrix</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap');
:root {
--neon-cyan: #0ff;
--neon-magenta: #f0f;
--neon-yellow: #ff0;
--dark-bg: #0a0a0f;
--darker-bg: #050508;
--grid-color: rgba(0, 255, 255, 0.03);
--text-primary: #e0e0e0;
--text-dim: #606080;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Share Tech Mono', monospace;
background: var(--dark-bg);
color: var(--text-primary);
min-height: 100vh;
}
body::before {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 50px 50px;
animation: gridScroll 20s linear infinite;
pointer-events: none;
z-index: -1;
}
@keyframes gridScroll {
0% { transform: translateY(0); }
100% { transform: translateY(50px); }
}
body::after {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px);
pointer-events: none;
z-index: 1000;
}
.cyber-header {
text-align: center;
padding: 2rem;
border-bottom: 1px solid var(--neon-cyan);
background: linear-gradient(180deg, rgba(0,255,255,0.1) 0%, transparent 100%);
}
.cyber-header h1 {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--neon-cyan);
text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan), 0 0 40px var(--neon-cyan);
animation: flicker 3s infinite;
}
@keyframes flicker {
0%, 100% { opacity: 1; }
92% { opacity: 1; }
93% { opacity: 0.8; }
94% { opacity: 1; }
}
.cyber-header .subtitle {
font-size: 0.85rem;
color: var(--neon-magenta);
margin-top: 0.5rem;
letter-spacing: 0.4em;
}
.status-bar {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
font-size: 0.75rem;
color: var(--text-dim);
flex-wrap: wrap;
}
.status-bar span::before { content: '▸ '; color: var(--neon-cyan); }
.category-filter {
display: flex;
justify-content: center;
gap: 0.5rem;
padding: 1rem;
flex-wrap: wrap;
}
.category-btn {
background: rgba(0,255,255,0.1);
border: 1px solid var(--neon-cyan);
color: var(--neon-cyan);
padding: 0.4rem 1rem;
font-family: 'Orbitron', sans-serif;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
}
.category-btn:hover, .category-btn.active {
background: var(--neon-cyan);
color: var(--dark-bg);
box-shadow: 0 0 15px var(--neon-cyan);
}
.feed-container {
max-width: 900px;
margin: 0 auto;
padding: 1.5rem;
}
.feed-item {
background: linear-gradient(135deg, rgba(0,255,255,0.05) 0%, rgba(255,0,255,0.02) 100%);
border: 1px solid rgba(0,255,255,0.2);
border-left: 3px solid var(--neon-cyan);
margin-bottom: 1.2rem;
padding: 1.2rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.feed-item::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.1), transparent);
transform: translateX(-100%);
transition: transform 0.5s ease;
}
.feed-item:hover {
border-color: var(--neon-magenta);
box-shadow: 0 0 20px rgba(0,255,255,0.2), inset 0 0 20px rgba(255,0,255,0.05);
}
.feed-item:hover::before { transform: translateX(100%); }
.feed-item .meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.feed-item .timestamp {
font-size: 0.7rem;
color: var(--neon-magenta);
text-transform: uppercase;
letter-spacing: 0.15em;
}
.feed-item .source-tag {
background: rgba(255,0,255,0.2);
border: 1px solid var(--neon-magenta);
padding: 0.15rem 0.5rem;
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.feed-item .category-tag {
background: rgba(0,255,255,0.2);
border: 1px solid var(--neon-cyan);
padding: 0.15rem 0.5rem;
font-size: 0.6rem;
text-transform: uppercase;
}
.feed-item .title {
font-family: 'Orbitron', sans-serif;
font-size: 1rem;
font-weight: 700;
color: var(--neon-cyan);
margin-bottom: 0.6rem;
line-height: 1.4;
}
.feed-item .title a {
color: inherit;
text-decoration: none;
transition: text-shadow 0.3s ease;
}
.feed-item .title a:hover { text-shadow: 0 0 10px var(--neon-cyan); }
.feed-item .description {
font-size: 0.85rem;
color: var(--text-primary);
line-height: 1.5;
opacity: 0.85;
}
.cyber-footer {
text-align: center;
padding: 2rem;
border-top: 1px solid rgba(0,255,255,0.2);
font-size: 0.7rem;
color: var(--text-dim);
}
.cyber-footer a { color: var(--neon-cyan); text-decoration: none; }
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text-dim);
}
.empty-state .icon { font-size: 4rem; margin-bottom: 1rem; }
@media (max-width: 600px) {
.feed-container { padding: 1rem; }
.feed-item { padding: 1rem; }
.status-bar { gap: 1rem; }
}
</style>
</head>
<body>
<header class="cyber-header">
<h1>⚡ CYBERFEED ⚡</h1>
<p class="subtitle">NEURAL RSS MATRIX INTERFACE</p>
<div class="status-bar">
<span id="feed-count">FEEDS: --</span>
<span id="last-update">SYNC: --:--:--</span>
<span>STATUS: ONLINE</span>
</div>
</header>
<div class="category-filter">
<button class="category-btn active" onclick="filterCategory('all')">ALL</button>
<button class="category-btn" onclick="filterCategory('security')">SECURITY</button>
<button class="category-btn" onclick="filterCategory('tech')">TECH</button>
<button class="category-btn" onclick="filterCategory('social')">SOCIAL</button>
<button class="category-btn" onclick="filterCategory('news')">NEWS</button>
</div>
<main class="feed-container" id="feed-items">
<div class="empty-state">
<div class="icon">🔮</div>
<p>Awaiting Neural Feed Connection...</p>
</div>
</main>
<footer class="cyber-footer">
<p>⚡ CYBERFEED v0.1 | Powered by <a href="https://cybermind.fr">CyberMind.FR</a> | SecuBox Module ⚡</p>
<p>░▒▓ JACK IN TO THE MATRIX ▓▒░</p>
</footer>
<script>
let feedData = [];
let currentFilter = 'all';
async function loadFeeds() {
try {
const resp = await fetch('/cyberfeed/feeds.json?' + Date.now());
feedData = await resp.json();
renderFeeds();
document.getElementById('feed-count').textContent = 'FEEDS: ' + feedData.length;
document.getElementById('last-update').textContent = 'SYNC: ' + new Date().toLocaleTimeString('fr-FR', {hour12: false});
} catch(e) {
console.error('Feed load error:', e);
}
}
function filterCategory(cat) {
currentFilter = cat;
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
renderFeeds();
}
function renderFeeds() {
const container = document.getElementById('feed-items');
const filtered = currentFilter === 'all' ? feedData : feedData.filter(f => f.category === currentFilter);
if (filtered.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="icon">📡</div><p>No feeds in this category</p></div>';
return;
}
container.innerHTML = filtered.map(item => `
<article class="feed-item" data-category="${item.category || 'custom'}">
<div class="meta">
<span class="timestamp">⏰ ${item.date || 'Unknown'}</span>
<div>
<span class="source-tag">${item.source || 'RSS'}</span>
<span class="category-tag">${item.category || 'custom'}</span>
</div>
</div>
<h2 class="title"><a href="${item.link}" target="_blank" rel="noopener">${item.title}</a></h2>
<p class="description">${item.desc || ''}</p>
</article>
`).join('');
}
loadFeeds();
setInterval(loadFeeds, 300000);
</script>
</body>
</html>
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

View File

@ -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

View File

@ -0,0 +1,2 @@
<!-- CyberFeed HTML Template - Placeholder -->
<!-- Generated dynamically by /usr/bin/cyberfeed -->