Skip to content

Commit 7951f46

Browse files
authored
Merge pull request #578 from gitroomhq/feat/nostr
Nostr provider
2 parents 8b9f060 + b048dbd commit 7951f46

File tree

9 files changed

+944
-565
lines changed

9 files changed

+944
-565
lines changed
15.6 KB
Loading

apps/frontend/src/components/launches/connect.with.wallet.tsx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
2+
3+
export default withProvider(
4+
null,
5+
undefined,
6+
undefined,
7+
async () => {
8+
return true;
9+
},
10+
undefined,
11+
);

apps/frontend/src/components/launches/providers/show.all.providers.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import BlueskyProvider from '@gitroom/frontend/components/launches/providers/blu
2020
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
2121
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
2222
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
23+
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
2324

2425
export const Providers = [
2526
{identifier: 'devto', component: DevtoProvider},
@@ -44,6 +45,7 @@ export const Providers = [
4445
{identifier: 'lemmy', component: LemmyProvider},
4546
{identifier: 'wrapcast', component: WarpcastProvider},
4647
{identifier: 'telegram', component: TelegramProvider},
48+
{identifier: 'nostr', component: NostrProvider},
4749
];
4850

4951

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
import '@neynar/react/dist/style.css';
3+
import React, { FC, useMemo, useState, useCallback, useEffect } from 'react';
4+
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
5+
import { useVariables } from '@gitroom/react/helpers/variable.context';
6+
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
7+
import { useModals } from '@mantine/modals';
8+
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
9+
import {
10+
NeynarAuthButton,
11+
NeynarContextProvider,
12+
Theme,
13+
useNeynarContext,
14+
} from '@neynar/react';
15+
import { INeynarAuthenticatedUser } from '@neynar/react/dist/types/common';
16+
import { ButtonCaster } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
17+
18+
export const WrapcasterProvider: FC<Web3ProviderInterface> = (props) => {
19+
const [_, state] = props.nonce.split('||');
20+
const modal = useModals();
21+
const [hide, setHide] = useState(false);
22+
23+
const auth = useCallback((code: string) => {
24+
setHide(true);
25+
return props.onComplete(code, state);
26+
}, [state]);
27+
28+
return (
29+
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
30+
<TopTitle title={`Add Wrapcast`} />
31+
<button
32+
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
33+
type="button"
34+
onClick={() => modal.closeAll()}
35+
>
36+
<svg
37+
viewBox="0 0 15 15"
38+
fill="none"
39+
xmlns="http://www.w3.org/2000/svg"
40+
width="16"
41+
height="16"
42+
>
43+
<path
44+
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
45+
fill="currentColor"
46+
fillRule="evenodd"
47+
clipRule="evenodd"
48+
></path>
49+
</svg>
50+
</button>
51+
<div className="justify-center items-center flex">
52+
{hide ? (
53+
<div className="justify-center items-center flex -mt-[90px]">
54+
<LoadingComponent width={100} height={100} />
55+
</div>
56+
) : (
57+
<div className="justify-center items-center py-[20px] flex-col w-[500px]">
58+
<ButtonCaster login={auth} />
59+
</div>
60+
)}
61+
</div>
62+
</div>
63+
);
64+
};

libraries/nestjs-libraries/src/integrations/integration.manager.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { LemmyProvider } from '@gitroom/nestjs-libraries/integrations/social/lem
2525
import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.standalone.provider';
2626
import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social/farcaster.provider';
2727
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
28+
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
2829

2930
const socialIntegrationList: SocialProvider[] = [
3031
new XProvider(),
@@ -46,6 +47,7 @@ const socialIntegrationList: SocialProvider[] = [
4647
new LemmyProvider(),
4748
new FarcasterProvider(),
4849
new TelegramProvider(),
50+
new NostrProvider(),
4951
// new MastodonCustomProvider(),
5052
];
5153

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
AuthTokenDetails,
3+
PostDetails,
4+
PostResponse,
5+
SocialProvider,
6+
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
7+
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
8+
import dayjs from 'dayjs';
9+
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
10+
import { getPublicKey, Relay, finalizeEvent } from 'nostr-tools';
11+
import WebSocket from 'ws';
12+
import { AuthService } from '@gitroom/helpers/auth/auth.service';
13+
14+
// @ts-ignore
15+
global.WebSocket = WebSocket;
16+
17+
const list = [
18+
'wss://relay.primal.net',
19+
'wss://relay.damus.io',
20+
'wss://relay.snort.social',
21+
'wss://nostr.wine',
22+
'wss://nos.lol',
23+
'wss://relay.primal.net',
24+
];
25+
26+
export class NostrProvider extends SocialAbstract implements SocialProvider {
27+
identifier = 'nostr';
28+
name = 'Nostr';
29+
isBetweenSteps = false;
30+
scopes = [];
31+
32+
async customFields() {
33+
return [
34+
{
35+
key: 'password',
36+
label: 'Nostr private key',
37+
validation: `/^.{3,}$/`,
38+
type: 'password' as const,
39+
},
40+
];
41+
}
42+
43+
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
44+
return {
45+
refreshToken: '',
46+
expiresIn: 0,
47+
accessToken: '',
48+
id: '',
49+
name: '',
50+
picture: '',
51+
username: '',
52+
};
53+
}
54+
55+
async generateAuthUrl() {
56+
const state = makeId(17);
57+
return {
58+
url: '',
59+
codeVerifier: makeId(10),
60+
state,
61+
};
62+
}
63+
64+
private async findRelayInformation(pubkey: string) {
65+
for (const relay of list) {
66+
const relayInstance = await Relay.connect(relay);
67+
const value = await new Promise<any>((resolve) => {
68+
console.log('connecting');
69+
relayInstance.subscribe([{ kinds: [0], authors: [pubkey] }], {
70+
eoseTimeout: 6000,
71+
onevent: (event) => {
72+
resolve(event);
73+
},
74+
oneose: () => {
75+
resolve({});
76+
},
77+
onclose: () => {
78+
resolve({});
79+
},
80+
});
81+
});
82+
83+
relayInstance.close();
84+
const content = JSON.parse(value?.content || '{}');
85+
if (content.name || content.displayName || content.display_name) {
86+
return content;
87+
}
88+
}
89+
90+
return {};
91+
}
92+
93+
private async publish(pubkey: string, event: any) {
94+
let id = '';
95+
for (const relay of list) {
96+
try {
97+
const relayInstance = await Relay.connect(relay);
98+
const value = new Promise<any>((resolve) => {
99+
relayInstance.subscribe([{ kinds: [1], authors: [pubkey] }], {
100+
eoseTimeout: 6000,
101+
onevent: (event) => {
102+
resolve(event);
103+
},
104+
oneose: () => {
105+
resolve({});
106+
},
107+
onclose: () => {
108+
resolve({});
109+
},
110+
});
111+
});
112+
113+
await relayInstance.publish(event);
114+
const all = await value;
115+
relayInstance.close();
116+
// relayInstance.close();
117+
id = id || all?.id;
118+
} catch (err) {
119+
/**empty**/
120+
}
121+
}
122+
123+
return id;
124+
}
125+
126+
async authenticate(params: {
127+
code: string;
128+
codeVerifier: string;
129+
refresh?: string;
130+
}) {
131+
try {
132+
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
133+
134+
const pubkey = getPublicKey(
135+
Uint8Array.from(
136+
body.password.match(/.{1,2}/g).map((byte: any) => parseInt(byte, 16))
137+
)
138+
);
139+
140+
const user = await this.findRelayInformation(pubkey);
141+
142+
return {
143+
id: String(user.pubkey),
144+
name: user.display_name || user.displayName || 'No Name',
145+
accessToken: AuthService.signJWT({ password: body.password }),
146+
refreshToken: '',
147+
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
148+
picture: user.picture,
149+
username: user.name || 'nousername',
150+
};
151+
} catch (e) {
152+
console.log(e);
153+
return 'Invalid credentials';
154+
}
155+
}
156+
157+
async post(
158+
id: string,
159+
accessToken: string,
160+
postDetails: PostDetails[]
161+
): Promise<PostResponse[]> {
162+
const { password } = AuthService.verifyJWT(accessToken) as any;
163+
164+
let lastId = '';
165+
const ids: PostResponse[] = [];
166+
for (const post of postDetails) {
167+
const textEvent = finalizeEvent(
168+
{
169+
kind: 1, // Text note
170+
content:
171+
post.message + '\n\n' + post.media?.map((m) => m.url).join('\n\n'),
172+
tags: [
173+
...(lastId
174+
? [
175+
['e', lastId, '', 'reply'],
176+
['p', id],
177+
]
178+
: []),
179+
], // Include delegation token in the event
180+
created_at: Math.floor(Date.now() / 1000),
181+
},
182+
password
183+
);
184+
185+
lastId = await this.publish(id, textEvent);
186+
ids.push({
187+
id: post.id,
188+
postId: String(lastId),
189+
releaseURL: `https://primal.net/e/${lastId}`,
190+
status: 'completed',
191+
});
192+
}
193+
194+
return ids;
195+
}
196+
}

0 commit comments

Comments
 (0)