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:
parent
45c9a4b7dc
commit
22f6f26a01
39
package/secubox/luci-app-cyberfeed/Makefile
Normal file
39
package/secubox/luci-app-cyberfeed/Makefile
Normal 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))
|
||||
@ -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] || {}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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); }
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
74
package/secubox/secubox-app-cyberfeed/Makefile
Normal file
74
package/secubox/secubox-app-cyberfeed/Makefile
Normal 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))
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
615
package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed
Normal file
615
package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed
Normal 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
|
||||
@ -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
|
||||
@ -0,0 +1,2 @@
|
||||
<!-- CyberFeed HTML Template - Placeholder -->
|
||||
<!-- Generated dynamically by /usr/bin/cyberfeed -->
|
||||
Loading…
Reference in New Issue
Block a user