Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

1
p2post/.mtimes.json Normal file
View File

@@ -0,0 +1 @@
{"dataManager.py": 1757582441.3228955, "p2post.py": 1757583058.8108132, "p2postApiBlueprint.py": 1757586306.2077203, "p2postSocketio.py": 1754200648.0560544, "vue\\router.js": 1753521292.8932686, "vue\\api\\p2postApi.js": 1757586213.915512, "vue\\api\\socketEvents.js": 1754201038.0272875, "vue\\components\\CreatePost.vue": 1755344874.1115193, "vue\\components\\Feed.vue": 1755344430.197177, "vue\\components\\SideBar.vue": 1757586181.5301366, "vue\\stores\\p2postStore.js": 1754201461.7792923, "vue\\views\\HomeView.vue": 1754302593.861452}

108
p2post/dataManager.py Normal file
View File

@@ -0,0 +1,108 @@
from typing import Dict, List, Optional, Any
from libs.app.common.store import DataStore
from libs.fspn.utils.sha256_util import hash_string
class DataManager():
def __init__(self):
self.store = DataStore(path="p2post/data.json", default_data={"users":[], "networks":[], "posts":[], "medias":[]})
# TODO Load networks from noSys
# ------------------- USER CRUD -------------------
def add_user(self, pubkey: str, nickname: str, avatar: Optional[str] = None, bio: Optional[str] = None):
if self.get_user(pubkey):
raise ValueError(f"User {pubkey} already exists")
user = {"pubkey": pubkey, "nickname": nickname, "avatar": avatar or "", "bio": bio or ""}
self.store.add_item("users", user, unique=True, id_field="pubkey", id=pubkey)
return user
def get_user(self, pubkey: str):
return self.store.get_item("users", "pubkey", pubkey)
def delete_user(self, pubkey: str) -> bool:
return self.store.remove_item("users", "pubkey", pubkey)
def list_users(self):
return self.store.list_items("users")
def update_user(self, pubkey: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("users", "pubkey", pubkey, updates)
# ---------------- NETWORK CRUD ----------------
def add_network(self, hash: str, name: str):
if self.get_network(hash):
raise ValueError(f"Network {hash} already exists")
network = {
"hash": hash,
"posts": []
}
self.store.add_item("networks", network, unique=True, id_field="hash", id=hash)
return network
def get_network(self, hash: str):
return self.store.get_item("networks", "hash", hash)
def delete_network(self, hash: str) -> bool:
return self.store.remove_item("networks", "hash", hash)
def list_networks(self):
return self.store.list_items("networks")
def update_network(self, hash: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("networks", "hash", hash, updates)
# ---------------- POST CRUD ----------------
def add_post(self, hash: str, timestamp: int, author: str, content: str, signature: str, medias: Optional[list[Dict]] = None):
if self.get_post(hash):
raise ValueError(f"Post {hash} already exists")
post = {
"hash": hash,
"timestamp": timestamp,
"author": author,
"content": content,
"medias": medias,
"signature": signature
}
self.store.add_item("posts", post, unique=True, id_field="hash", id=hash)
return post
def get_post(self, hash: str):
return self.store.get_item("posts", "hash", hash)
def delete_post(self, hash: str) -> bool:
return self.store.remove_item("posts", "hash", hash)
def list_posts(self):
return self.store.list_items("posts")
def update_post(self, hash: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("posts", "hash", hash, updates)
# ---------------- Media ----------------
# TODO Add field status. Create method to verify media
def add_media(self, hash, file_path):
if self.get_media(hash):
raise ValueError(f"Media {hash} already exists")
media = {
"hash":hash,
"file_path": file_path
}
self.store.add_item("medias", media, unique=True, id_field="hash", id=hash)
return media
def get_media(self, hash: str):
return self.store.get_item("medias", "hash", hash)
def delete_media(self, hash: str) -> bool:
return self.store.remove_item("medias", "hash", hash)
def list_medias(self):
return self.store.list_items("medias")
def update_media(self, hash: str, updates: Dict[str, Any]) -> bool:
return self.store.update_item("medias", "hash", hash, updates)

11
p2post/info.json Normal file
View File

@@ -0,0 +1,11 @@
{
"id": "p2post",
"version": 0.014,
"modules": [
{
"id": "p2post",
"version": 0
}
],
"frontend": "vue"
}

196
p2post/p2post.py Normal file
View File

@@ -0,0 +1,196 @@
import sys, os
import math
from enum import Enum, auto
from pathlib import Path
import time
import json
from datetime import datetime, timezone
from libs.noSys.noSysModule import NoSysModule
from libs.noSys.events import Events as NoSysEvents, DynamicEvents as NoSysDynamicEvents
from libs.noSys.networks import Networks
from libs.noSys.peers import Peer
from libs.app.common.logging import get_logger
from libs.app.common.paths import ROOT_DIR, FILES_DIR
from libs.fspn.utils.sha256_util import hash_file, hash_bytes
from libs.app.common.store import DataStore
from libs.p2post.p2postApiBlueprint import Blueprint
from libs.p2post.p2postSocketio import HandlerSocketio
from libs.fileTransfer.fileTransfer import FileTransfer, File, FileEvents
from .dataManager import DataManager
logger = get_logger()
class P2postEvents(Enum):
ON_RECEIVING_ = auto()
class P2post(NoSysModule):
def __init__(self, nosys_core):
super().__init__(nosys_core)
self.data = DataManager()
self.file_transfer:FileTransfer = None
self.requested_signatures = {}
def setup(self):
self.nosys_core.subscribe_event(NoSysEvents.USER_ADDED, self.on_user_added)
self.nosys_core.modules.api.register_blueprint(Blueprint(self).blueprint)
self.nosys_core.modules.api.register_socketio(HandlerSocketio(self))
self.networks_module:Networks = self.nosys_core.modules.get("noSys", "networks")
self.file_transfer = self.nosys_core.modules.get("fileTransfer", "fileTransfer")
self.file_transfer.subscribe_module_file_events(self.id, self.on_file_event)
def on_nosys_ready(self, event):
pass
def on_user_added(self, event):
user_id = event.user_id
if not self.data.get_user(user_id):
temp_nickname = user_id[:4] + "..." + user_id[-4:]
self.data.add_user(user_id, temp_nickname)
def on_file_event(self, event):
file:File = event.file
file.subscribe_event(FileEvents.ON_FILE_COMPLETED.name, self.on_file_done)
file.subscribe_event(FileEvents.ON_FILE_ERROR.name, self.on_file_done)
file.subscribe_event(FileEvents.ON_FILE_UPDATE.name, self.on_file_update)
logger.error(f"{file.status} {file.id}")
file.approve_transfer(True) # TODO Check configuration
def on_file_done(self, event):
logger.info(f"File Done {event.source.status} {event.source.hash}")
self.data.add_media(event.source.hash, event.final_path)
def on_file_update(self, event):
pass
def on_module_connection(self, event):
pass
# self.send_networks(event.connection.id)
def on_module_message(self, event):
action = event.data['action']
handler_action = getattr(self, 'on_'+action)
handler_action(event)
def on_network_connection(self, event):
network_id:str = event.network_id
peer:Peer = event.peer
self.send_network_post(network_id, peer.id)
def send_network_post(self, network_id, peer_id):
network = self.data.get_network(network_id)
if network:
body = {"action":"network_posts", "hash":network["hash"], "posts":network["posts"]}
self.nosys_core.dispatcher.send_message(body, peer_id, self.id)
def on_network_posts(self, event):
payload = event.data
network = self.data.get_network(payload["hash"])
if network:
for post in payload["posts"]:
if not self.data.get_post(post):
self.send_get_post(post, network, event.peer.id)
def send_get_post(self, hash, network, peer_id):
body = {"action":"get_post", "hash":hash, "network":network}
self.nosys_core.dispatcher.send_message(body, peer_id, self.id)
def on_get_post(self, event):
payload = event.data
self.send_post(payload["hash"], payload["network"], event.peer.id)
def send_post(self, hash, network, peer_id):
post = self.data.get_post(hash)
body = {"action":"post", "post":post, "network":network}
self.nosys_core.dispatcher.send_message(body, peer_id, self.id)
def on_post(self, event):
payload = event.data
post = payload["post"]
network = payload["network"]
# TODO VERIFY POST
if not self.data.get_post(post["hash"]):
self.data.add_post(post["hash"], post["timestamp"], post["author"], post["content"], post["signature"], post["medias"])
for media in post["medias"]:
if media["type"] == "local" and (not self.data.get_media(media["hash"])):
self.send_get_media(media["hash"], event.peer.id)
self.data.get_network(network)["posts"].append(post["hash"])
if not self.data.get_user(post["author"]):
author_key = post["author"]
temp_nickname = author_key[:4] + "..." + author_key[-4:]
self.data.add_user(author_key, temp_nickname)
self.data.store.save()
elif post["hash"] not in self.data.get_network(network)["posts"]:
self.data.get_network(network)["posts"].append(post["hash"])
self.data.store.save()
def send_get_media(self, hash, peer_id):
body = {"action":"get_media", "hash":hash}
self.nosys_core.dispatcher.send_message(body, peer_id, self.id)
def on_get_media(self, event):
payload = event.data
media = self.data.get_media(payload["hash"])
self.file_transfer.send_file(media["file_path"], event.peer.id, self.id)
def create_post(self, author, content, medias, networks:list[str]):
# TODO File server to respond frontend requests
medias_data = []
for media in medias:
if media["type"] == "local":
file_hash = os.path.splitext(os.path.basename(media["file_path"]))[0]
medias_data.append({"type":"local", "hash":file_hash})
self.data.add_media(file_hash, media["file_path"])
else:
medias_data.append({"type":media["type"], "url":media["url"]})
current_utc_datetime = datetime.now(timezone.utc)
utc_timestamp = current_utc_datetime.timestamp()
post = {
"timestamp": utc_timestamp,
"author": author,
"content": content,
"medias": medias_data
}
post_serialized = json.dumps(post, sort_keys=True, separators=(",", ":"))
post["hash"] = hash_bytes(post_serialized.encode('utf-8'))
request_id = self.nosys_core.modules.pmc.sign(post["hash"], author, self.signature_callback, f"New Post {post}")
self.requested_signatures[request_id] = (post, networks)
logger.debug(f"Post waiting signature {request_id}: {post}")
return post["hash"]
def signature_callback(self, request_id, signature):
post, networks = self.requested_signatures[request_id]
if signature:
self.data.add_post(post["hash"], post["timestamp"], post["author"], post["content"], signature, post["medias"])
for network in networks:
self.data.get_network(network)["posts"].append(post["hash"])
network_state = self.networks_module.network_states.get(network)
for peer in network_state["peers"]:
self.send_post(post["hash"], network, peer)
self.data.store.save()
def test(self):
file_path = r"C:\Workspace\libs\noSys\index.html"
file_url = "https://pbs.twimg.com/media/GwvwQamW4AAQYiD?format=jpg&name=small"
medias = []
medias.append({
"type":"local",
"file_path": file_path
})
medias.append({
"type":"external",
"url": file_url
})
self.create_post("A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj", "ContentTest", medias, ["629985fd999a675072a2e3f22f74e60c62e8c5b8efb21478c9b04e7ff2fe9779",])

BIN
p2post/p2post.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,94 @@
from libs.api.apiBlueprint import ApiBlueprint
from libs.fspn.utils.sha256_util import hash_bytes
from libs.app.common.paths import FILES_DIR
from flask import jsonify, request, send_from_directory
import os
import copy
import random
MEDIAS_FOLDER = os.path.join(FILES_DIR, "p2post", "medias")
class Blueprint(ApiBlueprint):
def routes(self):
from .p2post import P2post
self.module:P2post = self.module
@self.blueprint.route('/')
def show():
return self.module.name
@self.blueprint.route('/myusers', methods=["GET", "POST"])
def my_users():
if request.method == "GET":
users = []
for user_id in self.module.nosys_core.users.users:
user = self.module.data.get_user(user_id)
if user:
user["data"] = self.module.nosys_core.data.get_user(user_id)
users.append(user)
return jsonify(users)
elif request.method == "POST":
content:dict = request.json
self.module.data.add_user(content["pubkey"], content["nickname"], content["avatar"], content["bio"])
return jsonify({"pubkey":content["pubkey"]})
@self.blueprint.route('/users', methods=["GET", "POST"])
def users():
if request.method == "GET":
users = self.module.data.list_users()
return jsonify({"users":users})
elif request.method == "POST":
content:dict = request.json
self.module.data.add_user(content["pubkey"], content["nickname"], content["avatar"], content["bio"])
return jsonify({"pubkey":content["pubkey"]})
@self.blueprint.route('/posts', methods=["GET", "POST"])
def posts():
if request.method == "GET":
raw_posts = self.module.data.list_posts()
posts = copy.deepcopy(raw_posts)
for post in posts:
for media in post.get("medias"):
if media.get("type") == "local":
media_info = self.module.data.get_media(media.get("hash"))
media["file_path"] = media_info.get("file_path")
return jsonify(posts)
elif request.method == "POST":
content:dict = request.json
hash = self.module.create_post(content["user"], content["content"], content["medias"], content["networks"])
return jsonify(hash)
@self.blueprint.route('/medias', methods=['POST'])
def medias():
file = request.files.get("file")
if not file:
return jsonify({"error": "Empty file"}), 400
file_bytes = file.read()
file_hash = hash_bytes(file_bytes)
file_ext = os.path.splitext(file.filename)[1]
file_path = os.path.join(MEDIAS_FOLDER, f"{file_hash}{file_ext}")
if not os.path.exists(file_path):
with open(file_path, "wb") as f:
f.write(file_bytes)
return jsonify({
"status": "ok",
"hash": file_hash,
"path": f"{file_hash}{file_ext}"
})
@self.blueprint.route('/medias/<filename>')
def media_file(filename):
return send_from_directory(MEDIAS_FOLDER, filename)

26
p2post/p2postSocketio.py Normal file
View File

@@ -0,0 +1,26 @@
from flask import Blueprint, make_response, request, jsonify, session, request
from flask_socketio import SocketIO, emit, join_room, leave_room
from libs.api.eventsSocketio import EventsSocketio
import os, json
import logging
class HandlerSocketio(EventsSocketio):
def events(self):
@self.socketio.on("message", namespace=self.namespace)
def on_message(*args, **kwargs):
print('P2Post Message',args, kwargs)
self.socketio.send(f"Message from P2Post {args[0]}", namespace=self.namespace)
@self.socketio.on("ola", namespace=self.namespace)
def on_test(*args, **kwargs):
print('P2Post ola',args, kwargs)
self.socketio.emit("ola",f"Ola from P2Post {args[0]}", namespace=self.namespace)
def send_test(self, message):
self.socketio.emit("test",message, namespace=self.namespace)
def emit_new_user(self, user):
self.socketio.emit("newUser", user, namespace=self.namespace)

View File

@@ -0,0 +1,86 @@
import { useP2postStore } from '../stores/p2postStore'
const p2postStore = useP2postStore()
const p2postApiUrl = "/api/p2post"
export const p2postApi = {
async getMyUsers(){
try {
const response = await fetch(p2postApiUrl+'/myusers')
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json()
p2postStore.users = result
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
async getPosts(){
try {
const response = await fetch(p2postApiUrl+'/posts')
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json()
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
async createPosts(user, content, medias, networks){
const data = {"user": user, "content": content, "medias": medias, "networks": networks};
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: JSON.stringify(data),
};
try {
const response = await fetch(p2postApiUrl+"/posts", requestOptions);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error fetching posts: ${response.status} - ${errorText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error fetching posts:", error);
throw error;
}
},
async uploadFile(file) {
const formData = new FormData();
formData.append("file", file);
try {
const response = await fetch(p2postApiUrl + "/medias", {
method: "POST",
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error uploading file: ${response.status} - ${errorText}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error uploading file:", error);
throw error;
}
}
}

View File

@@ -0,0 +1,15 @@
import { useP2postStore } from '../stores/p2postStore'
const p2postStore = useP2postStore()
export default function registerSocketEvents(socket) {
socket.on('connect', (data) => {
console.log('Connected p2Post')
}),
socket.on('newPost', (data) => {
p2postStore.addPost(data)
console.log('New Post', data)
})
}

View File

@@ -0,0 +1,221 @@
<script setup>
import Card from '@/components/cards/Card.vue';
import { p2postApi } from '../api/p2postApi'
import { ref } from 'vue';
import InputComboBox from '@/components/inputs/InputComboBox.vue';
import Label from '@/components/labels/Label.vue';
import InputText from '@/components/inputs/InputText.vue';
import Button from '@/components/buttons/Button.vue';
import { ArrowUpTrayIcon, ChatBubbleBottomCenterTextIcon, RocketLaunchIcon, XMarkIcon } from '@heroicons/vue/24/solid';
const newPostUser = ref(null);
const newPostContent = ref("");
const newPostFiles = ref([]);
const newPostFilesPreview = ref([]);
const newPostNetworks = ref([]);
const onFileSelected = (e) => {
const newFiles = Array.from(e.target.files);
newFiles.forEach(file => {
const exists = newPostFiles.value.some(
f => f.name === file.name && f.size === file.size
);
if (!exists) {
newPostFiles.value.push(file);
newPostFilesPreview.value.push({
file,
url: URL.createObjectURL(file),
type: file.type
});
}
});
e.target.value = null;
};
function removePreview(index) {
const [removed] = newPostFilesPreview.value.splice(index, 1);
URL.revokeObjectURL(removed.url);
newPostFiles.value.splice(index, 1);
}
async function createPost(){
// TODO Select users
const user = "A4DZSk+TlR+4w39MbiIAQbti+N0H1QlJEhRH2DI6Iubj"
const content = newPostContent.value
var medias = []
for (var file of newPostFilesPreview.value){
const response = await p2postApi.uploadFile(file.file)
console.log(response)
medias.push({type: "local",
"file_path": response.path})
}
// TODO Select networks
const networks = ["net"]
const response = await p2postApi.createPosts(user, content, medias, networks)
}
</script>
<template>
<Card class="bg-black border-yellow-400/20 p-6">
<h2 class="text-xl font-bold text-yellow-400 mb-6 flex items-center">
<ChatBubbleBottomCenterTextIcon class="mr-2 h-5 w-5" />
Create New Post
</h2>
<div class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div>
<Label class="text-gray-300 mb-2 block">Select User</Label>
<InputComboBox/>
</div>
<div class="bg-black border border-yellow-400/20 rounded-lg p-3">
<div class="flex items-center space-x-3">
<!-- <img src={user.photo || "/placeholder.svg"} alt="" class="w-10 h-10 rounded-full" /> -->
<div>
<p class="text-white font-semibold">{user.nickname}</p>
<p class="text-xs text-gray-400 font-mono">{user.publicKey}</p>
</div>
</div>
</div>
</div>
<div>
<Label class="text-gray-300 mb-2 block">Post Content</Label>
<InputText
value={postContent}
placeholder="Share your thoughts with the decentralized world..."
class="bg-black border-yellow-400/30 text-white min-h-24"
/>
</div>
<div class="flex gap-4">
<Button
@click=""
variant="outline"
class="border-yellow-400 text-yellow-400 hover:bg-yellow-400 hover:text-black bg-transparent"
>
<ArrowUpTrayIcon class="mr-2 h-4 w-4" />
Add Files
</Button>
<div class="flex-1 bg-black border border-yellow-400/20 rounded-lg p-3">
<div class="flex flex-wrap gap-2">
<div key={file} class="flex items-center space-x-2 bg-gray-800 rounded px-2 py-1">
<File size={14} class="text-yellow-400" />
<span class="text-white text-sm">{file}</span>
<Button
size="sm"
variant="ghost"
class="text-red-400 hover:bg-red-400/20 p-0 h-auto"
>
<XMarkIcon class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<div>
<Label class="text-gray-300 mb-2 block">Select Networks</Label>
<!-- <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
<div key={network.id} class="flex items-center space-x-2">
<Checkbox
id={network.id}
checked={selectedNetworks.includes(network.name)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedNetworks((prev) => [...prev, network.name])
} else {
setSelectedNetworks((prev) => prev.filter((n) => n !== network.name))
}
class="border-yellow-400 data-[state=checked]:bg-yellow-400 data-[state=checked]:border-yellow-400"
/>
<Label htmlFor={network.id} class="text-white text-sm">
{network.name}
</Label>
</div>
))}
</div> -->
</div>
<Button
@click=""
class="bg-yellow-400 text-black hover:bg-yellow-300 font-bold"
>
<RocketLaunchIcon class="h-4 w-4 mr-2" />
Post to Networks
</Button>
</div>
</Card>
<!-- <div class="bg-gray-800 rounded-xl shadow p-4 mb-6 text-white">
<textarea
v-model="newPostContent"
placeholder="Share something with the network..."
class="w-full p-2 bg-gray-700 border border-gray-600 rounded mb-2"
rows="3"
></textarea>
<div
v-for="(preview, index) in newPostFilesPreview"
:key="preview.url"
class="bg-gray-700 p-2 rounded relative flex items-center justify-center h-40"
>
<button
@click="removePreview(index)"
class="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center hover:bg-red-700"
title="Remover"
>
&times;
</button>
<img
v-if="preview.type.startsWith('image/')"
:src="preview.url"
class="object-contain max-h-full max-w-full"
alt="Preview"
/>
<audio
v-else-if="preview.type.startsWith('audio/')"
:src="preview.url"
controls
class="w-full"
></audio>
<video
v-else-if="preview.type.startsWith('video/')"
:src="preview.url"
controls
class="object-contain max-h-full max-w-full"
></video>
<div v-else class="text-sm text-center px-2 break-all">
{{ preview.file.name }}
</div>
</div>
<div class="flex items-center justify-between">
<input type="file" multiple @change="onFileSelected" class="text-sm text-white" />
<button
@click="createPost"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
>
Post
</button>
</div>
</div> -->
</template>

View File

@@ -0,0 +1,79 @@
<script setup>
import Card from '@/components/cards/Card.vue';
import { useP2postStore } from '../stores/p2postStore';
import InputComboBox from '@/components/inputs/InputComboBox.vue';
import { ClockIcon, FunnelIcon, HashtagIcon } from '@heroicons/vue/24/solid';
const p2postStore = useP2postStore()
</script>
<template>
<Card class="bg-black border-yellow-400/20 p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-yellow-400 flex items-center">
<HashtagIcon class="mr-2 h-5 w-5" />
Feed
</h2>
<div class="flex items-center space-x-2">
<FunnelIcon class="text-gray-400 h-5 w-5" />
<InputComboBox></InputComboBox>
</div>
</div>
<div class="space-y-4">
<div key={post.id} class="bg-black border border-yellow-400/20 rounded-lg p-4">
<div class="flex items-start justify-between mb-3">
<div class="flex items-center space-x-3">
<!-- <img src={post.photo || "/placeholder.svg"} alt="" class="w-12 h-12 rounded-full" /> -->
<div>
<div class="flex items-center space-x-2">
<span class="text-white font-semibold">{post.nickname}</span>
<div class="flex items-center space-x-1">
<Shield size={14} class="text-yellow-400" />
<span class="text-yellow-400 text-sm">PoW: {post.powDifficulty}</span>
</div>
</div>
<p class="text-xs text-gray-400 font-mono">{post.userPublicKey}</p>
</div>
</div>
<div class="flex items-center space-x-1 text-gray-400 text-sm">
<ClockIcon class="h-4 w-4"/>
<span>{new Date(post.postedAt).toLocaleString()}</span>
</div>
</div>
<div class="mb-3">
<p class="text-white leading-relaxed">{post.content}</p>
</div>
<div class="mb-3">
FILES
</div>
<div class="flex flex-wrap gap-2">
<span key={network} class="bg-yellow-400 text-black text-sm px-2 py-1 rounded-full">
{network}
</span>
</div>
</div>
</div>
</Card>
<!-- <div v-for="post in p2postStore.posts.value" :key="post.hash" class="bg-gray-800 p-4 rounded-xl shadow mb-4">
<div class="flex items-center space-x-3 mb-2">
<img :src="post.author.avatar" class="w-8 h-8 rounded-full" alt="Avatar" />
<div>
<p class="text-sm font-semibold">{{ post.author.nickname }}</p>
<p class="text-xs text-gray-400">{{ formatTimestamp(post.timestamp) }}</p>
</div>
</div>
<p class="text-gray-200 whitespace-pre-wrap">{{ post.content }}</p>
<div v-for="media in post.media">
<img v-if="media.type === 'external'" :src="media.url" class="mt-2 rounded max-h-60" alt="Media" />
<img v-if="media.type === 'local'" :src="media.file_path" class="mt-2 rounded max-h-60" alt="Media" />
</div>
</div> -->
</template>

View File

@@ -0,0 +1,219 @@
<script setup>
import Card from '@/components/cards/Card.vue';
import { useP2postStore } from '../stores/p2postStore';
import { ref } from 'vue';
import InputText from '@/components/inputs/InputText.vue';
import Button from '@/components/buttons/Button.vue';
import { CommandLineIcon, GlobeAltIcon, PencilIcon, PlusIcon, UsersIcon, XMarkIcon } from '@heroicons/vue/24/solid';
import { p2postApi } from '../api/p2postApi';
import { noSysApi } from '@/modules/noSys/api/noSysApi';
const p2postStore = useP2postStore()
const newNetworkName = ref("")
async function loadData(){
const networks = await noSysApi.listNetworks()
const users = await p2postApi.getMyUsers()
console.log(users)
for(const user of users){
const userData = await noSysApi.getUser(user.pubkey)
for(const network of userData.networks){
p2postStore.networks.push(networks.find(n => n.data.id === network))
}
}
}
loadData()
</script>
<template>
<div class="w-80 bg-black border-r border-yellow-400/20 p-6 space-y-6">
<Card class="bg-black border-yellow-400/20 p-4">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<UsersIcon class="h-5 w-5 mr-2" />
My Users
</h3>
<div class="space-y-3">
<div v-for="user in p2postStore.users" class="bg-black border border-yellow-400/20 rounded-lg p-3">
<div v-if="false" class="space-y-2">
<InputText
value={editNickname}
placeholder="Nickname"
class="bg-gray-800 border-yellow-400/30 text-white text-sm"
/>
<InputText
value={editBio}
placeholder="Bio"
class="bg-gray-800 border-yellow-400/30 text-white text-sm h-16"
/>
<div class="flex gap-2">
<Button
@click=""
class="bg-yellow-400 text-black hover:bg-yellow-300"
>
Save
</Button>
<Button
@click=""
class="border-gray-400 text-gray-400"
>
Cancel
</Button>
</div>
</div>
<div v-if="true">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<!-- <img src={user.photo || "/placeholder.svg"} alt="" class="w-8 h-8 rounded-full" /> -->
<span class="text-white font-semibold text-sm">{{user.nickname}}</span>
</div>
<Button
@click=""
class="text-black hover:bg-yellow-400/20"
>
<PencilIcon class="w-4 h-4" />
Edit
</Button>
</div>
<p class="text-xs text-gray-400 font-mono mb-1">{{user.pubkey}}</p>
<p class="text-xs text-gray-300">{{user.bio}}</p>
</div>
</div>
</div>
</Card>
<Card class="bg-black border-yellow-400/20 p-4">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<GlobeAltIcon class="h-5 w-5 mr-2" />
Networks
</h3>
<div class="space-y-3 mb-4">
<div v-for="network in p2postStore.networks" class="bg-black border border-yellow-400/20 rounded-lg p-3">
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-white font-semibold">{{network.data.name}}</span>
<Button
size="sm"
variant="ghost"
@click=""
class="text-black hover:bg-yellow-400"
>
<XMarkIcon class="h-4 w-4" />
Remove
</Button>
</div>
<div class="text-xs text-gray-400 space-y-1">
<div>{{network.data.id}}</div>
<div>Posts: {{network.totalPosts}}</div>
</div>
</div>
</div>
<div class="flex gap-2">
<InputText
value={newNetworkName}
placeholder="Network name"
class="bg-black border-yellow-400/30 text-white text-sm"
/>
<Button size="sm" onClick={addNetwork} class="bg-yellow-400 text-black hover:bg-yellow-300">
<PlusIcon class="h-4 w-4"/>
Add
</Button>
</div>
</Card>
<Card v-if="false" class="bg-black border-yellow-400/20 p-4 flex-1">
<h3 class="text-lg font-bold text-yellow-400 mb-4 flex items-center">
<CommandLineIcon class="h-5 w-5 mr-2" />
System Logs
</h3>
<div
ref={logsRef}
class="bg-black border border-yellow-400/20 rounded-lg p-3 h-64 overflow-y-auto font-mono text-xs"
>
<!-- V-for logs -->
<div key={log.id} class="mb-1">
<span class="text-gray-500">{new Date(log.timestamp).toLocaleTimeString()}</span>
<span>
<!-- TODO log level/type colors -->
{log.message}
</span>
</div>
</div>
</Card>
</div>
<!-- <aside class="w-64 bg-gray-800 shadow-md p-4 overflow-y-auto">
<div class="mt-6">
<h3 class="text-lg font-semibold mb-2">Accounts</h3>
<div v-for="peer in p2postStore.peers.value" :key="peer.pubkey" class="flex items-center space-x-2 mb-2">
<img :src="peer.avatar" class="w-8 h-8 rounded-full" alt="Avatar" />
<span class="text-sm font-medium">{{ peer.nickname }}</span>
</div>
</div>
<div class="mt-6">
<h3 class="text-lg font-semibold mb-2">Networks</h3>
<ul>
<li
v-for="network in p2postStore.networks.value"
:key="network.name"
@click="selectNetwork(network)"
class="cursor-pointer hover:text-blue-400"
>
<span class="font-medium">{{ network.name }}</span>
<span class="text-sm text-gray-400 block">
{{ network.peers }} peers, {{ network.posts }} posts
</span>
<button @click="removeNetwork(network.name)" class="text-red-400 hover:underline">
Remove
</button>
</li>
</ul>
<form @submit.prevent="addNetwork" class="mb-4">
<input
v-model="newNetworkName"
type="text"
placeholder="New network..."
class="w-full bg-gray-700 border border-gray-600 rounded p-2 mb-2 text-sm text-white"
/>
<button
type="submit"
class="w-full bg-green-600 text-white text-sm py-1 rounded hover:bg-green-700"
>
Add Network
</button>
</form>
</div> -->
<!-- <div class="mt-6">
<h3 class="text-lg font-semibold mb-2">Accounts</h3>
<form @submit.prevent="updateUser" class="space-y-2">
<input
v-model="me.nickname"
type="text"
placeholder="Your nickname"
class="w-full bg-gray-700 border border-gray-600 rounded p-2 text-sm text-white"
/>
<input
type="file"
@change="onAvatarSelected"
class="text-sm text-white"
/>
<button
type="submit"
class="w-full bg-blue-600 text-white text-sm py-1 rounded hover:bg-blue-700"
>
Update Profile
</button>
</form>
<div class="mt-2 text-sm text-gray-400">
<p><strong>Public Key:</strong></p>
<p class="break-all text-xs text-gray-500">{{ me.pubkey }}</p>
</div>
</div> -->
<!-- </aside> -->
</template>

7
p2post/vue/router.js Normal file
View File

@@ -0,0 +1,7 @@
import HomeView from "./views/HomeView.vue";
const routes = [
{path: '/', name:'p2post', component: HomeView},
]
export {routes};

View File

@@ -0,0 +1,16 @@
import { defineStore } from 'pinia'
export const useP2postStore = defineStore('p2postStore', {
state: () => ({
users: [],
networks: [],
}),
getters: {
},
actions: {
}
})

View File

@@ -0,0 +1,57 @@
<script setup>
import { ref } from 'vue';
import { p2postApi } from '../api/p2postApi'
import CreatePost from '../components/CreatePost.vue'
import SideBar from '../components/SideBar.vue';
import Feed from '../components/Feed.vue'
import { useP2postStore } from '../stores/p2postStore';
const p2postStore = useP2postStore()
</script>
<template>
<div class="min-h-screen bg-black text-yellow-400">
<header class="border-b border-yellow-400/20 py-6">
<div class="container mx-auto px-6">
<div class="flex items-center space-x-4">
<div class="text-3xl font-black">
<span class="text-yellow-400">P2</span>
<span class="text-white">POST</span>
</div>
<div class="w-1 h-8 bg-yellow-400"></div>
<span class="text-gray-400">Decentralized Social Media</span>
</div>
</div>
</header>
<div class="flex min-h-screen">
<SideBar/>
<div class="flex-1 p-6 space-y-6">
<CreatePost/>
<Feed/>
</div>
</div>
</div>
<!-- <div class="flex h-screen bg-gray-900 text-white">
<SideBar/>
<main class="flex-1 p-6 overflow-y-auto">
<div class="max-w-2xl mx-auto">
<CreatePost/>
<Feed/>
</div>
</main>
</div> -->
</template>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background-color: #111827;
color: white;
}
</style>