charek's avatar
charek
npub1amqx...x3tc
charek's avatar
charek 7 months ago
Let's talk business models what are really novel ways to start businesses with nostr, and with all the other people that dedicate time to it? can we collect them here for the interest of anyone curious
charek's avatar
charek 7 months ago
What happens to images sent in private chats and stored on blossom servers: Do they get encrypted?
charek's avatar
charek 8 months ago
Social Media may not be the strength of NOSTR the ability to integrate social features into any other use case IS a strength if NOSTR
charek's avatar
charek 9 months ago
Saving this for Future Reference ;) How to create a nostr app quickly using [applesauce](https://hzrd149.github.io/applesauce/) In this guide we are going to build a nostr app that lets users follow and unfollow [fiatjaf](@fiatjaf) ## 1. Setup new project Start by setting up a new vite app using `pnpm create vite`, then set the name and select `Solid` and `Typescript` ```sh ➜ pnpm create vite │ ◇ Project name: │ followjaf │ ◇ Select a framework: │ Solid │ ◇ Select a variant: │ TypeScript │ ◇ Scaffolding project in ./followjaf... │ └ Done. Now run: cd followjaf pnpm install pnpm run dev ``` ## 2. Adding nostr dependencies There are a few useful nostr dependencies we are going to need. `nostr-tools` for the types and small methods, and [`rx-nostr`](https://penpenpng.github.io/rx-nostr/) for making relay connections ```sh pnpm install nostr-tools rx-nostr ``` ## 3. Setup rx-nostr Next we need to setup rxNostr so we can make connections to relays. create a new `src/nostr.ts` file with ```ts import { createRxNostr, noopVerifier } from "rx-nostr"; export const rxNostr = createRxNostr({ // skip verification here because we are going to verify events at the event store skipVerify: true, verifier: noopVerifier, }); ``` ## 4. Setup the event store Now that we have a way to connect to relays, we need a place to store events. We will use the [`EventStore`](https://hzrd149.github.io/applesauce/typedoc/classes/applesauce_core.EventStore.html) class from `applesauce-core` for this. create a new `src/stores.ts` file with > The event store does not store any events in the browsers local storage or anywhere else. It's in-memory only and provides a model for the UI ```ts import { EventStore } from "applesauce-core"; import { verifyEvent } from "nostr-tools"; export const eventStore = new EventStore(); // verify the events when they are added to the store eventStore.verifyEvent = verifyEvent; ``` ## 5. Create the query store The event store is where we store all the events, but we need a way for the UI to query them. We can use the [`QueryStore`](https://hzrd149.github.io/applesauce/typedoc/classes/applesauce_core.QueryStore.html) class from `applesauce-core` for this. Create a query store in `src/stores.ts` ```ts import { QueryStore } from "applesauce-core"; // ... // the query store needs the event store to subscribe to it export const queryStore = new QueryStore(eventStore); ``` ## 6. Setup the profile loader Next we need a way to fetch user profiles. We are going to use the [`ReplaceableLoader`](https://hzrd149.github.io/applesauce/overview/loaders.html#replaceable-loader) class from [`applesauce-loaders`](https://www.npmjs.com/package/applesauce-loaders) for this. > `applesauce-loaders` is a package that contains a few loader classes that can be used to fetch different types of data from relays. First install the package ```sh pnpm install applesauce-loaders ``` Then create a `src/loaders.ts` file with ```ts import { ReplaceableLoader } from "applesauce-loaders"; import { rxNostr } from "./nostr"; import { eventStore } from "./stores"; export const replaceableLoader = new ReplaceableLoader(rxNostr); // Start the loader and send any events to the event store replaceableLoader.subscribe((packet) => { eventStore.add(packet.event, packet.from); }); ``` ## 7. Fetch fiatjaf's profile Now that we have a way to store events, and a loader to help with fetching them, we should update the `src/App.tsx` component to fetch the profile. We can do this by calling the `next` method on the loader and passing a `pubkey`, `kind` and `relays` to it ```tsx function App() { // ... onMount(() => { // fetch fiatjaf's profile on load replaceableLoader.next({ pubkey: "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", kind: 0, relays: ["wss://pyramid.fiatjaf.com/"], }); }); // ... } ``` ## 8. Display the profile Now that we have a way to fetch the profile, we need to display it in the UI. We can do this by using the [`ProfileQuery`](https://hzrd149.github.io/applesauce/typedoc/functions/applesauce_core.Queries.ProfileQuery.html) which gives us a stream of updates to a pubkey's profile. Create the profile using `queryStore.createQuery` and pass in the `ProfileQuery` and the pubkey. ```tsx const fiatjaf = queryStore.createQuery( ProfileQuery, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" ); ``` But this just gives us an [observable]( we need to subscribe to it to get the profile. Luckily SolidJS profiles a simple [`from`](https://docs.solidjs.com/reference/reactive-utilities/from) method to subscribe to any observable. > To make things reactive SolidJS uses accessors, so to get the profile we need to call `fiatjaf()` ```tsx function App() { // ... // Subscribe to fiatjaf's profile from the query store const fiatjaf = from( queryStore.createQuery(ProfileQuery, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") ); return ( <> {/* replace the vite and solid logos with the profile picture */} <div> <img src={fiatjaf()?.picture} class="logo" /> </div> <h1>{fiatjaf()?.name}</h1> {/* ... */} </> ); } ``` ## 9. Letting the user signin Now we should let the user signin to the app. We can do this by creating a [`AccountManager`](https://hzrd149.github.io/applesauce/accounts/manager.html) class from `applesauce-accounts` First we need to install the packages ```sh pnpm install applesauce-accounts applesauce-signers ``` Then create a new `src/accounts.ts` file with ```ts import { AccountManager } from "applesauce-accounts"; import { registerCommonAccountTypes } from "applesauce-accounts/accounts"; // create an account manager instance export const accounts = new AccountManager(); // Adds the common account types to the manager registerCommonAccountTypes(accounts); ``` Next lets presume the user has a NIP-07 browser extension installed and add a signin button. ```tsx function App() { const signin = async () => { // do nothing if the user is already signed in if (accounts.active) return; // create a new nip-07 signer and try to get the pubkey const signer = new ExtensionSigner(); const pubkey = await signer.getPublicKey(); // create a new extension account, add it, and make it the active account const account = new ExtensionAccount(pubkey, signer); accounts.addAccount(account); accounts.setActive(account); }; return ( <> {/* ... */} <div class="card"> <p>Are you following the fiatjaf? the creator of "The nostr"</p> <button onClick={signin}>Check</button> </div> </> ); } ``` Now when the user clicks the button the app will ask for the users pubkey, then do nothing... but it's a start. > We are not persisting the accounts, so when the page reloads the user will NOT be signed in. you can learn about persisting the accounts in the [docs]( ## 10. Showing the signed-in state We should show some indication to the user that they are signed in. We can do this by modifying the signin button if the user is signed in and giving them a way to sign-out ```tsx function App() { // subscribe to the currently active account (make sure to use the account$ observable) const account = from(accounts.active$); // ... const signout = () => { // do nothing if the user is not signed in if (!accounts.active) return; // signout the user const account = accounts.active; accounts.removeAccount(account); accounts.clearActive(); }; return ( <> {/* ... */} <div class="card"> <p>Are you following the fiatjaf? ( creator of "The nostr" )</p> {account() === undefined ? <button onClick={signin}>Check</button> : <button onClick={signout}>Signout</button>} </div> </> ); } ``` ## 11. Fetching the user's profile Now that we have a way to sign in and out of the app, we should fetch the user's profile when they sign in. ```tsx function App() { // ... // fetch the user's profile when they sign in createEffect(async () => { const active = account(); if (active) { // get the user's relays or fallback to some default relays const usersRelays = await active.getRelays?.(); const relays = usersRelays ? Object.keys(usersRelays) : ["wss://relay.damus.io", "wss://nos.lol"]; // tell the loader to fetch the users profile event replaceableLoader.next({ pubkey: active.pubkey, kind: 0, relays, }); // tell the loader to fetch the users contacts replaceableLoader.next({ pubkey: active.pubkey, kind: 3, relays, }); // tell the loader to fetch the users mailboxes replaceableLoader.next({ pubkey: active.pubkey, kind: 10002, relays, }); } }); // ... } ``` Next we need to subscribe to the users profile, to do this we can use some rxjs operators to chain the observables together. ```tsx import { Match, Switch } from "solid-js"; import { of, switchMap } from "rxjs"; function App() { // ... // subscribe to the active account, then subscribe to the users profile or undefined const profile = from( accounts.active$.pipe( switchMap((account) => (account ? queryStore.createQuery(ProfileQuery, account!.pubkey) : of(undefined))) ) ); // ... return ( <> {/* ... */} <div class="card"> <Switch> <Match when={account() && !profile()}> <p>Loading profile...</p> </Match> <Match when={profile()}> <p style="font-size: 1.2rem; font-weight: bold;">Welcome {profile()?.name}</p> </Match> </Switch> {/* ... */} </div> </> ); } ``` ## 12. Showing if the user is following fiatjaf Now that the app is fetching the users profile and contacts we should show if the user is following fiatjaf. ```tsx function App() { // ... // subscribe to the active account, then subscribe to the users contacts or undefined const contacts = from( accounts.active$.pipe( switchMap((account) => (account ? queryStore.createQuery(UserContactsQuery, account!.pubkey) : of(undefined))) ) ); const isFollowing = createMemo(() => { return contacts()?.some((c) => c.pubkey === "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"); }); // ... return ( <> {/* ... */} <div class="card"> {/* ... */} <Switch fallback={ <p style="font-size: 1.2rem;"> Sign in to check if you are a follower of the fiatjaf ( creator of "The nostr" ) </p> } > <Match when={contacts() && isFollowing() === undefined}> <p>checking...</p> </Match> <Match when={contacts() && isFollowing() === true}> <p style="color: green; font-weight: bold; font-size: 2rem;"> Congratulations! You are a follower of the fiatjaf </p> </Match> <Match when={contacts() && isFollowing() === false}> <p style="color: red; font-weight: bold; font-size: 2rem;"> Why don't you follow the fiatjaf? do you even like nostr? </p> </Match> </Switch> {/* ... */} </div> </> ); } ``` ## 13. Adding the follow button Now that we have a way to check if the user is following fiatjaf, we should add a button to follow him. We can do this with [Actions](https://hzrd149.github.io/applesauce/overview/actions.html) which are pre-built methods to modify nostr events for a user. First we need to install the `applesauce-actions` and `applesauce-factory` package ```sh pnpm install applesauce-actions applesauce-factory ``` Then create a `src/actions.ts` file with ```ts import { EventFactory } from "applesauce-factory"; import { ActionHub } from "applesauce-actions"; import { eventStore } from "./stores"; import { accounts } from "./accounts"; // The event factory is used to build and modify nostr events export const factory = new EventFactory({ // accounts.signer is a NIP-07 signer that signs with the currently active account signer: accounts.signer, }); // The action hub is used to run Actions against the event store export const actions = new ActionHub(eventStore, factory); ``` Then create a `toggleFollow` method that will add or remove fiatjaf from the users contacts. > We are using the `exec` method to run the action, and the [`forEach`]( method from RxJS allows us to await for all the events to be published ```tsx function App() { // ... const toggleFollow = async () => { // send any created events to rxNostr and the event store const publish = (event: NostrEvent) => { eventStore.add(event); rxNostr.send(event); }; if (isFollowing()) { await actions .exec(UnfollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") .forEach(publish); } else { await actions .exec( FollowUser, "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "wss://pyramid.fiatjaf.com/" ) .forEach(publish); } }; // ... } ``` ## 14. Adding outbox support The app looks like it works now but if the user reloads the page they will still see an the old version of their contacts list. we need to make sure rxNostr is publishing the events to the users outbox relays. To do this we can subscribe to the signed in users mailboxes using the query store in `src/nostr.ts` ```ts import { MailboxesQuery } from "applesauce-core/queries"; import { accounts } from "./accounts"; import { of, switchMap } from "rxjs"; import { queryStore } from "./stores"; // ... // subscribe to the active account, then subscribe to the users mailboxes and update rxNostr accounts.active$ .pipe(switchMap((account) => (account ? queryStore.createQuery(MailboxesQuery, account.pubkey) : of(undefined)))) .subscribe((mailboxes) => { if (mailboxes) rxNostr.setDefaultRelays(mailboxes.outboxes); else rxNostr.setDefaultRelays([]); }); ``` And that's it! we have a working nostr app that lets users follow and unfollow fiatjaf.
charek's avatar
charek 10 months ago
Does NOSTR already have a standard to sign an event with multiple pubkeys? say you have a post that is actually a collaboration between two authors it's in their interest to reach out to both of their followers with the same event rather than tagging, commenting, replying, they may like to BOTH sign the event, and for BOTH of their user bases to see the same event
charek's avatar
charek 10 months ago
Did you know In India, people have been implementing Open Data protocols on massive scale? Becknprotocol.io they have open ride sharing for example nammayatri.in