2024-02-21 18:18:43 +00:00
import { string , ValidationError , number , object , array , addMethod , boolean , date } from 'yup'
2023-11-21 23:32:22 +00:00
import {
BOOST _MIN , MAX _POLL _CHOICE _LENGTH , MAX _TITLE _LENGTH , MAX _POLL _NUM _CHOICES ,
MIN _POLL _NUM _CHOICES , MAX _FORWARDS , BOOST _MULT , MAX _TERRITORY _DESC _LENGTH , POST _TYPES ,
2024-01-07 17:00:24 +00:00
TERRITORY _BILLING _TYPES , MAX _COMMENT _TEXT _LENGTH , MAX _POST _TEXT _LENGTH , MIN _TITLE _LENGTH , BOUNTY _MIN , BOUNTY _MAX , BALANCE _LIMIT _MSATS
2023-11-21 23:32:22 +00:00
} from './constants'
2023-02-08 19:38:04 +00:00
import { SUPPORTED _CURRENCIES } from './currency'
import { NOSTR _MAX _RELAY _NUM , NOSTR _PUBKEY _BECH32 , NOSTR _PUBKEY _HEX } from './nostr'
2024-04-14 22:34:21 +00:00
import { msatsToSats , numWithUnits , abbrNum , ensureB64 , B64 _URL _REGEX } from './format'
2024-03-20 00:37:31 +00:00
import * as usersFragments from '@/fragments/users'
import * as subsFragments from '@/fragments/subs'
2024-02-18 21:08:10 +00:00
import { isInvoicableMacaroon , isInvoiceMacaroon } from './macaroon'
2024-02-14 21:09:13 +00:00
import { parseNwcUrl } from './url'
2024-02-21 18:18:43 +00:00
import { datePivot } from './time'
2024-04-14 22:34:21 +00:00
import { decodeRune } from '@/lib/cln'
2024-02-14 21:09:13 +00:00
2023-11-21 23:32:22 +00:00
const { SUB } = subsFragments
const { NAME _QUERY } = usersFragments
2023-02-08 19:38:04 +00:00
2023-09-26 00:54:35 +00:00
export async function ssValidate ( schema , data , args ) {
2023-02-08 19:38:04 +00:00
try {
if ( typeof schema === 'function' ) {
2023-09-26 00:54:35 +00:00
await schema ( args ) . validate ( data )
2023-02-08 19:38:04 +00:00
} else {
await schema . validate ( data )
}
} catch ( e ) {
2023-07-24 22:50:12 +00:00
if ( e instanceof ValidationError ) {
2023-02-08 19:38:04 +00:00
throw new Error ( ` ${ e . path } : ${ e . message } ` )
}
throw e
}
}
2023-07-24 22:50:12 +00:00
addMethod ( string , 'or' , function ( schemas , msg ) {
2023-02-08 19:38:04 +00:00
return this . test ( {
name : 'or' ,
message : msg ,
test : value => {
if ( Array . isArray ( schemas ) && schemas . length > 1 ) {
const resee = schemas . map ( schema => schema . isValidSync ( value ) )
return resee . some ( res => res )
} else {
throw new TypeError ( 'Schemas is not correct array schema' )
}
} ,
exclusive : false
} )
} )
2024-02-13 19:17:56 +00:00
addMethod ( string , 'url' , function ( schemas , msg = 'invalid url' ) {
return this . test ( {
name : 'url' ,
message : msg ,
test : value => {
try {
// eslint-disable-next-line no-new
new URL ( value )
return true
} catch ( e ) {
try {
// eslint-disable-next-line no-new
new URL ( ` http:// ${ value } ` )
return true
} catch ( e ) {
return false
}
}
} ,
exclusive : false
} )
} )
addMethod ( string , 'ws' , function ( schemas , msg = 'invalid websocket' ) {
return this . test ( {
name : 'ws' ,
message : msg ,
test : value => {
2024-02-14 17:53:41 +00:00
if ( typeof value === 'undefined' ) return true
2024-02-13 19:17:56 +00:00
try {
2024-02-14 17:53:41 +00:00
const url = new URL ( value )
return url . protocol === 'ws:' || url . protocol === 'wss:'
2024-02-13 19:17:56 +00:00
} catch ( e ) {
return false
}
} ,
exclusive : false
} )
} )
addMethod ( string , 'socket' , function ( schemas , msg = 'invalid socket' ) {
return this . test ( {
name : 'socket' ,
message : msg ,
test : value => {
try {
const url = new URL ( ` http:// ${ value } ` )
return url . hostname && url . port && ! url . username && ! url . password &&
( ! url . pathname || url . pathname === '/' ) && ! url . search && ! url . hash
} catch ( e ) {
return false
}
} ,
exclusive : false
} )
} )
2024-02-11 23:39:06 +00:00
addMethod ( string , 'https' , function ( ) {
return this . test ( {
name : 'https' ,
message : 'https required' ,
test : ( url ) => {
try {
return new URL ( url ) . protocol === 'https:'
} catch {
return false
}
}
} )
} )
2024-02-14 21:09:13 +00:00
addMethod ( string , 'wss' , function ( msg ) {
return this . test ( {
name : 'wss' ,
message : msg || 'wss required' ,
test : ( url ) => {
try {
return new URL ( url ) . protocol === 'wss:'
} catch {
return false
}
}
} )
} )
2023-07-24 22:50:12 +00:00
const titleValidator = string ( ) . required ( 'required' ) . trim ( ) . max (
2023-02-08 19:38:04 +00:00
MAX _TITLE _LENGTH ,
2023-09-12 00:59:36 +00:00
( { max , value } ) => ` - ${ Math . abs ( max - value . length ) } characters remaining `
2023-12-08 20:31:06 +00:00
) . min ( MIN _TITLE _LENGTH , ` must be at least ${ MIN _TITLE _LENGTH } characters ` )
2023-02-08 19:38:04 +00:00
2023-12-04 19:20:56 +00:00
const textValidator = ( max ) => string ( ) . trim ( ) . max (
max ,
( { max , value } ) => ` - ${ Math . abs ( max - value . length ) } characters remaining `
)
2023-11-21 23:32:22 +00:00
const nameValidator = string ( )
. required ( 'required' )
. matches ( /^[\w_]+$/ , 'only letters, numbers, and _' )
. max ( 32 , 'too long' )
2023-12-04 19:20:56 +00:00
2023-07-24 22:50:12 +00:00
const intValidator = number ( ) . typeError ( 'must be a number' ) . integer ( 'must be whole' )
2024-01-07 17:00:24 +00:00
const floatValidator = number ( ) . typeError ( 'must be a number' )
const lightningAddressValidator = process . env . NODE _ENV === 'development'
? string ( ) . or (
[ string ( ) . matches ( /^[\w_]+@localhost:\d+$/ ) , string ( ) . email ( ) ] ,
'address is no good' )
: string ( ) . email ( 'address is no good' )
2023-02-08 19:38:04 +00:00
2024-02-18 21:08:10 +00:00
const hexOrBase64Validator = string ( ) . test ( {
name : 'hex-or-base64' ,
message : 'invalid encoding' ,
test : ( val ) => {
2024-04-11 23:59:51 +00:00
if ( typeof val === 'undefined' ) return true
2024-02-18 21:08:10 +00:00
try {
ensureB64 ( val )
return true
} catch {
return false
}
}
} )
2024-02-13 19:17:56 +00:00
2023-09-26 00:54:35 +00:00
async function usernameExists ( name , { client , models } ) {
if ( ! client && ! models ) {
2023-02-08 19:38:04 +00:00
throw new Error ( 'cannot check for user' )
}
// apollo client
2023-09-26 00:54:35 +00:00
if ( client ) {
2023-07-23 15:08:43 +00:00
const { data } = await client . query ( { query : NAME _QUERY , variables : { name } } )
2023-02-08 19:38:04 +00:00
return ! data . nameAvailable
}
// prisma client
2023-09-26 00:54:35 +00:00
const user = await models . user . findUnique ( { where : { name } } )
2023-02-08 19:38:04 +00:00
return ! ! user
}
2024-03-14 16:17:53 +00:00
async function subExists ( name , { client , models , me , filter } ) {
2023-11-21 23:32:22 +00:00
if ( ! client && ! models ) {
throw new Error ( 'cannot check for territory' )
}
let sub
// apollo client
if ( client ) {
2024-03-14 16:17:53 +00:00
const { data } = await client . query ( { query : SUB , variables : { sub : name } , fetchPolicy : 'no-cache' } )
2023-11-21 23:32:22 +00:00
sub = data ? . sub
} else {
sub = await models . sub . findUnique ( { where : { name } } )
}
2024-03-14 16:17:53 +00:00
return ! ! sub && ( ! filter || filter ( sub ) )
2023-11-21 23:32:22 +00:00
}
async function subActive ( name , { client , models , me } ) {
if ( ! client && ! models ) {
throw new Error ( 'cannot check if territory is active' )
}
let sub
// apollo client
if ( client ) {
const { data } = await client . query ( { query : SUB , variables : { sub : name } } )
sub = data ? . sub
} else {
sub = await models . sub . findUnique ( { where : { name } } )
}
2024-03-19 22:23:59 +00:00
return sub ? sub . status !== 'STOPPED' : undefined
2023-11-21 23:32:22 +00:00
}
async function subHasPostType ( name , type , { client , models } ) {
if ( ! client && ! models ) {
throw new Error ( 'cannot check for territory' )
}
// apollo client
if ( client ) {
const { data } = await client . query ( { query : SUB , variables : { name } } )
return ! ! ( data ? . sub ? . postTypes ? . includes ( type ) )
}
// prisma client
const sub = await models . sub . findUnique ( { where : { name } } )
return ! ! ( sub ? . postTypes ? . includes ( type ) )
}
2023-09-26 00:54:35 +00:00
export function advPostSchemaMembers ( { me , existingBoost = 0 , ... args } ) {
const boostMin = existingBoost || BOOST _MIN
2023-02-08 19:38:04 +00:00
return {
boost : intValidator
2023-09-26 00:54:35 +00:00
. min ( boostMin , ` must be ${ existingBoost ? '' : 'blank or ' } at least ${ boostMin } ` ) . test ( {
2023-02-08 19:38:04 +00:00
name : 'boost' ,
2023-09-26 00:54:35 +00:00
test : async boost => ( ! existingBoost && ! boost ) || boost % BOOST _MULT === 0 ,
message : ` must be divisble be ${ BOOST _MULT } `
2023-02-08 19:38:04 +00:00
} ) ,
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
forward : array ( )
. max ( MAX _FORWARDS , ` you can only configure ${ MAX _FORWARDS } forward recipients ` )
. of ( object ( ) . shape ( {
2023-09-12 16:56:59 +00:00
nym : string ( ) . required ( 'must specify a stacker' )
. test ( {
name : 'nym' ,
test : async name => {
if ( ! name || ! name . length ) return false
2023-09-26 00:54:35 +00:00
return await usernameExists ( name , args )
2023-09-12 16:56:59 +00:00
} ,
message : 'stacker does not exist'
} )
. test ( {
name : 'self' ,
test : async name => {
return me ? . name !== name
} ,
message : 'cannot forward to yourself'
} ) ,
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
pct : intValidator . required ( 'must specify a percentage' ) . min ( 1 , 'percentage must be at least 1' ) . max ( 100 , 'percentage must not exceed 100' )
} ) )
. compact ( ( v ) => ! v . nym && ! v . pct )
2023-02-08 19:38:04 +00:00
. test ( {
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
name : 'sum' ,
2023-08-28 12:12:36 +00:00
test : forwards => forwards ? forwards . map ( fwd => Number ( fwd . pct ) ) . reduce ( ( sum , cur ) => sum + cur , 0 ) <= 100 : true ,
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
message : 'the total forward percentage exceeds 100%'
} )
. test ( {
name : 'uniqueStackers' ,
2023-08-28 12:12:36 +00:00
test : forwards => forwards ? new Set ( forwards . map ( fwd => fwd . nym ) ) . size === forwards . length : true ,
multiple forwards on a post (#403)
* multiple forwards on a post
first phase of the multi-forward support
* update the graphql mutation for discussion posts to accept and validate multiple forwards
* update the discussion form to allow multiple forwards in the UI
* start working on db schema changes
* uncomment db schema, add migration to create the new model, and update create_item, update_item
stored procedures
* Propagate updates from discussion to poll, link, and bounty forms
Update the create, update poll sql functions for multi forward support
* Update gql, typedefs, and resolver to return forwarded users in items responses
* UI changes to show multiple forward recipients, and conditional upvote logic changes
* Update notification text to reflect multiple forwards upon vote action
* Disallow duplicate stacker entries
* reduce duplication in populating adv-post-form initial values
* Update item_act sql function to implement multi-way forwarding
* Update referral functions to scale referral bonuses for forwarded users
* Update notification text to reflect non-100% forwarded sats cases
* Update wallet history sql queries to accommodate multi-forward use cases
* Block zaps for posts you are forwarded zaps at the API layer, in addition
to in the UI
* Delete fwdUserId column from Item table as part of migration
* Fix how we calculate stacked sats after partial forwards in wallet history
* Exclude entries from wallet history that are 0 stacked sats from posts with 100% forwarded to other users
* Fix wallet history query for forwarded stacked sats to be scaled by the fwd pct
* Reduce duplication in adv post form, and do some style tweaks for better layout
* Use MAX_FORWARDS constants
* Address various PR feedback
* first enhancement pass
* enhancement pass too
---------
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-23 22:44:17 +00:00
message : 'duplicate stackers cannot be specified to receive forwarded sats'
2023-02-08 19:38:04 +00:00
} )
}
}
2023-11-21 23:32:22 +00:00
export function subSelectSchemaMembers ( args ) {
2023-05-11 00:26:07 +00:00
return {
2023-11-21 23:32:22 +00:00
sub : string ( ) . required ( 'required' ) . test ( {
name : 'sub' ,
test : async sub => {
if ( ! sub || ! sub . length ) return false
2024-03-14 16:17:53 +00:00
return await subExists ( sub , args )
2023-11-21 23:32:22 +00:00
} ,
message : 'pick valid territory'
} ) . test ( {
name : 'sub' ,
test : async sub => {
if ( ! sub || ! sub . length ) return false
2024-03-14 16:17:53 +00:00
return await subActive ( sub , args )
2023-11-21 23:32:22 +00:00
} ,
message : 'territory is not active'
} )
2023-05-11 00:26:07 +00:00
}
}
2023-09-26 00:54:35 +00:00
// for testing advPostSchemaMembers in isolation
export function advSchema ( args ) {
return object ( {
... advPostSchemaMembers ( args )
} )
}
2023-05-11 00:26:07 +00:00
2024-01-12 15:37:50 +00:00
export function lnAddrAutowithdrawSchema ( { me } = { } ) {
2024-01-07 17:00:24 +00:00
return object ( {
2024-02-13 19:17:56 +00:00
address : lightningAddressValidator . required ( 'required' ) . test ( {
name : 'address' ,
2024-01-13 04:22:00 +00:00
test : addr => ! addr . endsWith ( '@stacker.news' ) ,
message : 'automated withdrawals must be external'
2024-01-12 15:37:50 +00:00
} ) ,
2024-02-13 19:17:56 +00:00
... autowithdrawSchemaMembers ( { me } )
} )
}
export function LNDAutowithdrawSchema ( { me } = { } ) {
return object ( {
socket : string ( ) . socket ( ) . required ( 'required' ) ,
macaroon : hexOrBase64Validator . required ( 'required' ) . test ( {
name : 'macaroon' ,
2024-02-14 18:56:37 +00:00
test : v => isInvoiceMacaroon ( v ) || isInvoicableMacaroon ( v ) ,
2024-02-18 21:08:10 +00:00
message : 'not an invoice macaroon or an invoicable macaroon'
2024-02-13 19:17:56 +00:00
} ) ,
cert : hexOrBase64Validator ,
... autowithdrawSchemaMembers ( { me } )
} )
}
2024-04-14 22:34:21 +00:00
export function CLNAutowithdrawSchema ( { me } = { } ) {
return object ( {
socket : string ( ) . socket ( ) . required ( 'required' ) ,
rune : string ( ) . matches ( B64 _URL _REGEX , { message : 'invalid rune' } ) . required ( 'required' )
. test ( {
name : 'rune' ,
test : ( v , context ) => {
const decoded = decodeRune ( v )
if ( ! decoded ) return context . createError ( { message : 'invalid rune' } )
if ( decoded . restrictions . length === 0 ) {
return context . createError ( { message : 'rune must be restricted to method=invoice' } )
}
if ( decoded . restrictions . length !== 1 || decoded . restrictions [ 0 ] . alternatives . length !== 1 ) {
return context . createError ( { message : 'rune must be restricted to method=invoice only' } )
}
if ( decoded . restrictions [ 0 ] . alternatives [ 0 ] !== 'method=invoice' ) {
return context . createError ( { message : 'rune must be restricted to method=invoice only' } )
}
return true
}
} ) ,
cert : hexOrBase64Validator ,
... autowithdrawSchemaMembers ( { me } )
} )
}
2024-02-13 19:17:56 +00:00
export function autowithdrawSchemaMembers ( { me } = { } ) {
return {
priority : boolean ( ) ,
2024-01-07 17:00:24 +00:00
autoWithdrawThreshold : intValidator . required ( 'required' ) . min ( 0 , 'must be at least 0' ) . max ( msatsToSats ( BALANCE _LIMIT _MSATS ) , ` must be at most ${ abbrNum ( msatsToSats ( BALANCE _LIMIT _MSATS ) ) } ` ) ,
autoWithdrawMaxFeePercent : floatValidator . required ( 'required' ) . min ( 0 , 'must be at least 0' ) . max ( 50 , 'must not exceed 50' )
2024-02-13 19:17:56 +00:00
}
2024-01-07 17:00:24 +00:00
}
2023-09-26 00:54:35 +00:00
export function bountySchema ( args ) {
2023-07-24 22:50:12 +00:00
return object ( {
2023-02-08 19:38:04 +00:00
title : titleValidator ,
2023-12-04 19:20:56 +00:00
text : textValidator ( MAX _POST _TEXT _LENGTH ) ,
2023-02-08 19:38:04 +00:00
bounty : intValidator
2023-12-27 16:23:54 +00:00
. min ( BOUNTY _MIN , ` must be at least ${ numWithUnits ( BOUNTY _MIN ) } ` )
. max ( BOUNTY _MAX , ` must be at most ${ numWithUnits ( BOUNTY _MAX ) } ` ) ,
2023-09-26 00:54:35 +00:00
... advPostSchemaMembers ( args ) ,
2023-11-21 23:32:22 +00:00
... subSelectSchemaMembers ( args )
} ) . test ( {
name : 'post-type-supported' ,
test : ( { sub } ) => subHasPostType ( sub , 'BOUNTY' , args ) ,
message : 'territory does not support bounties'
2023-02-08 19:38:04 +00:00
} )
}
2023-09-26 00:54:35 +00:00
export function discussionSchema ( args ) {
2023-07-24 22:50:12 +00:00
return object ( {
2023-02-08 19:38:04 +00:00
title : titleValidator ,
2023-12-04 19:20:56 +00:00
text : textValidator ( MAX _POST _TEXT _LENGTH ) ,
2023-09-26 00:54:35 +00:00
... advPostSchemaMembers ( args ) ,
2023-11-21 23:32:22 +00:00
... subSelectSchemaMembers ( args )
} ) . test ( {
name : 'post-type-supported' ,
test : ( { sub } ) => subHasPostType ( sub , 'DISCUSSION' , args ) ,
message : 'territory does not support discussions'
2023-02-08 19:38:04 +00:00
} )
}
2023-09-26 00:54:35 +00:00
export function linkSchema ( args ) {
2023-07-24 22:50:12 +00:00
return object ( {
2023-02-08 19:38:04 +00:00
title : titleValidator ,
2023-12-04 19:20:56 +00:00
text : textValidator ( MAX _POST _TEXT _LENGTH ) ,
2024-02-13 19:17:56 +00:00
url : string ( ) . url ( ) . required ( 'required' ) ,
2023-09-26 00:54:35 +00:00
... advPostSchemaMembers ( args ) ,
2023-11-21 23:32:22 +00:00
... subSelectSchemaMembers ( args )
} ) . test ( {
name : 'post-type-supported' ,
test : ( { sub } ) => subHasPostType ( sub , 'LINK' , args ) ,
message : 'territory does not support links'
2023-02-08 19:38:04 +00:00
} )
}
2023-09-26 00:54:35 +00:00
export function pollSchema ( { numExistingChoices = 0 , ... args } ) {
2023-07-24 22:50:12 +00:00
return object ( {
2023-02-08 19:38:04 +00:00
title : titleValidator ,
2023-12-04 19:20:56 +00:00
text : textValidator ( MAX _POST _TEXT _LENGTH ) ,
2023-07-24 22:50:12 +00:00
options : array ( ) . of (
string ( ) . trim ( ) . test ( 'my-test' , 'required' , function ( value ) {
2023-02-08 19:38:04 +00:00
return ( this . path !== 'options[0]' && this . path !== 'options[1]' ) || value
} ) . max ( MAX _POLL _CHOICE _LENGTH ,
2023-09-12 00:59:36 +00:00
( { max , value } ) => ` - ${ Math . abs ( max - value . length ) } characters remaining `
2023-02-08 19:38:04 +00:00
)
) . test ( {
message : ` at most ${ MAX _POLL _NUM _CHOICES } choices ` ,
test : arr => arr . length <= MAX _POLL _NUM _CHOICES - numExistingChoices
} ) . test ( {
message : ` at least ${ MIN _POLL _NUM _CHOICES } choices required ` ,
test : arr => arr . length >= MIN _POLL _NUM _CHOICES - numExistingChoices
} ) ,
2024-02-21 18:18:43 +00:00
pollExpiresAt : date ( ) . nullable ( ) . min ( datePivot ( new Date ( ) , { days : 1 } ) , 'Expiration must be at least 1 day in the future' ) ,
2023-09-26 00:54:35 +00:00
... advPostSchemaMembers ( args ) ,
2023-11-21 23:32:22 +00:00
... subSelectSchemaMembers ( args )
} ) . test ( {
name : 'post-type-supported' ,
test : ( { sub } ) => subHasPostType ( sub , 'POLL' , args ) ,
message : 'territory does not support polls'
} )
}
export function territorySchema ( args ) {
return object ( {
name : nameValidator
. test ( {
name : 'name' ,
test : async name => {
if ( ! name || ! name . length ) return false
2024-03-19 22:23:59 +00:00
const editing = ! ! args . sub ? . name
// don't block submission on edits or unarchival
const isEdit = sub => sub . name === args . sub . name
const isArchived = sub => sub . status === 'STOPPED'
const filter = sub => editing ? ! isEdit ( sub ) : ! isArchived ( sub )
const exists = await subExists ( name , { ... args , filter } )
2024-03-14 16:17:53 +00:00
return ! exists
2023-11-21 23:32:22 +00:00
} ,
message : 'taken'
} ) ,
desc : string ( ) . required ( 'required' ) . trim ( ) . max (
MAX _TERRITORY _DESC _LENGTH ,
( { max , value } ) => ` - ${ Math . abs ( max - value . length ) } characters remaining `
) ,
baseCost : intValidator
. min ( 1 , 'must be at least 1' )
. max ( 100000 , 'must be at most 100k' ) ,
postTypes : array ( ) . of ( string ( ) . oneOf ( POST _TYPES ) ) . min ( 1 , 'must support at least one post type' ) ,
2024-02-10 02:35:32 +00:00
billingType : string ( ) . required ( 'required' ) . oneOf ( TERRITORY _BILLING _TYPES , 'required' ) ,
nsfw : boolean ( )
2023-02-08 19:38:04 +00:00
} )
}
2024-03-05 19:56:02 +00:00
export function territoryTransferSchema ( { me , ... args } ) {
return object ( {
userName : nameValidator
. test ( {
name : 'name' ,
test : async name => {
if ( ! name || ! name . length ) return false
return await usernameExists ( name , args )
} ,
message : 'user does not exist'
} )
. test ( {
name : 'name' ,
test : name => ! me || me . name !== name ,
message : 'cannot transfer to yourself'
} )
} )
}
2023-09-26 00:54:35 +00:00
export function userSchema ( args ) {
2023-07-24 22:50:12 +00:00
return object ( {
2023-11-21 23:32:22 +00:00
name : nameValidator
2023-02-08 19:38:04 +00:00
. test ( {
name : 'name' ,
test : async name => {
if ( ! name || ! name . length ) return false
2023-09-26 00:54:35 +00:00
return ! ( await usernameExists ( name , args ) )
2023-02-08 19:38:04 +00:00
} ,
message : 'taken'
} )
} )
}
2023-07-24 22:50:12 +00:00
export const commentSchema = object ( {
2023-12-04 19:20:56 +00:00
text : textValidator ( MAX _COMMENT _TEXT _LENGTH ) . required ( 'required' )
2023-02-08 19:38:04 +00:00
} )
2023-07-24 22:50:12 +00:00
export const jobSchema = object ( {
2023-02-08 19:38:04 +00:00
title : titleValidator ,
2023-07-24 22:50:12 +00:00
company : string ( ) . required ( 'required' ) . trim ( ) ,
2023-12-04 19:20:56 +00:00
text : textValidator ( MAX _POST _TEXT _LENGTH ) . required ( 'required' ) ,
2023-07-24 22:50:12 +00:00
url : string ( )
. or ( [ string ( ) . email ( ) , string ( ) . url ( ) ] , 'invalid url or email' )
2023-02-08 19:38:04 +00:00
. required ( 'required' ) ,
maxBid : intValidator . min ( 0 , 'must be at least 0' ) . required ( 'required' ) ,
2023-07-24 22:50:12 +00:00
location : string ( ) . test (
2023-02-08 19:38:04 +00:00
'no-remote' ,
"don't write remote, just check the box" ,
v => ! v ? . match ( /\bremote\b/gi ) )
. when ( 'remote' , {
is : ( value ) => ! value ,
2023-07-27 00:18:42 +00:00
then : schema => schema . required ( 'required' ) . trim ( )
2023-02-08 19:38:04 +00:00
} )
} )
2023-07-24 22:50:12 +00:00
export const emailSchema = object ( {
email : string ( ) . email ( 'email is no good' ) . required ( 'required' )
2023-02-08 19:38:04 +00:00
} )
2023-07-24 22:50:12 +00:00
export const urlSchema = object ( {
2024-02-13 19:17:56 +00:00
url : string ( ) . url ( ) . required ( 'required' )
2023-02-08 19:38:04 +00:00
} )
2023-07-24 22:50:12 +00:00
export const namedUrlSchema = object ( {
text : string ( ) . required ( 'required' ) . trim ( ) ,
2024-02-13 19:17:56 +00:00
url : string ( ) . url ( ) . required ( 'required' )
2023-02-08 19:38:04 +00:00
} )
2023-07-24 22:50:12 +00:00
export const amountSchema = object ( {
2023-02-08 19:38:04 +00:00
amount : intValidator . required ( 'required' ) . positive ( 'must be positive' )
} )
2023-12-26 22:51:47 +00:00
export const actSchema = object ( {
sats : intValidator . required ( 'required' ) . positive ( 'must be positive' ) ,
act : string ( ) . required ( 'required' ) . oneOf ( [ 'TIP' , 'DONT_LIKE_THIS' ] )
} )
2023-07-24 22:50:12 +00:00
export const settingsSchema = object ( {
2023-02-08 19:38:04 +00:00
tipDefault : intValidator . required ( 'required' ) . positive ( 'must be positive' ) ,
2023-07-24 22:50:12 +00:00
fiatCurrency : string ( ) . required ( 'required' ) . oneOf ( SUPPORTED _CURRENCIES ) ,
2023-10-21 00:09:41 +00:00
withdrawMaxFeeDefault : intValidator . required ( 'required' ) . positive ( 'must be positive' ) ,
2023-07-24 22:50:12 +00:00
nostrPubkey : string ( ) . nullable ( )
2023-02-08 19:38:04 +00:00
. or ( [
2023-07-24 22:50:12 +00:00
string ( ) . nullable ( ) . matches ( NOSTR _PUBKEY _HEX , 'must be 64 hex chars' ) ,
string ( ) . nullable ( ) . matches ( NOSTR _PUBKEY _BECH32 , 'invalid bech32 encoding' ) ] , 'invalid pubkey' ) ,
nostrRelays : array ( ) . of (
2024-02-13 19:17:56 +00:00
string ( ) . ws ( )
2023-02-08 19:38:04 +00:00
) . max ( NOSTR _MAX _RELAY _NUM ,
2023-08-23 20:29:55 +00:00
( { max , value } ) => ` ${ Math . abs ( max - value . length ) } too many ` ) ,
2023-09-12 17:19:26 +00:00
hideBookmarks : boolean ( ) ,
2024-02-14 19:33:31 +00:00
hideGithub : boolean ( ) ,
hideNostr : boolean ( ) ,
hideTwitter : boolean ( ) ,
2023-09-18 18:57:02 +00:00
hideWalletBalance : boolean ( ) ,
2023-09-18 23:00:16 +00:00
diagnostics : boolean ( ) ,
2024-03-17 14:57:50 +00:00
noReferralLinks : boolean ( ) ,
2024-03-25 00:46:12 +00:00
hideIsContributor : boolean ( ) ,
2024-03-25 08:53:34 +00:00
zapUndos : intValidator . nullable ( ) . min ( 0 , 'must be greater or equal to 0' )
2023-02-08 19:38:04 +00:00
} )
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
2023-07-24 22:50:12 +00:00
export const lastAuthRemovalSchema = object ( {
warning : string ( ) . matches ( warningMessage , 'does not match' ) . required ( 'required' )
2023-02-08 19:38:04 +00:00
} )
2023-07-24 22:50:12 +00:00
export const withdrawlSchema = object ( {
invoice : string ( ) . required ( 'required' ) . trim ( ) ,
2023-02-08 19:38:04 +00:00
maxFee : intValidator . required ( 'required' ) . min ( 0 , 'must be at least 0' )
} )
2023-10-03 23:22:56 +00:00
export const lnAddrSchema = ( { payerData , min , max , commentAllowed } = { } ) =>
object ( {
2024-01-07 17:00:24 +00:00
addr : lightningAddressValidator . required ( 'required' ) ,
2023-10-03 23:22:56 +00:00
amount : ( ( ) => {
const schema = intValidator . required ( 'required' ) . positive ( 'must be positive' ) . min (
min || 1 , ` must be at least ${ min || 1 } ` )
return max ? schema . max ( max , ` must be at most ${ max } ` ) : schema
} ) ( ) ,
maxFee : intValidator . required ( 'required' ) . min ( 0 , 'must be at least 0' ) ,
comment : commentAllowed
? string ( ) . max ( commentAllowed , ` must be less than ${ commentAllowed } ` )
: string ( )
} ) . concat ( object ( ) . shape ( Object . keys ( payerData || { } ) . reduce ( ( accum , key ) => {
const entry = payerData [ key ]
if ( key === 'email' ) {
accum [ key ] = string ( ) . email ( )
} else if ( key === 'identifier' ) {
accum [ key ] = boolean ( )
} else {
accum [ key ] = string ( )
}
if ( entry ? . mandatory ) {
accum [ key ] = accum [ key ] . required ( )
}
return accum
} , { } ) ) )
2023-02-08 19:38:04 +00:00
2024-02-08 18:33:13 +00:00
export const lnbitsSchema = object ( {
url : process . env . NODE _ENV === 'development'
2024-02-11 23:39:06 +00:00
? string ( )
. or ( [ string ( ) . matches ( /^(http:\/\/)?localhost:\d+$/ ) , string ( ) . url ( ) ] , 'invalid url' )
. required ( 'required' ) . trim ( ) . https ( )
: string ( ) . url ( ) . required ( 'required' ) . trim ( ) . https ( ) ,
2024-02-08 18:33:13 +00:00
adminKey : string ( ) . length ( 32 )
} )
export const nwcSchema = object ( {
2024-02-14 21:09:13 +00:00
nwcUrl : string ( )
. required ( 'required' )
. test ( async ( nwcUrl , context ) => {
// run validation in sequence to control order of errors
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
try {
await string ( ) . required ( 'required' ) . validate ( nwcUrl )
await string ( ) . matches ( /^nostr\+?walletconnect:\/\// , { message : 'must start with nostr+walletconnect://' } ) . validate ( nwcUrl )
let relayUrl , walletPubkey , secret
try {
( { relayUrl , walletPubkey , secret } = parseNwcUrl ( nwcUrl ) )
} catch {
// invalid URL error. handle as if pubkey validation failed to not confuse user.
throw new Error ( 'pubkey must be 64 hex chars' )
}
await string ( ) . required ( 'pubkey required' ) . trim ( ) . matches ( NOSTR _PUBKEY _HEX , 'pubkey must be 64 hex chars' ) . validate ( walletPubkey )
await string ( ) . required ( 'relay url required' ) . trim ( ) . wss ( 'relay must use wss://' ) . validate ( relayUrl )
await string ( ) . required ( 'secret required' ) . trim ( ) . matches ( /^[0-9a-fA-F]{64}$/ , 'secret must be 64 hex chars' ) . validate ( secret )
} catch ( err ) {
return context . createError ( { message : err . message } )
}
return true
} )
2024-02-08 18:33:13 +00:00
} )
2023-07-24 22:50:12 +00:00
export const bioSchema = object ( {
bio : string ( ) . required ( 'required' ) . trim ( )
2023-02-08 19:38:04 +00:00
} )
2023-07-24 22:50:12 +00:00
export const inviteSchema = object ( {
2023-02-08 19:38:04 +00:00
gift : intValidator . positive ( 'must be greater than 0' ) . required ( 'required' ) ,
limit : intValidator . positive ( 'must be positive' )
} )
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
2023-07-24 22:50:12 +00:00
export const pushSubscriptionSchema = object ( {
endpoint : string ( ) . url ( ) . required ( 'required' ) . trim ( ) ,
p256dh : string ( ) . required ( 'required' ) . trim ( ) ,
auth : string ( ) . required ( 'required' ) . trim ( )
Service worker rework, Web Target Share API & Web Push API (#324)
* npm uninstall next-pwa
next-pwa was last updated in August 2022.
There is also an issue which mentions that next-pwa is abandoned (?): https://github.com/shadowwalker/next-pwa/issues/482
But the main reason for me uninstalling it is that it adds a lot of preconfigured stuff which is not necessary for us.
It even lead to a bug since pages were cached without our knowledge.
So I will go with a different PWA approach. This different approach should do the following:
- make it more transparent what the service worker is doing
- gives us more control to configure the service worker and thus making it easier
* Use workbox-webpack-plugin
Every other plugin (`next-offline`, `next-workbox-webpack-plugin`, `next-with-workbox`, ...) added unnecessary configuration which felt contrary to how PWAs should be built.
(PWAs should progressivly enhance the website in small steps, see https://web.dev/learn/pwa/getting-started/#focus-on-a-feature)
These default configurations even lead to worse UX since they made invalid assumptions about stacker.news:
We _do not_ want to cache our start url and we _do not_ want to cache anything unless explicitly told to.
Almost every page on SN should be fresh for the best UX.
To achieve this, by default, the service worker falls back to the network (as if the service worker wasn't there).
Therefore, this should be the simplest configuration with a valid precache and cache busting support.
In the future, we can try to use prefetching to improve performance of navigation requests.
* Add support for Web Share Target API
See https://developer.chrome.com/articles/web-share-target/
* Use Web Push API for push notifications
I followed this (very good!) guide: https://web.dev/notifications/
* Refactor code related to Web Push
* Send push notification to users on events
* Merge notifications
* Send notification to author of every parent recursively
* Remove unused userId param in savePushSubscription
As it should be, the user id is retrieved from the authenticated user in the backend.
* Resubscribe user if push subscription changed
* Update old subscription if oldEndpoint was given
* Allow users to unsubscribe
* Use LTREE operator instead of recursive query
* Always show checkbox for push notifications
* Justify checkbox to end
* Update title of first push notification
* Fix warning from uncontrolled to controlled
* Add comment about Notification.requestPermission
* Fix timestamp
* Catch error on push subscription toggle
* Wrap function bodies in try/catch
* Use Promise.allSettled
* Filter subscriptions by user notification settings
* Fix user notification filter
* Use skipWaiting
---------
Co-authored-by: ekzyis <ek@stacker.news>
2023-07-04 19:36:07 +00:00
} )
2023-10-03 19:35:53 +00:00
export const lud18PayerDataSchema = ( k1 ) => object ( {
name : string ( ) ,
pubkey : string ( ) ,
email : string ( ) . email ( 'bad email address' ) ,
identifier : string ( )
} )
2024-01-13 00:09:50 +00:00
// check if something is _really_ a number.
// returns true for every number in this range: [-Infinity, ..., 0, ..., Infinity]
export const isNumber = x => typeof x === 'number' && ! Number . isNaN ( x )