Compare commits
	
		
			2 Commits
		
	
	
		
			7fd4f58e81
			...
			d3ca87a78b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d3ca87a78b | ||
|  | c20a954cfc | 
| @ -1328,7 +1328,7 @@ export const createItem = async (parent, { forward, ...item }, { me, models, lnd | |||||||
|   return resultItem |   return resultItem | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const getForwardUsers = async (models, forward) => { | export const getForwardUsers = async (models, forward) => { | ||||||
|   const fwdUsers = [] |   const fwdUsers = [] | ||||||
|   if (forward) { |   if (forward) { | ||||||
|     // find all users in one db query
 |     // find all users in one db query
 | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ export default function WalletButtonBar ({ | |||||||
|   return ( |   return ( | ||||||
|     <div className={`mt-3 ${className}`}> |     <div className={`mt-3 ${className}`}> | ||||||
|       <div className='d-flex justify-content-between'> |       <div className='d-flex justify-content-between'> | ||||||
|         {wallet.isConfigured && |         {wallet.hasConfig && wallet.isConfigured && | ||||||
|           <Button onClick={onDelete} variant='grey-medium'>{deleteText}</Button>} |           <Button onClick={onDelete} variant='grey-medium'>{deleteText}</Button>} | ||||||
|         {children} |         {children} | ||||||
|         <div className='d-flex align-items-center ms-auto'> |         <div className='d-flex align-items-center ms-auto'> | ||||||
|  | |||||||
| @ -1,215 +0,0 @@ | |||||||
| import { getGetServerSideProps } from '@/api/ssrApollo' |  | ||||||
| import Layout from '@/components/layout' |  | ||||||
| import { datePivot, dayMonthYearToDate } from '@/lib/time' |  | ||||||
| import { gql, useQuery } from '@apollo/client' |  | ||||||
| import { numWithUnits, suffix, abbrNum } from '@/lib/format' |  | ||||||
| import PageLoading from '@/components/page-loading' |  | ||||||
| import { useRouter } from 'next/router' |  | ||||||
| 
 |  | ||||||
| // force SSR to include CSP nonces
 |  | ||||||
| export const getServerSideProps = getGetServerSideProps({ query: null }) |  | ||||||
| 
 |  | ||||||
| const THIS_DAY = gql` |  | ||||||
|   query thisDay($to: String, $from: String) { |  | ||||||
|     posts: items (sort: "top", when: "custom", from: $from, to: $to, limit: 1) { |  | ||||||
|       items { |  | ||||||
|         id |  | ||||||
|         title |  | ||||||
|         text |  | ||||||
|         url |  | ||||||
|         ncomments |  | ||||||
|         sats |  | ||||||
|         boost |  | ||||||
|         subName |  | ||||||
|         user { |  | ||||||
|           name |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     comments: items (sort: "top", type: "comments", when: "custom", from: $from, to: $to, limit: 1) { |  | ||||||
|       items { |  | ||||||
|         id |  | ||||||
|         parentId |  | ||||||
|         text |  | ||||||
|         ncomments |  | ||||||
|         sats |  | ||||||
|         boost |  | ||||||
|         user { |  | ||||||
|           name |  | ||||||
|         } |  | ||||||
|         root { |  | ||||||
|           title |  | ||||||
|           id |  | ||||||
|           subName |  | ||||||
|           user { |  | ||||||
|             name |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     users: topUsers(when: "custom", from: $from, to: $to) { |  | ||||||
|       users { |  | ||||||
|         name |  | ||||||
|         nposts(when: "custom", from: $from, to: $to) |  | ||||||
|         ncomments(when: "custom", from: $from, to: $to) |  | ||||||
|         optional { |  | ||||||
|           stacked(when: "custom", from: $from, to: $to) |  | ||||||
|           spent(when: "custom", from: $from, to: $to) |  | ||||||
|           referrals(when: "custom", from: $from, to: $to) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     territories: topSubs(when: "custom", from: $from, to: $to, limit: 1) { |  | ||||||
|       subs { |  | ||||||
|         name |  | ||||||
|         createdAt |  | ||||||
|         desc |  | ||||||
|         user { |  | ||||||
|           name |  | ||||||
|           id |  | ||||||
|           optional { |  | ||||||
|             streak |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         ncomments(when: "custom", from: $from, to: $to) |  | ||||||
|         nposts(when: "custom", from: $from, to: $to) |  | ||||||
| 
 |  | ||||||
|         optional { |  | ||||||
|           stacked(when: "custom", from: $from, to: $to) |  | ||||||
|           spent(when: "custom", from: $from, to: $to) |  | ||||||
|           revenue(when: "custom", from: $from, to: $to) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| ` |  | ||||||
| 
 |  | ||||||
| export default function Index () { |  | ||||||
|   const router = useRouter() |  | ||||||
|   const days = [] |  | ||||||
|   let day = router.query.day |  | ||||||
|     ? datePivot(dayMonthYearToDate(router.query.day), { years: -1 }) |  | ||||||
|     : datePivot(new Date(), { years: -1 }) |  | ||||||
|   while (day > new Date('2021-06-10')) { |  | ||||||
|     days.push(day) |  | ||||||
|     day = datePivot(day, { years: -1 }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const sep = ` |  | ||||||
| https://imgprxy.stacker.news/fsFoWlgwKYsk5mxx2ijgqU8fg04I_2zA_D28t_grR74/rs:fit:960:540/aHR0cHM6Ly9tLnN0YWNrZXIubmV3cy8yMzc5Ng
 |  | ||||||
| ` |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <Layout> |  | ||||||
|       <code style={{ whiteSpace: 'pre-line' }}> |  | ||||||
|         * * - |  | ||||||
|         {days |  | ||||||
|           .map(day => <ThisDay key={day} day={day} />) |  | ||||||
|           .reduce((acc, x) => acc === null |  | ||||||
|             ? [x] |  | ||||||
|             : [acc, sep, x], null)} |  | ||||||
|       </code> |  | ||||||
|     </Layout> |  | ||||||
|   ) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function ThisDay ({ day }) { |  | ||||||
|   const [from, to] = [ |  | ||||||
|     String(new Date(new Date(day).setHours(0, 0, 0, 0)).getTime()), |  | ||||||
|     String(new Date(new Date(day).setHours(23, 59, 59, 999)).getTime())] |  | ||||||
| 
 |  | ||||||
|   const { data } = useQuery(THIS_DAY, { variables: { from, to } }) |  | ||||||
| 
 |  | ||||||
|   if (!data) return <PageLoading /> |  | ||||||
| 
 |  | ||||||
|   return ` |  | ||||||
| ### ${day.toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' })} 📅 |  | ||||||
| 
 |  | ||||||
| ---- |  | ||||||
| 
 |  | ||||||
| ### 📝 \`TOP POST\` |  | ||||||
| 
 |  | ||||||
| ${topPost(data.posts.items)} |  | ||||||
| 
 |  | ||||||
| ---- |  | ||||||
| 
 |  | ||||||
| ### 💬 \`TOP COMMENT\` |  | ||||||
| 
 |  | ||||||
| ${topComment(data.comments.items)} |  | ||||||
| 
 |  | ||||||
| ---- |  | ||||||
| 
 |  | ||||||
| ### 🏆 \`TOP STACKER\` |  | ||||||
| 
 |  | ||||||
| ${topStacker(data.users.users)} |  | ||||||
| 
 |  | ||||||
| ---- |  | ||||||
| 
 |  | ||||||
| ### 🗺️ \`TOP TERRITORY\` |  | ||||||
| 
 |  | ||||||
| ${topTerritory(data.territories.subs)} |  | ||||||
| ` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const truncateString = (string = '', maxLength = 140) => |  | ||||||
|   string.length > maxLength |  | ||||||
|     ? `${string.substring(0, 250)} […]` |  | ||||||
|     : string |  | ||||||
| 
 |  | ||||||
| function topPost (posts) { |  | ||||||
|   const post = posts?.[0] |  | ||||||
| 
 |  | ||||||
|   if (!post) return 'No top post' |  | ||||||
| 
 |  | ||||||
|   return `**[${post.title}](https://stacker.news/items/${post.id}/r/Undisciplined)**
 |  | ||||||
| ${post.text |  | ||||||
| ? ` |  | ||||||
| #### Excerpt |  | ||||||
| > ${truncateString(post.text)}` |  | ||||||
| : ''} |  | ||||||
| *${numWithUnits(post.sats)} \\ ${numWithUnits(post.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ @${post.user.name} \\ ~${post.subName}*` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function topComment (comments) { |  | ||||||
|   const comment = comments?.[0] |  | ||||||
| 
 |  | ||||||
|   if (!comment) return 'No top comment' |  | ||||||
| 
 |  | ||||||
|   return `**https://stacker.news/items/${comment.root.id}/r/Undisciplined?commentId=${comment.id}**
 |  | ||||||
| 
 |  | ||||||
| ${comment.text |  | ||||||
| ? ` |  | ||||||
| #### Excerpt |  | ||||||
| > ${truncateString(comment.text)}` |  | ||||||
| : ''} |  | ||||||
| 
 |  | ||||||
| *${numWithUnits(comment.sats)} \\ ${numWithUnits(comment.ncomments, { unitSingular: 'reply', unitPlural: 'replies' })} \\ @${comment.user.name}* |  | ||||||
| 
 |  | ||||||
| From **[${comment.root.title}](https://stacker.news/items/${comment.root.id}/r/Undisciplined)** by @${comment.root.user.name} in ~${comment.root.subName}`
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function topStacker (users) { |  | ||||||
|   const userIdx = users.findIndex(u => !!u) |  | ||||||
| 
 |  | ||||||
|   if (userIdx === -1) return 'No top stacker' |  | ||||||
|   const user = users[userIdx] |  | ||||||
| 
 |  | ||||||
|   return `${suffix(userIdx + 1)} place **@${user.name}** ${userIdx > 0 ? `(1st${userIdx > 1 ? `-${suffix(userIdx - 1)}` : ''} hiding)` : ''} |  | ||||||
| 
 |  | ||||||
| *${abbrNum(user.optional?.stacked)} stacked \\ ${abbrNum(user.optional?.spent)} spent \\ ${numWithUnits(user.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(user.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ ${numWithUnits(user.optional.referrals, { unitSingular: 'referral', unitPlural: 'referrals' })}*` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function topTerritory (subs) { |  | ||||||
|   const sub = subs?.[0] |  | ||||||
| 
 |  | ||||||
|   if (!sub) return 'No top territory' |  | ||||||
| 
 |  | ||||||
|   return `**~${sub.name}**
 |  | ||||||
| ${sub.desc |  | ||||||
| ? `> ${truncateString(sub.desc)}` |  | ||||||
| : ''} |  | ||||||
| 
 |  | ||||||
| founded by @${sub.user.name} on ${new Date(sub.createdAt).toDateString()} |  | ||||||
| 
 |  | ||||||
| *${abbrNum(sub.optional?.stacked)} stacked \\ ${abbrNum(sub.optional?.revenue)} revenue \\ ${abbrNum(sub.optional?.spent)} spent \\ ${numWithUnits(sub.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(sub.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })}*` |  | ||||||
| } |  | ||||||
| @ -1 +0,0 @@ | |||||||
| export { default, getServerSideProps } from './[day].js' |  | ||||||
| @ -10,6 +10,7 @@ import Info from '@/components/info' | |||||||
| import Text from '@/components/text' | import Text from '@/components/text' | ||||||
| import { AutowithdrawSettings } from '@/components/autowithdraw-shared' | import { AutowithdrawSettings } from '@/components/autowithdraw-shared' | ||||||
| import dynamic from 'next/dynamic' | import dynamic from 'next/dynamic' | ||||||
|  | import { useEffect, useState } from 'react' | ||||||
| 
 | 
 | ||||||
| const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false }) | const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false }) | ||||||
| 
 | 
 | ||||||
| @ -21,6 +22,14 @@ export default function WalletSettings () { | |||||||
|   const { wallet: name } = router.query |   const { wallet: name } = router.query | ||||||
|   const wallet = useWallet(name) |   const wallet = useWallet(name) | ||||||
| 
 | 
 | ||||||
|  |   const [mounted, setMounted] = useState(false) | ||||||
|  |   useEffect(() => { | ||||||
|  |     // mounted is required since available might depend
 | ||||||
|  |     // on values that are only available on the client (and not during SSR)
 | ||||||
|  |     // and thus we need to render the component again on the client
 | ||||||
|  |     setMounted(true) | ||||||
|  |   }, []) | ||||||
|  | 
 | ||||||
|   const initial = wallet.fields.reduce((acc, field) => { |   const initial = wallet.fields.reduce((acc, field) => { | ||||||
|     // We still need to run over all wallet fields via reduce
 |     // We still need to run over all wallet fields via reduce
 | ||||||
|     // even though we use wallet.config as the initial value
 |     // even though we use wallet.config as the initial value
 | ||||||
| @ -38,11 +47,13 @@ export default function WalletSettings () { | |||||||
|     ? { validate: wallet.fieldValidation } |     ? { validate: wallet.fieldValidation } | ||||||
|     : { schema: wallet.fieldValidation } |     : { schema: wallet.fieldValidation } | ||||||
| 
 | 
 | ||||||
|  |   const available = mounted && wallet.available !== undefined ? wallet.available : wallet.isConfigured | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <CenterLayout> |     <CenterLayout> | ||||||
|       <h2 className='pb-2'>{wallet.card.title}</h2> |       <h2 className='pb-2'>{wallet.card.title}</h2> | ||||||
|       <h6 className='text-muted text-center pb-3'><Text>{wallet.card.subtitle}</Text></h6> |       <h6 className='text-muted text-center pb-3'><Text>{wallet.card.subtitle}</Text></h6> | ||||||
|       {!wallet.walletType && <WalletSecurityBanner />} |       {!wallet.walletType && wallet.hasConfig > 0 && <WalletSecurityBanner />} | ||||||
|       <Form |       <Form | ||||||
|         initial={initial} |         initial={initial} | ||||||
|         {...validateProps} |         {...validateProps} | ||||||
| @ -74,7 +85,7 @@ export default function WalletSettings () { | |||||||
|           ? <AutowithdrawSettings wallet={wallet} /> |           ? <AutowithdrawSettings wallet={wallet} /> | ||||||
|           : ( |           : ( | ||||||
|             <ClientCheckbox |             <ClientCheckbox | ||||||
|               disabled={!wallet.isConfigured} |               disabled={!available} | ||||||
|               initialValue={wallet.status === Status.Enabled} |               initialValue={wallet.status === Status.Enabled} | ||||||
|               label='enabled' |               label='enabled' | ||||||
|               name='enabled' |               name='enabled' | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								prisma/migrations/20240723151608_this_day/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								prisma/migrations/20240723151608_this_day/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | CREATE OR REPLACE FUNCTION schedule_this_day_job() | ||||||
|  | RETURNS INTEGER | ||||||
|  | LANGUAGE plpgsql | ||||||
|  | AS $$ | ||||||
|  | DECLARE | ||||||
|  | BEGIN | ||||||
|  |     INSERT INTO pgboss.schedule (name, cron, timezone) | ||||||
|  |     VALUES ('thisDay', '0 5 * * *', 'America/Chicago') ON CONFLICT DO NOTHING; | ||||||
|  |     return 0; | ||||||
|  | EXCEPTION WHEN OTHERS THEN | ||||||
|  |     return 0; | ||||||
|  | END; | ||||||
|  | $$; | ||||||
|  | 
 | ||||||
|  | SELECT schedule_this_day_job(); | ||||||
|  | DROP FUNCTION IF EXISTS schedule_this_day_job; | ||||||
| @ -64,6 +64,10 @@ Since `name` will also be used in [wallet logs](https://stacker.news/wallet/logs | |||||||
| 
 | 
 | ||||||
| Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits). | Wallet fields define what this wallet requires for configuration and thus are used to construct the forms like the one you can see at [/settings/wallets/lnbits](https://stacker.news/settings/walletslnbits). | ||||||
| 
 | 
 | ||||||
|  | - `available?: boolean` | ||||||
|  | 
 | ||||||
|  | This property can be used to override the default behavior of the `enabled` checkbox in the wallet configuration form. By default, it will be clickable when a wallet is configured. However, if a wallet does not have any configuration, this checkbox will always be disabled. You can set `available` to an expression that will determine when a wallet can be enabled. | ||||||
|  | 
 | ||||||
| - `card: WalletCard` | - `card: WalletCard` | ||||||
| 
 | 
 | ||||||
| Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet. | Wallet cards are the components you can see at [/settings/wallets](https://stacker.news/settings/wallets). This property customizes this card for this wallet. | ||||||
|  | |||||||
| @ -4,5 +4,6 @@ import * as lnc from 'wallets/lnc/client' | |||||||
| import * as lnAddr from 'wallets/lightning-address/client' | import * as lnAddr from 'wallets/lightning-address/client' | ||||||
| import * as cln from 'wallets/cln/client' | import * as cln from 'wallets/cln/client' | ||||||
| import * as lnd from 'wallets/lnd/client' | import * as lnd from 'wallets/lnd/client' | ||||||
|  | import * as webln from 'wallets/webln/client' | ||||||
| 
 | 
 | ||||||
| export default [nwc, lnbits, lnc, lnAddr, cln, lnd] | export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln] | ||||||
|  | |||||||
| @ -29,6 +29,7 @@ export function useWallet (name) { | |||||||
|   const { logger, deleteLogs } = useWalletLogger(wallet) |   const { logger, deleteLogs } = useWalletLogger(wallet) | ||||||
| 
 | 
 | ||||||
|   const [config, saveConfig, clearConfig] = useConfig(wallet) |   const [config, saveConfig, clearConfig] = useConfig(wallet) | ||||||
|  |   const hasConfig = wallet?.fields.length > 0 | ||||||
|   const _isConfigured = isConfigured({ ...wallet, config }) |   const _isConfigured = isConfigured({ ...wallet, config }) | ||||||
| 
 | 
 | ||||||
|   const status = config?.enabled ? Status.Enabled : Status.Initialized |   const status = config?.enabled ? Status.Enabled : Status.Initialized | ||||||
| @ -108,6 +109,7 @@ export function useWallet (name) { | |||||||
|     enable, |     enable, | ||||||
|     disable, |     disable, | ||||||
|     setPriority, |     setPriority, | ||||||
|  |     hasConfig, | ||||||
|     isConfigured: _isConfigured, |     isConfigured: _isConfigured, | ||||||
|     status, |     status, | ||||||
|     enabled, |     enabled, | ||||||
| @ -135,6 +137,11 @@ function useConfig (wallet) { | |||||||
|     ...(hasServerConfig ? serverConfig : {}) |     ...(hasServerConfig ? serverConfig : {}) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   if (wallet?.available !== undefined && config.enabled !== undefined) { | ||||||
|  |     // wallet must be available to be enabled
 | ||||||
|  |     config.enabled &&= wallet.available | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   const saveConfig = useCallback(async (config) => { |   const saveConfig = useCallback(async (config) => { | ||||||
|     if (hasLocalConfig) setLocalConfig(config) |     if (hasLocalConfig) setLocalConfig(config) | ||||||
|     if (hasServerConfig) await setServerConfig(config) |     if (hasServerConfig) await setServerConfig(config) | ||||||
| @ -258,7 +265,13 @@ export function getEnabledWallet (me) { | |||||||
|       const priority = config?.priority |       const priority = config?.priority | ||||||
|       return { ...def, config, priority } |       return { ...def, config, priority } | ||||||
|     }) |     }) | ||||||
|     .filter(({ config }) => config?.enabled) |     .filter(({ available, config }) => { | ||||||
|  |       if (available !== undefined && config?.enabled !== undefined) { | ||||||
|  |         // wallet must be available to be enabled
 | ||||||
|  |         config.enabled &&= available | ||||||
|  |       } | ||||||
|  |       return config?.enabled | ||||||
|  |     }) | ||||||
|     .sort(walletPrioritySort)[0] |     .sort(walletPrioritySort)[0] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								wallets/webln/ATTACH.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								wallets/webln/ATTACH.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | Using webln will require installing the alby browser extension and connecting it to an alby hub connected to `stacker_lnd`. | ||||||
|  | 
 | ||||||
|  | 1. Install the [Alby browser extensions](https://chromewebstore.google.com/detail/alby-bitcoin-wallet-for-l/iokeahhehimjnekafflcihljlcjccdbe?pli=1) | ||||||
|  | 2. Create an Alby account | ||||||
|  | 3. Install the [Alby hub](https://guides.getalby.com/user-guide/v/alby-account-and-browser-extension/alby-hub/alby-hub-other-flavors/desktop) | ||||||
|  | 4. Connect Alby Hub to our regest lnd: | ||||||
|  |     - grpc host: `localhost:10010` | ||||||
|  |     - hex admin.macaroon: `0201036c6e6402f801030a10b28622d3f1881964730f73e04e22b82a1201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e65726174651204726561640000062052ac4803c92801f06bda51762aa006f8e3055ff0a57561df6ae1a7b09ae988fd` | ||||||
|  |     - hex tls cert: `2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494943527a434341653267417749424167495163303676574942755039754b65514e484b62466c6c44414b42676771686b6a4f50515144416a41344d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d52557745775944565151444577773459324d344e44466b0a4d6a59324d7a67774868634e4d6a51774d7a41334d5463774d6a45355768634e4d6a55774e5441794d5463774d6a4535576a41344d523877485159445651514b0a45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d52557745775944565151444577773459324d344e44466b4d6a59324d7a67770a5754415442676371686b6a4f5051494242676771686b6a4f50514d4242774e43414151542f6e77764d486156436664566165496776384d4b532b5348415339630a456c696637587161377173567650695737566e68344d445645426c4d357267306e6b614836563137734343337273652f4f71504c665659316f3448594d4948560a4d41344741315564447745422f775145417749437044415442674e56485355454444414b4267677242674546425163444154415042674e5648524d42416638450a425441444151482f4d42304741315564446751574242516d616d566e2f4b635271486f4e5239646b39433167324d2b6a5354422b42674e5648524545647a42310a6767773459324d344e44466b4d6a59324d7a694343577876593246736147397a6449494c633352685932746c636c3973626d534346476876633351755a47396a0a613256794c6d6c7564475679626d467367675231626d6c3467677031626d6c346347466a613256306767646964575a6a623235756877522f41414142687841410a414141414141414141414141414141414141414268775373477741474d416f4743437147534d343942414d43413067414d4555434946443237335742634d4b7a0a55506f4f4c3862777131354a587472534765504b7041654e3154626c5934513541694541764b74756b2b737378395751465a424569577843536a573567654b6b0a3648423754647873552b5a62664c673d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a` | ||||||
|  | 5. Connect Alby Hub to the alby extension in (2) | ||||||
							
								
								
									
										21
									
								
								wallets/webln/client.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								wallets/webln/client.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | export * from 'wallets/webln' | ||||||
|  | 
 | ||||||
|  | export const sendPayment = async (bolt11) => { | ||||||
|  |   if (typeof window.webln === 'undefined') { | ||||||
|  |     throw new Error('WebLN provider not found') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // this will prompt the user to unlock the wallet if it's locked
 | ||||||
|  |   await window.webln.enable() | ||||||
|  | 
 | ||||||
|  |   // this will prompt for payment if no budget is set
 | ||||||
|  |   const response = await window.webln.sendPayment(bolt11) | ||||||
|  |   if (!response) { | ||||||
|  |     // sendPayment returns nothing if WebLN was enabled
 | ||||||
|  |     // but browser extension that provides WebLN was then disabled
 | ||||||
|  |     // without reloading the page
 | ||||||
|  |     throw new Error('sendPayment returned no response') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return response.preimage | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								wallets/webln/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								wallets/webln/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | import { SSR } from '@/lib/constants' | ||||||
|  | 
 | ||||||
|  | export const name = 'webln' | ||||||
|  | 
 | ||||||
|  | export const fields = [] | ||||||
|  | 
 | ||||||
|  | export const available = SSR ? false : typeof window.webln !== 'undefined' | ||||||
|  | 
 | ||||||
|  | export const card = { | ||||||
|  |   title: 'WebLN', | ||||||
|  |   subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments', | ||||||
|  |   badges: ['send only'] | ||||||
|  | } | ||||||
| @ -26,6 +26,7 @@ import { autoWithdraw } from './autowithdraw.js' | |||||||
| import { saltAndHashEmails } from './saltAndHashEmails.js' | import { saltAndHashEmails } from './saltAndHashEmails.js' | ||||||
| import { remindUser } from './reminder.js' | import { remindUser } from './reminder.js' | ||||||
| import { holdAction, settleAction, settleActionError } from './paidAction.js' | import { holdAction, settleAction, settleActionError } from './paidAction.js' | ||||||
|  | import { thisDay } from './thisDay.js' | ||||||
| 
 | 
 | ||||||
| const { loadEnvConfig } = nextEnv | const { loadEnvConfig } = nextEnv | ||||||
| const { ApolloClient, HttpLink, InMemoryCache } = apolloClient | const { ApolloClient, HttpLink, InMemoryCache } = apolloClient | ||||||
| @ -111,6 +112,7 @@ async function work () { | |||||||
|   await boss.work('settleAction', jobWrapper(settleAction)) |   await boss.work('settleAction', jobWrapper(settleAction)) | ||||||
|   await boss.work('holdAction', jobWrapper(holdAction)) |   await boss.work('holdAction', jobWrapper(holdAction)) | ||||||
|   await boss.work('checkInvoice', jobWrapper(checkInvoice)) |   await boss.work('checkInvoice', jobWrapper(checkInvoice)) | ||||||
|  |   await boss.work('thisDay', jobWrapper(thisDay)) | ||||||
| 
 | 
 | ||||||
|   console.log('working jobs') |   console.log('working jobs') | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										187
									
								
								worker/thisDay.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								worker/thisDay.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | |||||||
|  | import { datePivot } from '@/lib/time' | ||||||
|  | import gql from 'graphql-tag' | ||||||
|  | import { numWithUnits, abbrNum } from '@/lib/format' | ||||||
|  | import { paidActions } from '@/api/paidAction' | ||||||
|  | import { USER_ID } from '@/lib/constants' | ||||||
|  | import { getForwardUsers } from '@/api/resolvers/item' | ||||||
|  | 
 | ||||||
|  | export async function thisDay ({ models, apollo }) { | ||||||
|  |   const days = [] | ||||||
|  |   let yearsAgo = 1 | ||||||
|  |   while (datePivot(new Date(), { years: -yearsAgo }) > new Date('2021-06-10')) { | ||||||
|  |     const [{ from, to }] = await models.$queryRaw` | ||||||
|  |     SELECT (date_trunc('day',  (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') - ${`${yearsAgo} year`}::interval as from, | ||||||
|  |            (date_trunc('day',  (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') - ${`${yearsAgo} year`}::interval + interval '1 day - 1 second' as to` | ||||||
|  | 
 | ||||||
|  |     const { data } = await apollo.query({ | ||||||
|  |       query: THIS_DAY, | ||||||
|  |       variables: { from: new Date(from).getTime().toString(), to: new Date(to).getTime().toString() } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     days.push({ | ||||||
|  |       data, | ||||||
|  |       day: new Date(from).toLocaleString('default', { timeZone: 'America/Chicago', month: 'long', day: 'numeric', year: 'numeric' }) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     yearsAgo++ | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const date = new Date().toLocaleString('default', { timeZone: 'America/Chicago', month: 'long', day: 'numeric' }) | ||||||
|  | 
 | ||||||
|  |   const text = `${topPosts(days)} | ||||||
|  | ${topStackers(days)} | ||||||
|  | ${topComments(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 })) | ||||||
|  |   forward.push({ nym: 'Undisciplined', pct: 50 }) | ||||||
|  |   const forwardUsers = await getForwardUsers(models, forward) | ||||||
|  |   await models.$transaction(async tx => { | ||||||
|  |     const context = { tx, cost: BigInt(1), user, models } | ||||||
|  |     const result = await paidActions.ITEM_CREATE.perform({ | ||||||
|  |       text, title: `This Day on SN: ${date}`, subName: 'meta', userId: USER_ID.sn, forwardUsers | ||||||
|  |     }, context) | ||||||
|  |     await paidActions.ITEM_CREATE.onPaid(result, context) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function topPosts (days) { | ||||||
|  |   let text = '#### Top Posts' | ||||||
|  |   for (const { day, data } of days) { | ||||||
|  |     const post = data.posts.items?.[0] | ||||||
|  |     if (post) { | ||||||
|  |       text += ` | ||||||
|  | - [${post.title}](${process.env.NEXT_PUBLIC_URL}/items/${post.id}) | ||||||
|  |     - ${numWithUnits(post.sats)} \\ ${numWithUnits(post.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ @${post.user.name} \\ ~${post.subName} \\ \`${day}\`` | ||||||
|  |     } else { | ||||||
|  |       text += ` | ||||||
|  | - no top post for \`${day}\`` | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return text | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function topStackers (days) { | ||||||
|  |   let text = '#### Top Stackers' | ||||||
|  |   for (const { day, data } of days) { | ||||||
|  |     const user = data.users.users?.[0] | ||||||
|  |     if (user) { | ||||||
|  |       text += ` | ||||||
|  | - @${user.name} | ||||||
|  |     - ${abbrNum(user.optional?.stacked)} stacked \\ ${abbrNum(user.optional?.spent)} spent \\ ${numWithUnits(user.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(user.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ \`${day}\`` | ||||||
|  |     } else { | ||||||
|  |       text += ` | ||||||
|  | - stacker is in hiding for \`${day}\`` | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return text | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function topComments (days) { | ||||||
|  |   let text = '#### Top Comments' | ||||||
|  |   for (const { day, data } of days) { | ||||||
|  |     const comment = data.comments.items?.[0] | ||||||
|  |     if (comment) { | ||||||
|  |       text += ` | ||||||
|  | - ${process.env.NEXT_PUBLIC_URL}/items/${comment.root.id}?commentId=${comment.id} on [${comment.root.title}](${process.env.NEXT_PUBLIC_URL}/items/${comment.root.id}) | ||||||
|  |     - ${numWithUnits(comment.sats)} \\ ${numWithUnits(comment.ncomments, { unitSingular: 'reply', unitPlural: 'replies' })} \\ @${comment.user.name} \\ \`${day}\` | ||||||
|  |         > ${comment.text.trim().split('\n')[0]} [...]` | ||||||
|  |     } else { | ||||||
|  |       text += ` | ||||||
|  | - no top comment for \`${day}\`` | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return text | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function topSubs (days) { | ||||||
|  |   let text = '#### Top Territories' | ||||||
|  |   for (const { day, data } of days) { | ||||||
|  |     const sub = data.territories.subs?.[0] | ||||||
|  |     if (sub) { | ||||||
|  |       text += ` | ||||||
|  | - ~${sub.name} | ||||||
|  |     - ${abbrNum(sub.optional?.stacked)} stacked \\ ${abbrNum(sub.optional?.revenue)} revenue \\ ${abbrNum(sub.optional?.spent)} spent \\ ${numWithUnits(sub.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(sub.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ \`${day}\`` | ||||||
|  |     } else { | ||||||
|  |       text += ` | ||||||
|  | - no top territory for \`${day}\`` | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return text | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const THIS_DAY = gql` | ||||||
|  |   query thisDay($to: String, $from: String) { | ||||||
|  |     posts: items (sort: "top", when: "custom", from: $from, to: $to, limit: 1) { | ||||||
|  |       items { | ||||||
|  |         id | ||||||
|  |         title | ||||||
|  |         text | ||||||
|  |         url | ||||||
|  |         ncomments | ||||||
|  |         sats | ||||||
|  |         boost | ||||||
|  |         subName | ||||||
|  |         user { | ||||||
|  |           name | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     comments: items (sort: "top", type: "comments", when: "custom", from: $from, to: $to, limit: 1) { | ||||||
|  |       items { | ||||||
|  |         id | ||||||
|  |         parentId | ||||||
|  |         text | ||||||
|  |         ncomments | ||||||
|  |         sats | ||||||
|  |         boost | ||||||
|  |         user { | ||||||
|  |           name | ||||||
|  |         } | ||||||
|  |         root { | ||||||
|  |           title | ||||||
|  |           id | ||||||
|  |           subName | ||||||
|  |           user { | ||||||
|  |             name | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     users: topUsers(when: "custom", from: $from, to: $to, limit: 1) { | ||||||
|  |       users { | ||||||
|  |         name | ||||||
|  |         nposts(when: "custom", from: $from, to: $to) | ||||||
|  |         ncomments(when: "custom", from: $from, to: $to) | ||||||
|  |         optional { | ||||||
|  |           stacked(when: "custom", from: $from, to: $to) | ||||||
|  |           spent(when: "custom", from: $from, to: $to) | ||||||
|  |           referrals(when: "custom", from: $from, to: $to) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     territories: topSubs(when: "custom", from: $from, to: $to, limit: 1) { | ||||||
|  |       subs { | ||||||
|  |         name | ||||||
|  |         createdAt | ||||||
|  |         desc | ||||||
|  |         user { | ||||||
|  |           name | ||||||
|  |           id | ||||||
|  |           optional { | ||||||
|  |             streak | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         ncomments(when: "custom", from: $from, to: $to) | ||||||
|  |         nposts(when: "custom", from: $from, to: $to) | ||||||
|  | 
 | ||||||
|  |         optional { | ||||||
|  |           stacked(when: "custom", from: $from, to: $to) | ||||||
|  |           spent(when: "custom", from: $from, to: $to) | ||||||
|  |           revenue(when: "custom", from: $from, to: $to) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | ` | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user