This commit is contained in:
Raven Scott
2025-12-17 20:49:34 -05:00
parent a977320bfc
commit 00709cb446
10 changed files with 4191 additions and 49 deletions
@@ -39,15 +39,17 @@ async function handleDomainsRoutes(req, res) {
for (const domain of domains) {
const hash = await getHashForDomain(domain) || 'none';
const isLocal = localWriter ? domainClaimants.get(domain)?.has(localWriter) || false : false;
// Check if local writer is the resolved claimant (owner)
let isOwner = false;
let consensusState = null;
try {
const consensusState = await getConsensusState(domain);
consensusState = await getConsensusState(domain);
isOwner = localWriter ? consensusState.resolvedClaimant === localWriter : false;
} catch (err) {
logDebug('Admin', `Error checking ownership for ${domain}: ${err.message}`);
logDebug('Admin', `Error checking ownership/consensus for ${domain}: ${err.message}`);
}
resolved.push({ domain, hash, isLocal, isOwner });
resolved.push({ domain, hash, isLocal, isOwner, consensusState });
}
let internalDomains = ['p2ns.admin'];
try {
@@ -59,7 +61,7 @@ async function handleDomainsRoutes(req, res) {
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true });
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true, consensusStatus: 'internal' });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
+9 -16
View File
@@ -107,24 +107,17 @@ window.tabs = {
<td class="p-3">${item.isLocal && item.hash !== 'internal' ? `<button onclick="removeDomain('${item.domain}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Remove</button>` : ''}</td>`;
return tr;
},
postFetch: async (data) => {
// Fetch consensus states for all domains
const domainsWithConsensus = await Promise.all(data.map(async (item) => {
if (item.hash === 'internal' || item.hash === 'none') {
postFetch: (data) => {
// Data already contains consensusState from backend
return data.map(item => {
if (item.consensusStatus === 'internal' || item.hash === 'internal' || item.hash === 'none') {
return { ...item, consensusStatus: 'internal' };
}
try {
const consensusRes = await fetch(`/api/consensus/${encodeURIComponent(item.domain)}`);
if (consensusRes.ok) {
const consensusState = await consensusRes.json();
return { ...item, consensusStatus: consensusState.status, consensusState };
}
} catch (err) {
console.error(`Failed to fetch consensus for ${item.domain}:`, err);
}
return { ...item, consensusStatus: 'unknown' };
}));
return domainsWithConsensus;
return {
...item,
consensusStatus: item.consensusState?.status || 'unknown'
};
});
}
},
entries: {
+1 -1
View File
@@ -25,7 +25,7 @@ async function genericFetch(tabId, shouldRender = true) {
window[config.dataKey] = data;
// Reset infinite scroll state when fetching fresh data
if (window.infiniteScrollState && window.infiniteScrollState[tabId]) {
if (shouldRender && window.infiniteScrollState && window.infiniteScrollState[tabId]) {
window.infiniteScrollState[tabId].loadedCount = 0;
window.infiniteScrollState[tabId].lastQuery = '';
// Disconnect existing observer
+3 -2
View File
@@ -148,8 +148,9 @@ function connectWebSocket() {
window.updateMap[data.type]();
} else {
window.updateMap[data.type].forEach(tab => {
if (window.activeTab === 'host' || window.activeTab === tab) {
if (window.genericFetch) window.genericFetch(tab, true);
// Always fetch data in background, but only render if tab is active
if (window.genericFetch) {
window.genericFetch(tab, window.activeTab === 'host' || window.activeTab === tab);
}
});
}
+12 -2
View File
@@ -39,7 +39,17 @@ async function handleDomainsRoutes(req, res) {
for (const domain of domains) {
const hash = await getHashForDomain(domain) || 'none';
const isLocal = localWriter ? domainClaimants.get(domain)?.has(localWriter) || false : false;
resolved.push({ domain, hash, isLocal });
let isOwner = false;
let consensusState = null;
try {
consensusState = await getConsensusState(domain);
isOwner = localWriter ? consensusState.resolvedClaimant === localWriter : false;
} catch (err) {
logDebug('Admin', `Error checking ownership/consensus for ${domain}: ${err.message}`);
}
resolved.push({ domain, hash, isLocal, isOwner, consensusState });
}
let internalDomains = ['p2ns.admin'];
try {
@@ -51,7 +61,7 @@ async function handleDomainsRoutes(req, res) {
// Only add internal domains that aren't already in the resolved list
for (const d of internalDomains) {
if (!resolved.some(r => r.domain === d)) {
resolved.push({ domain: d, hash: 'internal', isLocal: true });
resolved.push({ domain: d, hash: 'internal', isLocal: true, isOwner: true, consensusStatus: 'internal' });
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
+41 -1
View File
@@ -22,9 +22,26 @@ window.paginationState = {
'dns-conflicts': { current: 1, size: 10 },
'host-servers': { current: 1, size: 10 },
'host-clients': { current: 1, size: 10 },
settings: { current: 1, size: 20 }
plugins: { current: 1, size: 10 }
};
// Helper function to get consensus status badge
function getConsensusStatusBadge(status) {
const badges = {
'resolved': '<span class="px-2 py-1 bg-green-500 text-white rounded text-xs">Resolved</span>',
'tie': '<span class="px-2 py-1 bg-yellow-500 text-white rounded text-xs">Tie</span>',
'insufficient_quorum': '<span class="px-2 py-1 bg-orange-500 text-white rounded text-xs">No Quorum</span>',
'no_claims': '<span class="px-2 py-1 bg-gray-500 text-white rounded text-xs">No Claims</span>',
'error': '<span class="px-2 py-1 bg-red-500 text-white rounded text-xs">Error</span>',
'internal': '<span class="px-2 py-1 bg-blue-500 text-white rounded text-xs">Internal</span>',
'unknown': '<span class="px-2 py-1 bg-gray-400 text-white rounded text-xs">Unknown</span>'
};
return badges[status] || badges['unknown'];
}
// Make function globally available
window.getConsensusStatusBadge = getConsensusStatusBadge;
window.chartColors = {
primary: 'rgb(59, 130, 246)',
success: 'rgb(34, 197, 94)',
@@ -59,10 +76,33 @@ window.tabs = {
renderItem: (item) => {
const tr = document.createElement('tr');
tr.className = 'border-b hover:bg-gray-50 dark:hover:bg-gray-700';
let consensusInfo = '';
if (item.consensusState) {
const statusBadge = getConsensusStatusBadge(item.consensusStatus);
consensusInfo = `<td class="p-3">${statusBadge}</td>`;
} else if (item.consensusStatus === 'internal') {
consensusInfo = `<td class="p-3">${getConsensusStatusBadge('internal')}</td>`;
} else {
consensusInfo = `<td class="p-3">${getConsensusStatusBadge('unknown')}</td>`;
}
tr.innerHTML = `<td class="p-3">${item.domain}${item.isLocal ? '🏠' : ''}</td>
<td class="p-3 break-all">${item.hash}</td>
${consensusInfo}
<td class="p-3">${item.isLocal && item.hash !== 'internal' ? `<button onclick="removeDomain('${item.domain}')" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Remove</button>` : ''}</td>`;
return tr;
},
postFetch: (data) => {
return data.map(item => {
if (item.consensusStatus === 'internal' || item.hash === 'internal' || item.hash === 'none') {
return { ...item, consensusStatus: 'internal' };
}
return {
...item,
consensusStatus: item.consensusState?.status || 'unknown'
};
});
}
},
entries: {
+3 -2
View File
@@ -148,8 +148,9 @@ function connectWebSocket() {
window.updateMap[data.type]();
} else {
window.updateMap[data.type].forEach(tab => {
if (window.activeTab === 'host' || window.activeTab === tab) {
if (window.genericFetch) window.genericFetch(tab, true);
// Always fetch data in background, but only render if tab is active
if (window.genericFetch) {
window.genericFetch(tab, window.activeTab === 'host' || window.activeTab === tab);
}
});
}
+9 -20
View File
@@ -288,26 +288,15 @@ function handleWebSocketUpdate(data) {
window.utils.updateSidebarStats();
}
// Notify current view of update (only if view is active)
const activeView = document.querySelector('.view-content.active');
if (activeView) {
switch (currentView) {
case 'overview':
if (window.overviewView) {
window.overviewView.handleUpdate(data);
}
break;
case 'domain-list':
if (window.domainListView) {
window.domainListView.handleUpdate(data);
}
break;
case 'domain-detail':
if (window.domainDetailView) {
window.domainDetailView.handleUpdate(data);
}
break;
}
// Notify all views of update (they handle their own background refresh logic)
if (window.overviewView) {
window.overviewView.handleUpdate(data);
}
if (window.domainListView) {
window.domainListView.handleUpdate(data);
}
if (window.domainDetailView) {
window.domainDetailView.handleUpdate(data);
}
}
+431
View File
@@ -0,0 +1,431 @@
# Read.it - Decentralized Reddit Clone
> ⚠️ **Note**: This plugin is currently in **active development**. Features may change, and you may encounter bugs while we work on stabilizing the P2P synchronization and real-time updates.
A fully-featured, decentralized Reddit-like platform built on the P2NS (Peer-to-Peer Name System) plugin architecture. Read.it provides all the community-driven content features you'd expect from a modern social platform, but with decentralized data storage and peer-to-peer communication.
## 🌟 Features Overview
### Core Content Features
#### Posts
- **Multiple Post Types**: Text, Link, Image, and Poll posts
- **Rich Media Embeds**: Automatic embedding for YouTube, Vimeo, SoundCloud, Spotify, Twitter/X, Twitch, and Imgur
- **Markdown Support**: Full markdown rendering with syntax highlighting for code blocks
- **Post Flairs**: Customizable post categorization
- **Awards System**: Give and receive awards (Gold, Silver, Platinum, Helpful, Wholesome)
- **Reactions**: Multiple reaction types (❤️ Like, 😂 Funny, 😮 Wow, 😢 Sad, 😡 Angry) with ability to see who reacted
- **Crossposting**: Share posts across different communities
- **Post Scheduling**: Schedule posts for future publication
- **Draft System**: Auto-save and manual draft management
#### Comments
- **Threaded Comments**: Nested comment threads with collapsible replies
- **Markdown Support**: Full markdown in comments
- **Voting**: Upvote/downvote system
- **Editing & Deletion**: Authors can edit or delete their comments
#### Communities (Subreadits)
- **Create Communities**: Anyone can create a new community
- **Customization**: Description, rules, and display settings
- **Subscription System**: Join/leave communities
- **Member Counts**: Track subscriber numbers
- **Wiki Pages**: Community-editable wiki documentation
- **Community Events**: Schedule and manage community events
- **Real-time Chat**: Per-community chat rooms
### User Features
#### Profiles
- **Global Profile Integration**: Integrated with P2NS Global Profile SDK
- **Custom Avatars**: User avatars with fallback to identicons
- **Cover Photos**: Customizable profile cover images
- **Pinned Posts**: Pin your best posts to your profile
- **Custom Themes**: Per-user profile themes
- **Karma System**: Post and comment karma tracking
- **Badges & Trophies**: Achievement system with multiple tiers
- **User Flairs**: Per-subreddit user flairs
#### Social Features
- **Following**: Follow other users to see their content
- **Friends System**: Mutual follows create friendships
- **Blocking**: Block users you don't want to interact with
- **Private Messaging**: Direct message other users
- **Mentions**: @mention users in posts and comments
- **Notifications**: Real-time notifications for replies, mentions, upvotes, and more
#### Content Management
- **Saved Posts/Comments**: Bookmark content for later
- **Read Later Queue**: Separate queue for posts to read later
- **Bookmark Folders**: Organize saved items into folders
- **Reading Progress**: Track which posts you've read
- **Multi-Reddits**: Create custom feeds combining multiple communities
### Discovery & Navigation
#### Sorting Algorithms
- **Hot**: Time-decay algorithm balancing recency and popularity
- **New**: Chronological, newest first
- **Top**: Highest voted (with time filters: hour, day, week, month, year, all)
- **Controversial**: Posts with mixed voting patterns
#### Search
- **Full-text Search**: Search posts and comments
- **Subreddit Search**: Find communities by name
- **User Search**: Find users by ID
#### Discovery
- **Trending Topics**: Algorithm-detected trending keywords
- **Related Posts**: Keyword-based post suggestions
- **Community Recommendations**: Suggestions based on subscriptions
- **Random Subreddit**: Discover random communities
- **Leaderboards**: Top contributors globally and per-subreddit
### Moderation Tools
#### Basic Moderation
- **Remove Posts/Comments**: Remove rule-breaking content
- **Lock Posts**: Prevent new comments
- **Pin Posts**: Sticky important posts
- **Post Flairs**: Moderator-assigned flairs
#### Advanced Moderation
- **Ban System**: Ban users from communities (temporary or permanent)
- **Ban Appeals**: Users can appeal bans
- **Mod Roles**: Granular permissions (can ban, can flair, can remove, etc.)
- **Mod Mail**: Private messaging system for mod teams
- **Mod Queue**: Centralized view of reported content
- **Mod Activity Log**: Public transparency log of mod actions
- **User Notes**: Private moderator notes on users
#### AutoMod
- **Custom Rules**: Create automated moderation rules
- **Keyword Filters**: Auto-remove content with specific keywords
- **Domain Blacklists**: Block links from specific domains
- **Regex Patterns**: Advanced pattern matching
- **Spam Detection**: Automatic spam filtering
#### Community Moderation
- **Report System**: Users can report content
- **Crowd-sourced Moderation**: Community voting on reports
- **Content Filters**: Keyword/regex/domain blacklists
### Super Admin
A special super admin system exists for platform-wide moderation:
- **Hardcoded Public Key**: `a6a6d7ebcc1df8f33410067bf96ad2cc30d5276516f758bb0f34195e914c451f`
- **Global Mod Powers**: Can moderate any community
- **Platform Management**: Access to all moderation tools
### API & Integration
#### REST API
- `GET /api/v1/r/:sub/posts` - Get posts from a subreddit
- `GET /api/v1/post/:id` - Get a specific post
- `GET /api/v1/r/:sub` - Get subreddit info
- `GET /api/v1/user/:id` - Get user info
API authentication via `X-API-Key` header.
#### RSS Feeds
- `/r/:sub/rss` - RSS feed for a subreddit
- `/rss` or `/feed.xml` - Front page RSS feed
#### Webhooks
- Configure webhooks to notify external services on events
- Supports: new posts, new comments, reports
### PWA Support
Read.it is a Progressive Web App:
- **Installable**: Install as a native-like app
- **Offline Support**: Basic offline functionality via service worker
- **Push Notifications**: (when supported by browser)
## 🛠 Technical Architecture
### Backend (`index.js`)
The backend is a P2NS plugin that handles:
#### WebSocket Message Handlers
All real-time communication happens over WebSocket. Key message types:
**Subscriptions**
- `subscribe` - Subscribe to a channel (feed, post, subreadits)
- `unsubscribe` - Unsubscribe from a channel
**Posts**
- `post:create` - Create a new post
- `post:edit` - Edit an existing post
- `post:delete` - Delete a post
- `post:crosspost` - Crosspost to another community
**Comments**
- `comment:create` - Create a comment
- `comment:edit` - Edit a comment
- `comment:delete` - Delete a comment
**Voting**
- `vote` - Vote on a post or comment
**Communities**
- `subreadit:create` - Create a community
- `subreadit:subscribe` - Subscribe to a community
**Moderation**
- `mod:removePost` - Remove a post
- `mod:removeComment` - Remove a comment
- `mod:lockPost` - Lock/unlock a post
- `mod:pinPost` - Pin/unpin a post
- `mod:ban` - Ban a user
- `mod:unban` - Unban a user
**And many more...**
#### Database Collections (HyperDB)
| Collection | Description |
|------------|-------------|
| `@readit/posts` | All posts |
| `@readit/comments` | All comments |
| `@readit/subreadits` | Communities |
| `@readit/votes` | User votes |
| `@readit/saved` | Saved items |
| `@readit/flairs` | Post flairs |
| `@readit/notifications` | User notifications |
| `@readit/bans` | User bans |
| `@readit/modlogs` | Moderation logs |
| `@readit/media` | Uploaded media |
| `@readit/drafts` | Post drafts |
| `@readit/multis` | Multi-reddit feeds |
| `@readit/awards` | Given awards |
| `@readit/userflairs` | User flairs |
| `@readit/pollvotes` | Poll votes |
| `@readit/wikis` | Wiki pages |
| `@readit/messages` | Private messages |
| `@readit/chatmessages` | Chat messages |
| `@readit/follows` | User follows |
| `@readit/blocks` | User blocks |
| `@readit/reports` | Content reports |
| `@readit/modmail` | Mod mail |
| `@readit/automod` | AutoMod rules |
| `@readit/badges` | User badges |
| `@readit/events` | Community events |
| `@readit/templates` | Post templates |
| `@readit/reactions` | Post/comment reactions |
| `@readit/readlater` | Read later queue |
| `@readit/usernotes` | Moderator user notes |
| `@readit/apikeys` | API keys |
| `@readit/webhooks` | Webhook configurations |
| `@readit/banappeals` | Ban appeals |
| `@readit/modroles` | Moderator roles |
| `@readit/contentfilters` | Content filters |
| `@readit/reportvotes` | Community report votes |
| `@readit/userprofiles` | Enhanced user profiles |
| `@readit/trophies` | User trophies |
### Frontend
#### Technology Stack
- **Alpine.js**: Reactive UI framework
- **Tailwind CSS**: Utility-first CSS
- **DaisyUI**: Component library
- **marked.js**: Markdown rendering
- **highlight.js**: Code syntax highlighting
#### Key Files
- `www/index.html` - Main HTML with Alpine.js templates
- `www/js/app.js` - Alpine.js application logic
- `www/manifest.json` - PWA manifest
- `www/sw.js` - Service worker
#### Client-Side Routing
The app uses client-side routing with `history.pushState`:
| Route | View |
|-------|------|
| `/` | Home feed |
| `/r/:name` | Subreddit view |
| `/r/:name/post/:id` | Post detail |
| `/u/:id` | User profile |
| `/submit` | Create post |
| `/saved` | Saved items |
| `/subscriptions` | Subscribed communities |
| `/settings` | User settings |
| `/inbox` | Private messages |
| `/notifications` | Notifications |
| `/drafts` | Post drafts |
| `/feeds` | Multi-reddit management |
| `/feeds/:name` | Multi-reddit view |
| `/search` | Search results |
| `/trending` | Trending topics |
| `/events` | Community events |
| `/modqueue` | Mod queue |
| `/analytics` | Community analytics |
| `/leaderboard` | Top contributors |
### Theming
The app supports light and dark modes via CSS variables:
```css
:root {
--readit-dark: #1a1a1b;
--readit-darker: #030303;
--readit-border: #343536;
--readit-hover: #2d2d2e;
--readit-orange: #ff4500;
--readit-blue: #0079d3;
}
[data-theme="light"] {
--readit-dark: #ffffff;
--readit-darker: #f6f7f8;
--readit-border: #ccc;
--readit-hover: #f0f0f0;
}
```
## 📁 File Structure
```
plugin-sites/read.it/
├── config.json # Plugin configuration & DB schemas
├── index.js # Backend WebSocket & HTTP handlers
├── README.md # This file
└── www/
├── index.html # Main HTML & Alpine.js templates
├── manifest.json # PWA manifest
├── sw.js # Service worker
└── js/
└── app.js # Alpine.js application logic
```
## 🚀 Getting Started
### Prerequisites
- P2NS server running
- Plugin system enabled
### Installation
1. Place the `read.it` folder in your `plugin-sites` directory
2. The plugin will be automatically loaded by P2NS
3. Access via `https://read.it` (or your configured domain)
### Configuration
The `config.json` file defines:
- Plugin metadata (name, version, description)
- Database schemas and indexes
- Collection definitions
## 🔐 Authentication
Read.it uses the P2NS authentication system:
- Users authenticate via their P2NS identity
- Auth tokens are passed with WebSocket messages
- The `peerId` (public key) serves as the user identifier
## 🎨 UI Features
### Context Menu
Right-click on posts to access:
- Award
- Crosspost
- Read Later
- Edit (author only)
- Delete (author only)
- Report
- Mod actions (moderators only)
### Keyboard Shortcuts
- `j` / `k` - Navigate posts
- `a` / `z` - Upvote / Downvote
- `Enter` - Open post
- `Escape` - Close modals
- `c` - Focus comment box
- `s` - Save post
- `/` - Focus search
### View Modes
- Card view (default)
- Compact view
## 📊 Algorithms
### Hot Score
```javascript
const age = (Date.now() - post.createdAt) / 3600000; // hours
const score = post.upvotes - post.downvotes;
const order = Math.log10(Math.max(Math.abs(score), 1));
const sign = score > 0 ? 1 : score < 0 ? -1 : 0;
const hotScore = sign * order - age / 12;
```
### Controversial Score
```javascript
if (post.upvotes <= 0 || post.downvotes <= 0) return 0;
const magnitude = post.upvotes + post.downvotes;
const balance = post.downvotes / post.upvotes;
return magnitude * Math.min(balance, 1 / balance);
```
## 🔔 Notifications
Users receive notifications for:
- Replies to their posts
- Replies to their comments
- Mentions (@username)
- Upvote milestones (10, 50, 100, 500, 1000)
- Awards received
- New followers
- Mod actions on their content
## 🏆 Gamification
### Badges
Awarded automatically for milestones:
- First Post
- Prolific Poster (50+ posts)
- Conversation Starter (100+ comments)
- Popular (1000+ karma)
- And more...
### Trophies
Tiered achievements:
- Bronze, Silver, Gold, Platinum tiers
- Categories: Posts, Comments, Karma, Awards, etc.
### Leaderboards
- Global top contributors
- Per-subreddit top contributors
- Metrics: karma, posts, comments, awards
## 🛡️ Security
- All mod actions require authentication
- Super admin verified by public key
- API endpoints require API key authentication
- Content filters prevent malicious content
- Spam detection on post/comment creation
## 📝 License
Part of the P2NS project. See main repository for license information.
## 🤝 Contributing
Contributions welcome! Key areas:
- Bug fixes
- New features
- UI/UX improvements
- Documentation
- Performance optimization
---
Built with ❤️ on P2NS - The decentralized web awaits.
File diff suppressed because it is too large Load Diff