2024-02-11 00:00:27 -06:00
|
|
|
import { useState, useEffect, useRef } from "react";
|
2024-03-20 19:42:28 -05:00
|
|
|
import { SimplePool, nip19, verifyEvent } from "nostr-tools";
|
2024-03-27 14:44:54 -05:00
|
|
|
import axios from "axios";
|
2024-03-25 13:39:32 -05:00
|
|
|
import { useToast } from "./useToast";
|
2024-03-19 17:47:16 -05:00
|
|
|
|
|
|
|
const initialRelays = [
|
|
|
|
"wss://nos.lol/",
|
|
|
|
"wss://relay.damus.io/",
|
|
|
|
"wss://relay.snort.social/",
|
|
|
|
"wss://relay.nostr.band/",
|
|
|
|
"wss://nostr.mutinywallet.com/",
|
|
|
|
"wss://relay.mutinywallet.com/",
|
|
|
|
"wss://relay.primal.net/"
|
|
|
|
];
|
2024-02-11 00:00:27 -06:00
|
|
|
|
|
|
|
export const useNostr = () => {
|
|
|
|
const [relays, setRelays] = useState(initialRelays);
|
|
|
|
const [relayStatuses, setRelayStatuses] = useState({});
|
2024-03-19 17:47:16 -05:00
|
|
|
const [events, setEvents] = useState({
|
|
|
|
resources: [],
|
|
|
|
workshops: [],
|
|
|
|
courses: [],
|
|
|
|
streams: []
|
|
|
|
});
|
2024-02-11 00:00:27 -06:00
|
|
|
|
2024-03-27 14:44:54 -05:00
|
|
|
const { showToast } = useToast();
|
2024-03-25 13:39:32 -05:00
|
|
|
|
2024-02-11 00:00:27 -06:00
|
|
|
const pool = useRef(new SimplePool({ seenOnEnabled: true }));
|
|
|
|
const subscriptions = useRef([]);
|
|
|
|
|
|
|
|
const getRelayStatuses = () => {
|
|
|
|
if (pool.current && pool.current._conn) {
|
|
|
|
const statuses = {};
|
|
|
|
|
|
|
|
for (const url in pool.current._conn) {
|
|
|
|
const relay = pool.current._conn[url];
|
|
|
|
statuses[url] = relay.status; // Assuming 'status' is an accessible field in Relay object
|
|
|
|
}
|
|
|
|
|
|
|
|
setRelayStatuses(statuses);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const updateRelays = async (newRelays) => {
|
|
|
|
// Set new relays
|
|
|
|
setRelays(newRelays);
|
|
|
|
|
|
|
|
// Ensure the relays are connected before using them
|
|
|
|
await Promise.all(newRelays.map(relay => pool.current.ensureRelay(relay)));
|
|
|
|
};
|
|
|
|
|
2024-03-19 17:47:16 -05:00
|
|
|
const fetchEvents = async (filter, updateDataField, hasRequiredTags) => {
|
|
|
|
try {
|
|
|
|
const sub = pool.current.subscribeMany(relays, filter, {
|
2024-03-27 14:44:54 -05:00
|
|
|
onevent: async (event) => {
|
|
|
|
const shouldInclude = await hasRequiredTags(event.tags);
|
|
|
|
if (shouldInclude) {
|
2024-03-19 17:47:16 -05:00
|
|
|
setEvents(prevData => ({
|
|
|
|
...prevData,
|
|
|
|
[updateDataField]: [...prevData[updateDataField], event]
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
onerror: (error) => {
|
|
|
|
setError(error);
|
|
|
|
console.error(`Error fetching ${updateDataField}:`, error);
|
|
|
|
},
|
|
|
|
oneose: () => {
|
|
|
|
console.log("Subscription closed");
|
|
|
|
sub.close();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
setError(error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Fetch resources, workshops, courses, and streams with appropriate filters and update functions
|
2024-03-27 14:44:54 -05:00
|
|
|
const fetchResources = async () => {
|
|
|
|
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
|
|
|
|
const hasRequiredTags = async (eventData) => {
|
|
|
|
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
|
|
|
const hasResource = eventData.some(([tag, value]) => tag === "t" && value === "resource");
|
|
|
|
if (hasPlebDevs && hasResource) {
|
|
|
|
const resourceId = eventData.find(([tag]) => tag === "d")?.[1];
|
|
|
|
if (resourceId) {
|
|
|
|
try {
|
|
|
|
const response = await axios.get(`/api/resources/${resourceId}`);
|
|
|
|
return response.status === 200;
|
|
|
|
} catch (error) {
|
|
|
|
// Handle 404 or other errors gracefully
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
2024-03-19 17:47:16 -05:00
|
|
|
fetchEvents(filter, 'resources', hasRequiredTags);
|
|
|
|
};
|
|
|
|
|
2024-03-27 14:44:54 -05:00
|
|
|
const fetchWorkshops = async () => {
|
|
|
|
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
|
|
|
|
const hasRequiredTags = async (eventData) => {
|
|
|
|
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
|
|
|
const hasWorkshop = eventData.some(([tag, value]) => tag === "t" && value === "workshop");
|
|
|
|
if (hasPlebDevs && hasWorkshop) {
|
|
|
|
const workshopId = eventData.find(([tag]) => tag === "d")?.[1];
|
|
|
|
if (workshopId) {
|
|
|
|
try {
|
|
|
|
const response = await axios.get(`/api/resources/${workshopId}`);
|
|
|
|
return response.status === 200;
|
|
|
|
} catch (error) {
|
|
|
|
// Handle 404 or other errors gracefully
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
2024-03-19 17:47:16 -05:00
|
|
|
fetchEvents(filter, 'workshops', hasRequiredTags);
|
2024-03-27 14:44:54 -05:00
|
|
|
};
|
2024-03-19 17:47:16 -05:00
|
|
|
|
2024-03-27 14:44:54 -05:00
|
|
|
const fetchCourses = async () => {
|
|
|
|
const filter = [{ kinds: [30023], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"] }];
|
|
|
|
const hasRequiredTags = async (eventData) => {
|
|
|
|
const hasPlebDevs = eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
|
|
|
const hasCourse = eventData.some(([tag, value]) => tag === "t" && value === "course");
|
|
|
|
if (hasPlebDevs && hasCourse) {
|
|
|
|
const courseId = eventData.find(([tag]) => tag === "d")?.[1];
|
|
|
|
if (courseId) {
|
|
|
|
// try {
|
|
|
|
// const response = await axios.get(`/api/resources/${courseId}`);
|
|
|
|
// return response.status === 200;
|
|
|
|
// } catch (error) {
|
|
|
|
// // Handle 404 or other errors gracefully
|
|
|
|
// return false;
|
|
|
|
// }
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
2024-03-19 17:47:16 -05:00
|
|
|
fetchEvents(filter, 'courses', hasRequiredTags);
|
2024-03-27 14:44:54 -05:00
|
|
|
};
|
2024-03-19 17:47:16 -05:00
|
|
|
|
2024-03-27 14:44:54 -05:00
|
|
|
// const fetchStreams = () => {
|
|
|
|
// const filter = [{kinds: [30311], authors: ["f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741"]}];
|
|
|
|
// const hasRequiredTags = (eventData) => eventData.some(([tag, value]) => tag === "t" && value === "plebdevs");
|
|
|
|
// fetchEvents(filter, 'streams', hasRequiredTags);
|
|
|
|
// }
|
2024-03-19 17:47:16 -05:00
|
|
|
|
2024-02-11 00:00:27 -06:00
|
|
|
const fetchKind0 = async (criteria, params) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const events = [];
|
|
|
|
const timeoutDuration = 1000;
|
|
|
|
|
|
|
|
const sub = pool.current.subscribeMany(relays, criteria, {
|
|
|
|
...params,
|
|
|
|
onevent: (event) => {
|
|
|
|
events.push(event);
|
|
|
|
},
|
|
|
|
onerror: (error) => {
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Set a timeout to sort and resolve with the most recent event
|
|
|
|
setTimeout(() => {
|
|
|
|
if (events.length === 0) {
|
|
|
|
resolve(null); // or reject based on your needs
|
|
|
|
} else {
|
|
|
|
events.sort((a, b) => b.created_at - a.created_at); // Sort in descending order
|
|
|
|
const mostRecentEventContent = JSON.parse(events[0].content);
|
|
|
|
resolve(mostRecentEventContent);
|
|
|
|
}
|
|
|
|
}, timeoutDuration);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchSingleEvent = async (id) => {
|
|
|
|
return new Promise((resolve, reject) => {
|
2024-02-27 18:29:57 -06:00
|
|
|
const sub = pool.current.subscribeMany(relays, [{ ids: [id] }], {
|
|
|
|
onevent: (event) => {
|
|
|
|
resolve(event);
|
|
|
|
},
|
|
|
|
onerror: (error) => {
|
|
|
|
reject(error);
|
|
|
|
},
|
|
|
|
oneose: () => {
|
|
|
|
console.log("Subscription closed");
|
|
|
|
sub.close();
|
|
|
|
}
|
2024-02-11 00:00:27 -06:00
|
|
|
});
|
|
|
|
});
|
2024-03-19 17:47:16 -05:00
|
|
|
}
|
2024-02-11 00:00:27 -06:00
|
|
|
|
2024-03-20 19:42:28 -05:00
|
|
|
const publishEvent = async (relay, signedEvent) => {
|
|
|
|
console.log('publishing event to', relay);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const timeout = 3000
|
|
|
|
const wsRelay = new window.WebSocket(relay)
|
|
|
|
let timer
|
|
|
|
let isMessageSentSuccessfully = false
|
2024-03-27 14:44:54 -05:00
|
|
|
|
|
|
|
function timedout() {
|
|
|
|
clearTimeout(timer)
|
|
|
|
wsRelay.close()
|
|
|
|
reject(new Error(`relay timeout for ${relay}`))
|
2024-03-20 19:42:28 -05:00
|
|
|
}
|
2024-03-27 14:44:54 -05:00
|
|
|
|
2024-03-20 19:42:28 -05:00
|
|
|
timer = setTimeout(timedout, timeout)
|
2024-03-27 14:44:54 -05:00
|
|
|
|
2024-03-20 19:42:28 -05:00
|
|
|
wsRelay.onopen = function () {
|
2024-03-27 14:44:54 -05:00
|
|
|
clearTimeout(timer)
|
|
|
|
timer = setTimeout(timedout, timeout)
|
|
|
|
wsRelay.send(JSON.stringify(['EVENT', signedEvent]))
|
2024-03-20 19:42:28 -05:00
|
|
|
}
|
2024-03-27 14:44:54 -05:00
|
|
|
|
2024-03-20 19:42:28 -05:00
|
|
|
wsRelay.onmessage = function (msg) {
|
2024-03-27 14:44:54 -05:00
|
|
|
const m = JSON.parse(msg.data)
|
|
|
|
if (m[0] === 'OK') {
|
|
|
|
isMessageSentSuccessfully = true
|
|
|
|
clearTimeout(timer)
|
|
|
|
wsRelay.close()
|
|
|
|
console.log('Successfully sent event to', relay)
|
|
|
|
resolve()
|
|
|
|
}
|
2024-03-20 19:42:28 -05:00
|
|
|
}
|
2024-03-27 14:44:54 -05:00
|
|
|
|
2024-03-20 19:42:28 -05:00
|
|
|
wsRelay.onerror = function (error) {
|
2024-03-27 14:44:54 -05:00
|
|
|
clearTimeout(timer)
|
|
|
|
console.log(error)
|
|
|
|
reject(new Error(`relay error: Failed to send to ${relay}`))
|
2024-03-20 19:42:28 -05:00
|
|
|
}
|
2024-03-27 14:44:54 -05:00
|
|
|
|
2024-03-20 19:42:28 -05:00
|
|
|
wsRelay.onclose = function () {
|
2024-03-27 14:44:54 -05:00
|
|
|
clearTimeout(timer)
|
|
|
|
if (!isMessageSentSuccessfully) {
|
|
|
|
reject(new Error(`relay error: Failed to send to ${relay}`))
|
|
|
|
}
|
2024-03-20 19:42:28 -05:00
|
|
|
}
|
2024-03-27 14:44:54 -05:00
|
|
|
})
|
2024-03-20 19:42:28 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const publishAll = async (signedEvent) => {
|
2024-02-11 00:00:27 -06:00
|
|
|
try {
|
2024-03-20 19:42:28 -05:00
|
|
|
const promises = relays.map(relay => publishEvent(relay, signedEvent));
|
|
|
|
const results = await Promise.allSettled(promises)
|
|
|
|
const successfulRelays = []
|
|
|
|
const failedRelays = []
|
|
|
|
|
|
|
|
results.forEach((result, i) => {
|
|
|
|
if (result.status === 'fulfilled') {
|
|
|
|
successfulRelays.push(relays[i])
|
2024-03-25 13:39:32 -05:00
|
|
|
showToast('success', `published to ${relays[i]}`)
|
2024-03-20 19:42:28 -05:00
|
|
|
} else {
|
|
|
|
failedRelays.push(relays[i])
|
2024-03-25 13:39:32 -05:00
|
|
|
showToast('error', `failed to publish to ${relays[i]}`)
|
2024-03-20 19:42:28 -05:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return { successfulRelays, failedRelays }
|
2024-02-11 00:00:27 -06:00
|
|
|
} catch (error) {
|
2024-03-20 19:42:28 -05:00
|
|
|
console.error('Error publishing event:', error);
|
2024-02-11 00:00:27 -06:00
|
|
|
}
|
|
|
|
};
|
2024-03-27 14:44:54 -05:00
|
|
|
|
2024-02-11 00:00:27 -06:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
getRelayStatuses(); // Get initial statuses on mount
|
|
|
|
|
|
|
|
// Copy current subscriptions to a local variable inside the effect
|
|
|
|
const currentSubscriptions = subscriptions.current;
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
// Use the local variable in the cleanup function
|
|
|
|
currentSubscriptions.forEach((sub) => sub.unsub());
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return {
|
|
|
|
updateRelays,
|
|
|
|
fetchSingleEvent,
|
2024-03-20 19:42:28 -05:00
|
|
|
publishAll,
|
2024-02-11 00:00:27 -06:00
|
|
|
fetchKind0,
|
2024-02-11 16:26:33 -06:00
|
|
|
fetchResources,
|
2024-02-27 18:29:57 -06:00
|
|
|
fetchCourses,
|
2024-03-16 16:37:47 -05:00
|
|
|
fetchWorkshops,
|
2024-03-27 14:44:54 -05:00
|
|
|
// fetchStreams,
|
2024-02-11 00:00:27 -06:00
|
|
|
getRelayStatuses,
|
2024-03-19 17:47:16 -05:00
|
|
|
events
|
2024-02-11 00:00:27 -06:00
|
|
|
};
|
|
|
|
};
|