Compare commits

..

2 Commits

Author SHA1 Message Date
ekzyis ec5241ad29
Enable WebLN wallet on 'webln:enabled' (#1385)
* Enable WebLN wallet on 'webln:enabled'

* Optimistically use WebLN for login with lightning

* Don't scope WebLN config to user

* Rename var to wallet
2024-09-10 11:13:39 -05:00
Keyan f0e49c160a
automate meme monday, fact friday, what work wednesday (#1384) 2024-09-10 10:43:41 -05:00
9 changed files with 176 additions and 32 deletions

View File

@ -27,6 +27,18 @@ function QrAuth ({ k1, encodedUrl, callbackUrl }) {
} }
}, [data?.lnAuth]) }, [data?.lnAuth])
useEffect(() => {
if (typeof window.webln === 'undefined') return
// optimistically use WebLN for authentication
async function effect () {
// this will also enable our WebLN wallet
await window.webln.enable()
await window.webln.lnurl(encodedUrl)
}
effect()
}, [encodedUrl])
// output pubkey and k1 // output pubkey and k1
return ( return (
<Qr value={encodedUrl} status='waiting for you' /> <Qr value={encodedUrl} status='waiting for you' />

View File

@ -17,6 +17,9 @@
"@/components/*": [ "@/components/*": [
"components/*" "components/*"
], ],
"@/wallets/*": [
"wallets/*"
],
"@/styles/*": [ "@/styles/*": [
"styles/*" "styles/*"
], ],

View File

@ -21,6 +21,7 @@ import { WalletLoggerProvider } from '@/components/wallet-logger'
import { ChainFeeProvider } from '@/components/chain-fee.js' import { ChainFeeProvider } from '@/components/chain-fee.js'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { HasNewNotesProvider } from '@/components/use-has-new-notes' import { HasNewNotesProvider } from '@/components/use-has-new-notes'
import WebLnProvider from '@/wallets/webln'
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false }) const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
@ -106,24 +107,26 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
<HasNewNotesProvider> <HasNewNotesProvider>
<LoggerProvider> <LoggerProvider>
<WalletLoggerProvider> <WalletLoggerProvider>
<ServiceWorkerProvider> <WebLnProvider>
<PriceProvider price={price}> <ServiceWorkerProvider>
<LightningProvider> <PriceProvider price={price}>
<ToastProvider> <LightningProvider>
<ShowModalProvider> <ToastProvider>
<BlockHeightProvider blockHeight={blockHeight}> <ShowModalProvider>
<ChainFeeProvider chainFee={chainFee}> <BlockHeightProvider blockHeight={blockHeight}>
<ErrorBoundary> <ChainFeeProvider chainFee={chainFee}>
<Component ssrData={ssrData} {...otherProps} /> <ErrorBoundary>
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />} <Component ssrData={ssrData} {...otherProps} />
</ErrorBoundary> {!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
</ChainFeeProvider> </ErrorBoundary>
</BlockHeightProvider> </ChainFeeProvider>
</ShowModalProvider> </BlockHeightProvider>
</ToastProvider> </ShowModalProvider>
</LightningProvider> </ToastProvider>
</PriceProvider> </LightningProvider>
</ServiceWorkerProvider> </PriceProvider>
</ServiceWorkerProvider>
</WebLnProvider>
</WalletLoggerProvider> </WalletLoggerProvider>
</LoggerProvider> </LoggerProvider>
</HasNewNotesProvider> </HasNewNotesProvider>

View File

@ -0,0 +1,37 @@
CREATE OR REPLACE FUNCTION schedule_weekly_posts_job()
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
BEGIN
INSERT INTO pgboss.schedule (name, cron, timezone, data)
VALUES
(
'weeklyPost-meme-mon', '0 10 * * 1', 'America/Chicago',
jsonb_build_object(
'title', 'Meme Monday - Best Bitcoin Meme Gets 10,000 Sats',
'text', E'Time for another round of Meme Monday!\n\nWe have another 10,000 sats up for grabs for this week''s winner.\n\nThe sats will be given to the stacker with the best Bitcoin meme as voted by the "top" filter on this thread at 10am CT tomorrow.\n\nTo post an image on SN, check out our docs [here](https://stacker.news/faq#how-do-i-post-images-on-stacker-news).\n\nSend your best 👇',
'bounty', 10000)
),
(
'weeklyPost-what-wed', '0 10 * * 3', 'America/Chicago',
jsonb_build_object(
'title', 'What are you working on this week?',
'text', E'Calling all stackers!\n\nLeave a comment below to let the SN community know what you''re working on this week. It doesn''t matter how big or small your project is, or how much progress you''ve made.\n\nJust share what you''re up to, and let the community know if you want any feedback or help.'
)
),
(
'weeklyPost-fact-fri', '0 10 * * 5', 'America/Chicago',
jsonb_build_object(
'title', 'Fun Fact Friday - Best Fun Fact Gets 10,000 Sats',
'text', E'Let''s hear all your best fun facts, any topic counts!\n\nThe best comment as voted by the "top" filter at 10am CT tomorrow gets 10,000 sats.\n\nBonus sats for including a source link to your fun fact!\n\nSend your best 👇',
'bounty', 10000)
) ON CONFLICT DO NOTHING;
return 0;
EXCEPTION WHEN OTHERS THEN
return 0;
END;
$$;
SELECT schedule_weekly_posts_job();
DROP FUNCTION IF EXISTS schedule_weekly_posts_job;

View File

@ -412,24 +412,26 @@ export function useWallets () {
function getStorageKey (name, me) { function getStorageKey (name, me) {
let storageKey = `wallet:${name}` let storageKey = `wallet:${name}`
if (me) {
// WebLN has no credentials we need to scope to users
// so we can use the same storage key for all users
if (me && name !== 'webln') {
storageKey = `${storageKey}:${me.id}` storageKey = `${storageKey}:${me.id}`
} }
return storageKey return storageKey
} }
function enableWallet (name, me) { function enableWallet (name, me) {
const key = getStorageKey(name, me) const key = getStorageKey(name, me)
const config = JSON.parse(window.localStorage.getItem(key)) const config = JSON.parse(window.localStorage.getItem(key)) || {}
if (!config) return
config.enabled = true config.enabled = true
window.localStorage.setItem(key, JSON.stringify(config)) window.localStorage.setItem(key, JSON.stringify(config))
} }
function disableWallet (name, me) { function disableWallet (name, me) {
const key = getStorageKey(name, me) const key = getStorageKey(name, me)
const config = JSON.parse(window.localStorage.getItem(key)) const config = JSON.parse(window.localStorage.getItem(key)) || {}
if (!config) return
config.enabled = false config.enabled = false
window.localStorage.setItem(key, JSON.stringify(config)) window.localStorage.setItem(key, JSON.stringify(config))
} }

View File

@ -1,3 +1,6 @@
import { useEffect } from 'react'
import { useWallet } from 'wallets'
export const name = 'webln' export const name = 'webln'
export const fields = [] export const fields = []
@ -19,3 +22,27 @@ export const card = {
subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments', subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments',
badges: ['send only'] badges: ['send only']
} }
export default function WebLnProvider ({ children }) {
const wallet = useWallet(name)
useEffect(() => {
const onEnable = () => {
wallet.enablePayments()
}
const onDisable = () => {
wallet.disablePayments()
}
window.addEventListener('webln:enabled', onEnable)
// event is not fired by Alby browser extension but added here for sake of completeness
window.addEventListener('webln:disabled', onDisable)
return () => {
window.removeEventListener('webln:enabled', onEnable)
window.removeEventListener('webln:disabled', onDisable)
}
}, [])
return children
}

View File

@ -34,6 +34,7 @@ import {
} from './paidAction.js' } from './paidAction.js'
import { thisDay } from './thisDay.js' import { thisDay } from './thisDay.js'
import { isServiceEnabled } from '@/lib/sndev.js' import { isServiceEnabled } from '@/lib/sndev.js'
import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts.js'
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
@ -116,6 +117,8 @@ async function work () {
await boss.work('imgproxy', jobWrapper(imgproxy)) await boss.work('imgproxy', jobWrapper(imgproxy))
await boss.work('deleteUnusedImages', jobWrapper(deleteUnusedImages)) await boss.work('deleteUnusedImages', jobWrapper(deleteUnusedImages))
} }
await boss.work('weeklyPost-*', jobWrapper(weeklyPost))
await boss.work('payWeeklyPostBounty', jobWrapper(payWeeklyPostBounty))
await boss.work('repin-*', jobWrapper(repin)) await boss.work('repin-*', jobWrapper(repin))
await boss.work('trust', jobWrapper(trust)) await boss.work('trust', jobWrapper(trust))
await boss.work('timestampItem', jobWrapper(timestampItem)) await boss.work('timestampItem', jobWrapper(timestampItem))

