Compare commits

..

No commits in common. "main" and "v0.0.5" have entirely different histories.
main ... v0.0.5

10 changed files with 194 additions and 190 deletions

View File

@ -1,10 +1,10 @@
# About appticles # About appticles
A bunch of microapps (aka appticles) created for [minimo.io](https://minimo.io). A bunch of mini-apps or appticles created for [minimo.io](https://minimo.io).
These microapps are used for adding extra functionality to blog posts and articles. They are develop primarily using the Svelte Framework or Alpine.js. These micro-apps were embedded on blog posts for adding extra functionality. They were developed primarily using the Svelte Framework or Alpine.js. Most of them just as proof-of-concepts and for learning purposes.
Minimo.io itself [was built using Eleventy](https://github.com/minimo-io/minimo-11ty). The site [minimo.io](https://github.com/minimo-io/minimo-11ty) itself was built using [Eleventy](https://github.com/11ty/eleventy).
## Get in touch. ## Get in touch.

View File

@ -3,18 +3,22 @@
Find out who does and who doesn't follow you back on Nostr. Find out who does and who doesn't follow you back on Nostr.
Let promote some reciprocity here! 😹 Let promote some reciprocity here! 😹
> Check out the [demo](https://minimo.io/app/nostr-followback/).
## ToDo ## ToDo
- Create groups to checks to fire simultaneously instad of one by one (to remember: fireing all the `fetchProfile` at once for a given npub resulted in a permanent halt of the process -mainly for big users). Maybe using https://lodash.com/docs `_.chunk(array, [size=1])` - Actually load builds at minimo.io/app/ or something alike.
- Ask community for help about the bug (see below) so I can keep understanding the protocol and the library.
- Polish the proof-of-concept code, making it TS code and remove `// @ts-nocheck`!
- Create interfaces or new types instead of loose variables - Create interfaces or new types instead of loose variables
- Save followbackers and not followbackers in lists to see details. - Save followbackers and not followbackers in lists to see details.
- Create an UI/UX that's worth looking at. - Create an UI/UX that's worth looking at.
- Close connection with relays after all is checked - Close connection with relays after all is checked
- Show the relay list to be used - Show the relay list to be used
- Upload a localStorage list of relays & older results for folloback - Upload a localStorage list of relays
- ~~Actually load builds at minimo.io/app/ or something alike.~~
- ~~Configure Vite for the miniapp to be loaded in the article's url as base.~~ - ~~Configure Vite for the miniapp to be loaded in the article's url as base.~~
- ~~Remove SvelteKit (just Vite + Svelte).~~ - ~~Remove SvelteKit (just Vite + Svelte).~~
## BUG
- **When user follows lots of people, progress freezes:**<br>
[NDK](https://github.com/nostr-dev-kit/ndk) keeps trying to re-connect to relays, and progress slows down or halts completly. Maybe slow requests down? Are they blocking the requests because they are too many too fast?

File diff suppressed because one or more lines are too long

View File

@ -1,24 +1,24 @@
{ {
"name": "nostr-followback", "name": "nostr-followback",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json" "check": "svelte-check --tsconfig ./tsconfig.json"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2", "@tsconfig/svelte": "^5.0.2",
"svelte": "^4.2.12", "svelte": "^4.2.12",
"svelte-check": "^3.6.7", "svelte-check": "^3.6.7",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.2.0" "vite": "^5.2.0"
}, },
"dependencies": { "dependencies": {
"@nostr-dev-kit/ndk": "^2.8.1" "@nostr-dev-kit/ndk": "^2.8.1"
} }
} }

View File

@ -1,153 +1,158 @@
<script lang="ts"> <script lang="ts">
import NDK, { type NDKUserProfile } from "@nostr-dev-kit/ndk"; // @ts-nocheck
import type { Querying, FollowListed } from "./lib/types"; // Import the package
import NDK from "@nostr-dev-kit/ndk";
import { fetchUserProfile } from "./lib/fetchs";
import { relays } from "./lib/data/relays"; import { relays } from "./lib/data/relays";
import Form from "./lib/components/form.svelte"; // let npubToQuery = "npub1wujhdsytm3w6g0mpsqh8v7ezx83jcm64dlkwuqgm5v8lv0pds55ssudkw0";
let npubToQuery = "";
let npubToQuery = "npub1wujhdsytm3w6g0mpsqh8v7ezx83jcm64dlkwuqgm5v8lv0pds55ssudkw0"; let querying = false;
let userName = "";
let querying: Querying = "uninitiated"; let userThumb = "";
let userProfile: NDKUserProfile | null;
let followsCount = 0; let followsCount = 0;
let followBackCount = 0; let followBackCount = 0;
let notFollowBackCount = 0; let notFollowBackCount = 0;
let unknownFollowBack = 0; let unknownFollowBack = 0;
let totalCountOfContactsChecked = 0; let totalCountOfContactsChecked = 0;
$: progress = ((totalCountOfContactsChecked / followsCount) * 100) | 0; $: progress = ((totalCountOfContactsChecked / followsCount) * 100).toFixed() | 0;
$: notFollowBackPercentage = ((notFollowersBack.length / originalFollow.length) * 100) | 0;
let originalFollow: FollowListed[] = []; let originalFollow = [];
let notFollowersBack: string[] = []; let notFollowersBack = [];
async function checkFollowBacks() { async function checkFollowBacks() {
try { try {
const ndk = new NDK({ const ndk = new NDK({
explicitRelayUrls: relays, explicitRelayUrls: relays,
}); });
querying = "processing"; await ndk.connect();
await ndk.connect(6000);
// create user instance const user = await fetchUserProfile(npubToQuery, ndk);
const user = ndk.getUser({ npub: npubToQuery });
// query for user profile
userProfile = await user.fetchProfile();
if (userProfile) { userName = user.profile.name;
userThumb = user.profile.image;
if (userName) {
// console.log(user.profile); // console.log(user.profile);
const follows = await user.follows({ groupable: false }, false); const follows = await user.follows();
// query for a set of user followers
followsCount = follows.size; followsCount = follows.size;
let lastFollowerBack; let lastFollowerBack;
let i = 0; let i = 0;
follows.forEach(async (follower) => {
for await (const follower of follows) { //for (const follower of follows) {
// await new Promise((resolve) => setTimeout(resolve, 0));
const followerFollowList = await follower.follows(); const followerFollowList = await follower.follows();
try { originalFollow.push({ npub: follower.npub, followsBack: "-" }); // add to follower list
originalFollow.push({ npub: follower.npub, followsBack: "-" }); // add to follower list originalFollow = originalFollow;
originalFollow = originalFollow;
if (followerFollowList.size) { if (followerFollowList.size) {
// console.log(followerFollowList); // console.log(followerFollowList);
// check if the user is in the queried user follow list // check if the user is in the queried user follow list
let doesFollowBack = false; let doesFollowBack = false;
for (let contact of followerFollowList) { for (let contact of followerFollowList) {
lastFollowerBack = contact.npub; lastFollowerBack = contact.npub;
if (contact.npub == npubToQuery) { if (contact.npub == npubToQuery) {
doesFollowBack = true; doesFollowBack = true;
break; break;
}
} }
}
originalFollow[i].followsBack = doesFollowBack ? "1" : "0"; originalFollow[i].followBack = doesFollowBack ? "1" : "0";
// decision making time // decision making time
if (doesFollowBack) { if (doesFollowBack) {
followBackCount++; followBackCount++;
totalCountOfContactsChecked++; totalCountOfContactsChecked++;
// add here the ones who do follow back // add here the ones who do follow back
} else {
notFollowBackCount++;
notFollowersBack.push(follower.npub);
notFollowersBack = notFollowersBack;
totalCountOfContactsChecked++;
}
} else { } else {
unknownFollowBack++; notFollowBackCount++;
notFollowersBack.push(follower.npub);
notFollowersBack = notFollowersBack;
totalCountOfContactsChecked++; totalCountOfContactsChecked++;
} }
i++; } else {
} catch (error) { unknownFollowBack++;
console.log(follower); totalCountOfContactsChecked++;
console.error(`Error fetching npub:`, error);
} }
} i++;
});
// follows.forEach(async (follower) => {
// });
} }
querying = "completed";
} catch (error) { } catch (error) {
console.error(`Error fetching data (${npubToQuery}):`, error); console.error("Error fetching data:", error);
} }
} }
function ItemCount(i: number): string {
let ret = i.toString(); //checkFollowBacks();
if (i < 10) ret = `0${i}`;
return ret;
}
</script> </script>
<Form <form class="npub-form">
{progress} <input disabled={querying && progress < 100} type="text" placeholder="An npub to check" bind:value={npubToQuery} />
{querying} <input
bind:npubToQuery type="button"
on:click={async () => { on:click={async () => {
querying = "uninitiated"; querying = true;
userProfile = null; userName = "";
originalFollow = []; userThumb = "";
notFollowersBack = []; originalFollow = [];
followBackCount = 0; notFollowersBack = [];
notFollowBackCount = 0; followBackCount = 0;
unknownFollowBack = 0; notFollowBackCount = 0;
totalCountOfContactsChecked = 0; unknownFollowBack = 0;
await checkFollowBacks(); totalCountOfContactsChecked = 0;
}} await checkFollowBacks();
/> }}
disabled={!npubToQuery && progress < 100}
value="Analyze"
/>
</form>
{#if userProfile} <!-- {#if !querying}
{npubToQuery}
{/if} -->
{#if userThumb}
<div class="user-box"> <div class="user-box">
<img src={userProfile.image} width="50" style="border-radius:100%;" alt="user-thumb" /> <img src={userThumb} width="50" style="border-radius:100%;" alt="user-thumb" />
User: User:
<a href="https://primal.net/p/{npubToQuery}">{userProfile.displayName}</a> <a href="https://primal.net/p/{npubToQuery}">{userName}</a>
|  Follows: {followsCount} |  Follows: {followsCount}
<br /> <br />
Unknown: {unknownFollowBack} - Follow_Back: {followBackCount} - Unknown: {unknownFollowBack} | Follow_Back: {followBackCount} |
<strong>👉 Not_Follow_Back</strong> <strong>👉 Not_Follow_Back</strong>
: :
<!-- <span title="Actually Counted">{notFollowBackCount}</span>
/ -->
<span title="Actualy counted">{notFollowersBack.length}</span> <span title="Actualy counted">{notFollowersBack.length}</span>
<br /> <br />
{#if progress < 100} {#if progress < 100}
<p> <p>
Progress = <strong>{progress}%</strong> Progress =
<strong>{progress}%</strong>
- {totalCountOfContactsChecked} of {followsCount} - {totalCountOfContactsChecked} of {followsCount}
<!-- - {totalCountOfContactsChecked} of {followBackCount +
notFollowBackCount +
unknownFollowBack} -->
</p> </p>
{:else} {:else}
<p><strong>✅ Completed!</strong></p> <p><strong>Completed!</strong></p>
{/if} {/if}
<hr /> <hr />
<br /> <br />
<strong>Not followers</strong> Results ({originalFollow.length})
: {notFollowersBack.length} of
{originalFollow.length} ({notFollowBackPercentage}%)
<ul> <ul>
{#each originalFollow as item, i} {#each originalFollow as item, i}
<li> <li>
#{ItemCount(i + 1)} - {@html item.followsBack == "0" ? "<span>🔴</span>" : "<span>🟢</span>"} #{i + 1} - {@html item.followBack == "0" ? "<span>🔴</span>" : "<span>🟢</span>"}
<a target="_blank noreferrer noopener" href="https://primal.net/p/{item.npub}"> <a target="_blank noreferrer noopener" href="https://primal.net/p/{item.npub}">
{item.npub} {item.npub}
</a> </a>
@ -155,9 +160,30 @@
</li> </li>
{/each} {/each}
</ul> </ul>
<!-- <br />
<strong>They don't follow you ({notFollowersBack.length}):</strong>
<br />
<ul>
{#each notFollowersBack as item, i (item)}
<li>
#{i + 1} - <a href="https://nostr.band/{item}" target="_blank noreferrer noopener">Nostr.Band</a>
/ <a href="https://primal.net/p/{item}" target="_blank noreferrer noopener">Primal</a>
:
{item}
</li>
{/each}
</ul> -->
</div> </div>
{:else if querying == "uninitiated"} {:else if !querying}
<!-- <p>Let's find out who does not follow you back in Nostr!</p> --> <!-- <p>Let's find out who does not follow you back in Nostr!</p> -->
{:else} {:else}
<div class="loader">Loading data...</div> <div class="loader">Loading data...</div>
{/if} {/if}
<style>
.npub-form input[type="text"] {
padding: 5px;
width: 50%;
}
</style>

View File

@ -1,30 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Querying } from "../types";
const dispatch = createEventDispatcher();
export let npubToQuery = "";
export let progress = 0;
export let querying: Querying;
export let analyzeFn = function () {
dispatch("click");
};
</script>
<form class="npub-form">
<input
type="text"
placeholder="npub to check"
bind:value={npubToQuery}
disabled={querying == "processing" && progress < 100}
/>
<input type="button" on:click={analyzeFn} value="Analyze" />
</form>
<style>
.npub-form input[type="text"] {
padding: 5px;
width: 50%;
}
</style>

View File

@ -9,11 +9,11 @@ export const relays = [
"wss://nostr.mom", "wss://nostr.mom",
"wss://nostrelay.yeghro.site", "wss://nostrelay.yeghro.site",
"wss://relay.damus.io", "wss://relay.damus.io",
// "wss://relay.nostr.bg", "wss://relay.nostr.bg",
"wss://relay.snort.social", "wss://relay.snort.social",
"wss://relay.primal.net", "wss://relay.primal.net",
"wss://nostr.bitcoiner.social", "wss://nostr.bitcoiner.social",
"wss://nostr.mutinywallet.com", "wss://nostr.mutinywallet.com",
"wss://relay.current.fyi", "wss://relay.current.fyi",
// "wss://brb.io", "wss://brb.io",
]; ];

View File

@ -0,0 +1,9 @@
export async function fetchUserProfile(npub, ndk) {
const user = ndk.getUser({ npub });
await user.fetchProfile();
return user;
}
export async function fetchNotes(hexkey, ndk) {
const kind1filter = { kinds: [3], authors: [hexkey] };
return ndk.fetchEvent(kind1filter);
}

View File

@ -1,6 +0,0 @@
export type Querying = "completed" | "processing" | "uninitiated";
export type FollowListed = {
npub: string;
followsBack: string;
};

View File

@ -1,20 +1,20 @@
{ {
"extends": "@tsconfig/svelte/tsconfig.json", "extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"resolveJsonModule": true, "resolveJsonModule": true,
/** /**
* Typecheck JS in `.svelte` and `.js` files by default. * Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS. * Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use * Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files. * of JS in `.svelte` files.
*/ */
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"isolatedModules": true "isolatedModules": true
}, },
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }