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