View File

@ -1,11 +1,11 @@
import { datePivot } from '@/lib/time' import { datePivot } from '@/lib/time'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { numWithUnits, abbrNum } from '@/lib/format' import { numWithUnits, abbrNum } from '@/lib/format'
import { paidActions } from '@/api/paidAction'
import { USER_ID } from '@/lib/constants' import { USER_ID } from '@/lib/constants'
import { getForwardUsers } from '@/api/resolvers/item' import { getForwardUsers } from '@/api/resolvers/item'
import { autoPost } from './weeklyPosts'
export async function thisDay ({ models, apollo }) { export async function thisDay ({ models, apollo, lnd, boss }) {
const days = [] const days = []
let yearsAgo = 1 let yearsAgo = 1
while (datePivot(new Date(), { years: -yearsAgo }) > new Date('2021-06-10')) { while (datePivot(new Date(), { years: -yearsAgo }) > new Date('2021-06-10')) {
@ -33,16 +33,22 @@ ${topStackers(days)}
${topComments(days)} ${topComments(days)}
${topSubs(days)}` ${topSubs(days)}`
const user = await models.user.findUnique({ where: { id: USER_ID.sn } })
const forward = days.map(({ data }) => data.users.users?.[0]?.name).filter(Boolean).map(name => ({ nym: name, pct: 10 })) const forward = days.map(({ data }) => data.users.users?.[0]?.name).filter(Boolean).map(name => ({ nym: name, pct: 10 }))
forward.push({ nym: 'Undisciplined', pct: 50 }) forward.push({ nym: 'Undisciplined', pct: 50 })
const forwardUsers = await getForwardUsers(models, forward) const forwardUsers = await getForwardUsers(models, forward)
await models.$transaction(async tx => {
const context = { tx, cost: BigInt(1), user, models } await autoPost({
const result = await paidActions.ITEM_CREATE.perform({ data: {
text, title: `This Day on SN: ${date}`, subName: 'meta', userId: USER_ID.sn, forwardUsers text,
}, context) title: `This Day on SN: ${date}`,
await paidActions.ITEM_CREATE.onPaid(result, context) subName: 'meta',
userId: USER_ID.sn,
forwardUsers
},
models,
apollo,
lnd,
boss
}) })
} }

51
worker/weeklyPosts.js Normal file
View File

@ -0,0 +1,51 @@
import performPaidAction from '@/api/paidAction'
import { USER_ID } from '@/lib/constants'
import { datePivot } from '@/lib/time'
import gql from 'graphql-tag'
export async function autoPost ({ data: item, models, apollo, lnd, boss }) {
return await performPaidAction('ITEM_CREATE',
{ ...item, subName: 'meta', userId: USER_ID.sn, apiKey: true },
{ models, me: { id: USER_ID.sn }, lnd, forceFeeCredits: true })
}
export async function weeklyPost (args) {
const { result: { id, bounty } } = await autoPost(args)
if (bounty) {
args.boss.send('payWeeklyPostBounty', { id }, { startAfter: datePivot(new Date(), { hours: 24 }) })
}
}
export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd }) {
const itemQ = await apollo.query({
query: gql`
query item($id: ID!) {
item(id: $id) {
userId
bounty
bountyPaidTo
comments(sort: "top") {
id
}
}
}`,
variables: { id }
})
const item = itemQ.data.item
if (item.bountyPaidTo?.length > 0) {
throw new Error('Bounty already paid')
}
const winner = item.comments[0]
if (!winner) {
throw new Error('No winner')
}
await performPaidAction('ZAP',
{ id: winner.id, sats: item.bounty },
{ models, me: { id: USER_ID.sn }, lnd, forceFeeCredits: true })
}