Allow video uploads (#1399)
* Allow video uploads * fix video preview --------- Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
b3d9eb0eba
commit
4340a82a62
|
@ -11,14 +11,14 @@ export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio },
|
|||
const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } })
|
||||
const baseCost = sub ? satsToMsats(sub.baseCost) : 1000n
|
||||
|
||||
// cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + image fees + boost
|
||||
// cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost
|
||||
const [{ cost }] = await models.$queryRaw`
|
||||
SELECT ${baseCost}::INTEGER
|
||||
* POWER(10, item_spam(${parseInt(parentId)}::INTEGER, ${me?.id ?? USER_ID.anon}::INTEGER,
|
||||
${me?.id && !bio ? ITEM_SPAM_INTERVAL : ANON_ITEM_SPAM_INTERVAL}::INTERVAL))
|
||||
* ${me ? 1 : 100}::INTEGER
|
||||
+ (SELECT "nUnpaid" * "imageFeeMsats"
|
||||
FROM image_fees_info(${me?.id || USER_ID.anon}::INTEGER, ${uploadIds}::INTEGER[]))
|
||||
+ (SELECT "nUnpaid" * "uploadFeesMsats"
|
||||
FROM upload_fees(${me?.id || USER_ID.anon}::INTEGER, ${uploadIds}::INTEGER[]))
|
||||
+ ${satsToMsats(boost)}::INTEGER as cost`
|
||||
|
||||
// sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { USER_ID } from '@/lib/constants'
|
||||
import { imageFeesInfo } from '../resolvers/image'
|
||||
import { uploadFees } from '../resolvers/upload'
|
||||
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
|
||||
import { notifyItemMention, notifyMention } from '@/lib/webPush'
|
||||
import { satsToMsats } from '@/lib/format'
|
||||
|
@ -12,7 +12,7 @@ export async function getCost ({ id, boost = 0, uploadIds }, { me, models }) {
|
|||
// the only reason updating items costs anything is when it has new uploads
|
||||
// or more boost
|
||||
const old = await models.item.findUnique({ where: { id: parseInt(id) } })
|
||||
const { totalFeesMsats } = await imageFeesInfo(uploadIds, { models, me })
|
||||
const { totalFeesMsats } = await uploadFees(uploadIds, { models, me })
|
||||
return BigInt(totalFeesMsats) + satsToMsats(boost - (old.boost || 0))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { USER_ID, AWS_S3_URL_REGEXP } from '@/lib/constants'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
imageFeesInfo: async (parent, { s3Keys }, { models, me }) => {
|
||||
return imageFeesInfo(s3Keys, { models, me })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function uploadIdsFromText (text, { models }) {
|
||||
if (!text) return []
|
||||
return [...new Set([...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])))]
|
||||
}
|
||||
|
||||
export async function imageFeesInfo (s3Keys, { models, me }) {
|
||||
// returns info object in this format:
|
||||
// { bytes24h: int, bytesUnpaid: int, nUnpaid: int, imageFeeMsats: BigInt }
|
||||
const [info] = await models.$queryRawUnsafe('SELECT * FROM image_fees_info($1::INTEGER, $2::INTEGER[])', me ? me.id : USER_ID.anon, s3Keys)
|
||||
const imageFee = msatsToSats(info.imageFeeMsats)
|
||||
const totalFeesMsats = info.nUnpaid * Number(info.imageFeeMsats)
|
||||
const totalFees = msatsToSats(totalFeesMsats)
|
||||
return { ...info, imageFee, totalFees, totalFeesMsats }
|
||||
}
|
|
@ -16,7 +16,6 @@ import { GraphQLJSONObject as JSONObject } from 'graphql-type-json'
|
|||
import admin from './admin'
|
||||
import blockHeight from './blockHeight'
|
||||
import chainFee from './chainFee'
|
||||
import image from './image'
|
||||
import { GraphQLScalarType, Kind } from 'graphql'
|
||||
import { createIntScalar } from 'graphql-scalar'
|
||||
import paidAction from './paidAction'
|
||||
|
@ -56,4 +55,4 @@ const limit = createIntScalar({
|
|||
|
||||
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
||||
upload, search, growth, rewards, referrals, price, admin, blockHeight, chainFee,
|
||||
image, { JSONObject }, { Date: date }, { Limit: limit }, paidAction]
|
||||
{ JSONObject }, { Date: date }, { Limit: limit }, paidAction]
|
||||
|
|
|
@ -15,7 +15,7 @@ import uu from 'url-unshort'
|
|||
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate'
|
||||
import { defaultCommentSort, isJob, deleteItemByAuthor } from '@/lib/item'
|
||||
import { datePivot, whenRange } from '@/lib/time'
|
||||
import { uploadIdsFromText } from './image'
|
||||
import { uploadIdsFromText } from './upload'
|
||||
import assertGofacYourself from './ofac'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import performPaidAction from '../paidAction'
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW } from '@/lib/constants'
|
||||
import { USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW, AWS_S3_URL_REGEXP } from '@/lib/constants'
|
||||
import { createPresignedPost } from '@/api/s3'
|
||||
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
uploadFees: async (parent, { s3Keys }, { models, me }) => {
|
||||
return uploadFees(s3Keys, { models, me })
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
getSignedPOST: async (parent, { type, size, width, height, avatar }, { models, me }) => {
|
||||
if (UPLOAD_TYPES_ALLOW.indexOf(type) === -1) {
|
||||
|
@ -40,3 +46,18 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function uploadIdsFromText (text, { models }) {
|
||||
if (!text) return []
|
||||
return [...new Set([...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])))]
|
||||
}
|
||||
|
||||
export async function uploadFees (s3Keys, { models, me }) {
|
||||
// returns info object in this format:
|
||||
// { bytes24h: int, bytesUnpaid: int, nUnpaid: int, uploadFeesMsats: BigInt }
|
||||
const [info] = await models.$queryRawUnsafe('SELECT * FROM upload_fees($1::INTEGER, $2::INTEGER[])', me ? me.id : USER_ID.anon, s3Keys)
|
||||
const uploadFees = msatsToSats(info.uploadFeesMsats)
|
||||
const totalFeesMsats = info.nUnpaid * Number(info.uploadFeesMsats)
|
||||
const totalFees = msatsToSats(totalFeesMsats)
|
||||
return { ...info, uploadFees, totalFees, totalFeesMsats }
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { gql } from 'graphql-tag'
|
||||
|
||||
export default gql`
|
||||
type ImageFeesInfo {
|
||||
totalFees: Int!
|
||||
totalFeesMsats: Int!
|
||||
imageFee: Int!
|
||||
imageFeeMsats: Int!
|
||||
nUnpaid: Int!
|
||||
bytesUnpaid: Int!
|
||||
bytes24h: Int!
|
||||
}
|
||||
extend type Query {
|
||||
imageFeesInfo(s3Keys: [Int]!): ImageFeesInfo!
|
||||
}
|
||||
`
|
|
@ -17,7 +17,6 @@ import price from './price'
|
|||
import admin from './admin'
|
||||
import blockHeight from './blockHeight'
|
||||
import chainFee from './chainFee'
|
||||
import image from './image'
|
||||
import paidAction from './paidAction'
|
||||
|
||||
const common = gql`
|
||||
|
@ -39,4 +38,4 @@ const common = gql`
|
|||
`
|
||||
|
||||
export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
|
||||
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, image, paidAction]
|
||||
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, paidAction]
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
import { gql } from 'graphql-tag'
|
||||
|
||||
export default gql`
|
||||
extend type Mutation {
|
||||
getSignedPOST(type: String!, size: Int!, width: Int!, height: Int!, avatar: Boolean): SignedPost!
|
||||
type UploadFees {
|
||||
totalFees: Int!
|
||||
totalFeesMsats: Int!
|
||||
uploadFees: Int!
|
||||
uploadFeesMsats: Int!
|
||||
nUnpaid: Int!
|
||||
bytesUnpaid: Int!
|
||||
bytes24h: Int!
|
||||
}
|
||||
|
||||
type SignedPost {
|
||||
url: String!
|
||||
fields: JSONObject!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
uploadFees(s3Keys: [Int]!): UploadFees!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
getSignedPOST(type: String!, size: Int!, width: Int!, height: Int!, avatar: Boolean): SignedPost!
|
||||
}
|
||||
`
|
||||
|
|
|
@ -5,7 +5,7 @@ import BootstrapForm from 'react-bootstrap/Form'
|
|||
import EditImage from '@/svgs/image-edit-fill.svg'
|
||||
import Moon from '@/svgs/moon-fill.svg'
|
||||
import { useShowModal } from './modal'
|
||||
import { ImageUpload } from './image-upload'
|
||||
import { FileUpload } from './file-upload'
|
||||
|
||||
export default function Avatar ({ onSuccess }) {
|
||||
const [uploading, setUploading] = useState()
|
||||
|
@ -49,7 +49,8 @@ export default function Avatar ({ onSuccess }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<ImageUpload
|
||||
<FileUpload
|
||||
allow='image/*'
|
||||
avatar
|
||||
onError={e => {
|
||||
console.log(e)
|
||||
|
@ -84,6 +85,6 @@ export default function Avatar ({ onSuccess }) {
|
|||
? <Moon className='fill-white spin' />
|
||||
: <EditImage className='fill-white' />}
|
||||
</div>
|
||||
</ImageUpload>
|
||||
</FileUpload>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import gql from 'graphql-tag'
|
|||
import { useMutation } from '@apollo/client'
|
||||
import piexif from 'piexifjs'
|
||||
|
||||
export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload, onSuccess, onError, multiple, avatar }, ref) => {
|
||||
export const FileUpload = forwardRef(({ children, className, onSelect, onUpload, onSuccess, onError, multiple, avatar, allow }, ref) => {
|
||||
const toaster = useToast()
|
||||
ref ??= useRef(null)
|
||||
|
||||
|
@ -19,18 +19,22 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload
|
|||
}`)
|
||||
|
||||
const s3Upload = useCallback(async file => {
|
||||
const img = new window.Image()
|
||||
const element = file.type.startsWith('image/')
|
||||
? new window.Image()
|
||||
: document.createElement('video')
|
||||
|
||||
file = await removeExifData(file)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = async () => {
|
||||
async function onload () {
|
||||
onUpload?.(file)
|
||||
let data
|
||||
const variables = {
|
||||
avatar,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
width: img.width,
|
||||
height: img.height
|
||||
width: element.width,
|
||||
height: element.height
|
||||
}
|
||||
try {
|
||||
({ data } = await getSignedPOST({ variables }))
|
||||
|
@ -66,13 +70,22 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload
|
|||
// key is upload id in database
|
||||
const id = data.getSignedPOST.fields.key
|
||||
onSuccess?.({ ...variables, id, name: file.name, url, file })
|
||||
|
||||
console.log('resolve id', id)
|
||||
resolve(id)
|
||||
}
|
||||
img.onerror = reject
|
||||
img.src = window.URL.createObjectURL(file)
|
||||
|
||||
// img fire 'load' event while videos fire 'loadeddata'
|
||||
element.onload = onload
|
||||
element.onloadeddata = onload
|
||||
|
||||
element.onerror = reject
|
||||
element.src = window.URL.createObjectURL(file)
|
||||
})
|
||||
}, [toaster, getSignedPOST])
|
||||
|
||||
const accept = UPLOAD_TYPES_ALLOW.filter(type => allow ? new RegExp(allow).test(type) : true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
|
@ -80,12 +93,12 @@ export const ImageUpload = forwardRef(({ children, className, onSelect, onUpload
|
|||
type='file'
|
||||
multiple={multiple}
|
||||
className='d-none'
|
||||
accept={UPLOAD_TYPES_ALLOW.join(', ')}
|
||||
accept={accept.join(', ')}
|
||||
onChange={async (e) => {
|
||||
const fileList = e.target.files
|
||||
for (const file of Array.from(fileList)) {
|
||||
if (UPLOAD_TYPES_ALLOW.indexOf(file.type) === -1) {
|
||||
toaster.danger(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
|
||||
if (accept.indexOf(file.type) === -1) {
|
||||
toaster.danger(`image must be ${accept.map(t => t.replace('image/', '').replace('video/', '')).join(', ')}`)
|
||||
continue
|
||||
}
|
||||
if (onSelect) await onSelect?.(file, s3Upload)
|
|
@ -9,7 +9,7 @@ import Dropdown from 'react-bootstrap/Dropdown'
|
|||
import Nav from 'react-bootstrap/Nav'
|
||||
import Row from 'react-bootstrap/Row'
|
||||
import Markdown from '@/svgs/markdown-line.svg'
|
||||
import AddImageIcon from '@/svgs/image-add-line.svg'
|
||||
import AddFileIcon from '@/svgs/file-upload-line.svg'
|
||||
import styles from './form.module.css'
|
||||
import Text from '@/components/text'
|
||||
import AddIcon from '@/svgs/add-fill.svg'
|
||||
|
@ -23,7 +23,7 @@ import textAreaCaret from 'textarea-caret'
|
|||
import ReactDatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
import useDebounceCallback, { debounce } from './use-debounce-callback'
|
||||
import { ImageUpload } from './image-upload'
|
||||
import { FileUpload } from './file-upload'
|
||||
import { AWS_S3_URL_REGEXP } from '@/lib/constants'
|
||||
import { whenRange } from '@/lib/time'
|
||||
import { useFeeButton } from './fee-button'
|
||||
|
@ -122,12 +122,12 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
const previousTab = useRef(tab)
|
||||
const { merge, setDisabled: setSubmitDisabled } = useFeeButton()
|
||||
|
||||
const [updateImageFeesInfo] = useLazyQuery(gql`
|
||||
query imageFeesInfo($s3Keys: [Int]!) {
|
||||
imageFeesInfo(s3Keys: $s3Keys) {
|
||||
const [updateUploadFees] = useLazyQuery(gql`
|
||||
query uploadFees($s3Keys: [Int]!) {
|
||||
uploadFees(s3Keys: $s3Keys) {
|
||||
totalFees
|
||||
nUnpaid
|
||||
imageFee
|
||||
uploadFees
|
||||
bytes24h
|
||||
}
|
||||
}`, {
|
||||
|
@ -136,13 +136,13 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
onError: (err) => {
|
||||
console.error(err)
|
||||
},
|
||||
onCompleted: ({ imageFeesInfo }) => {
|
||||
onCompleted: ({ uploadFees }) => {
|
||||
merge({
|
||||
imageFee: {
|
||||
term: `+ ${numWithUnits(imageFeesInfo.totalFees, { abbreviate: false })}`,
|
||||
label: 'image fee',
|
||||
modifier: cost => cost + imageFeesInfo.totalFees,
|
||||
omit: !imageFeesInfo.totalFees
|
||||
uploadFees: {
|
||||
term: `+ ${numWithUnits(uploadFees.totalFees, { abbreviate: false })}`,
|
||||
label: 'upload fee',
|
||||
modifier: cost => cost + uploadFees.totalFees,
|
||||
omit: !uploadFees.totalFees
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -184,17 +184,17 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
innerRef.current.focus()
|
||||
}, [mention, meta?.value, helpers?.setValue])
|
||||
|
||||
const imageFeesUpdate = useDebounceCallback(
|
||||
const uploadFeesUpdate = useDebounceCallback(
|
||||
(text) => {
|
||||
const s3Keys = text ? [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])) : []
|
||||
updateImageFeesInfo({ variables: { s3Keys } })
|
||||
}, 1000, [updateImageFeesInfo])
|
||||
updateUploadFees({ variables: { s3Keys } })
|
||||
}, 1000, [updateUploadFees])
|
||||
|
||||
const onChangeInner = useCallback((formik, e) => {
|
||||
if (onChange) onChange(formik, e)
|
||||
// check for mention editing
|
||||
const { value, selectionStart } = e.target
|
||||
imageFeesUpdate(value)
|
||||
uploadFeesUpdate(value)
|
||||
|
||||
if (!value || selectionStart === undefined) {
|
||||
setMention(undefined)
|
||||
|
@ -233,7 +233,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
} else {
|
||||
setMention(undefined)
|
||||
}
|
||||
}, [onChange, setMention, imageFeesUpdate])
|
||||
}, [onChange, setMention, uploadFeesUpdate])
|
||||
|
||||
const onKeyDownInner = useCallback((userSuggestOnKeyDown) => {
|
||||
return (e) => {
|
||||
|
@ -321,7 +321,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
<Nav.Link className={styles.previewTab} eventKey='preview' disabled={!meta.value}>preview</Nav.Link>
|
||||
</Nav.Item>
|
||||
<span className='ms-auto text-muted d-flex align-items-center'>
|
||||
<ImageUpload
|
||||
<FileUpload
|
||||
multiple
|
||||
ref={imageUploadRef}
|
||||
className='d-flex align-items-center me-1'
|
||||
|
@ -344,7 +344,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
text = text.replace(`![Uploading ${name}…]()`, `![](${url})`)
|
||||
helpers.setValue(text)
|
||||
const s3Keys = [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
|
||||
updateImageFeesInfo({ variables: { s3Keys } })
|
||||
updateUploadFees({ variables: { s3Keys } })
|
||||
setSubmitDisabled?.(false)
|
||||
}}
|
||||
onError={({ name }) => {
|
||||
|
@ -354,8 +354,8 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
setSubmitDisabled?.(false)
|
||||
}}
|
||||
>
|
||||
<AddImageIcon width={18} height={18} />
|
||||
</ImageUpload>
|
||||
<AddFileIcon width={18} height={18} />
|
||||
</FileUpload>
|
||||
<a
|
||||
className='d-flex align-items-center'
|
||||
href='https://guides.github.com/features/mastering-markdown/' target='_blank' rel='noreferrer'
|
||||
|
|
|
@ -112,7 +112,7 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
|
|||
const { me } = useMe()
|
||||
const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src])
|
||||
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
|
||||
const [isImage, setIsImage] = useState(!video && trusted)
|
||||
const [isImage, setIsImage] = useState(video === false && trusted)
|
||||
const [isVideo, setIsVideo] = useState(video)
|
||||
const showMedia = useMemo(() => tab === 'preview' || me?.privates?.showImagesAndVideos !== false, [tab, me?.privates?.showImagesAndVideos])
|
||||
const embed = useMemo(() => parseEmbedUrl(src), [src])
|
||||
|
@ -123,15 +123,19 @@ export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) =>
|
|||
// make sure it's not a false negative by trying to load URL as <img>
|
||||
const img = new window.Image()
|
||||
img.onload = () => setIsImage(true)
|
||||
img.onerror = () => setIsImage(false)
|
||||
img.src = src
|
||||
const video = document.createElement('video')
|
||||
video.onloadeddata = () => setIsVideo(true)
|
||||
video.onerror = () => setIsVideo(false)
|
||||
video.src = src
|
||||
|
||||
return () => {
|
||||
img.onload = null
|
||||
img.onerror = null
|
||||
img.src = ''
|
||||
video.onloadeddata = null
|
||||
video.onerror = null
|
||||
video.src = ''
|
||||
}
|
||||
}, [src, setIsImage, setIsVideo, showMedia, isVideo, embed])
|
||||
|
|
|
@ -19,7 +19,10 @@ export const UPLOAD_TYPES_ALLOW = [
|
|||
'image/heic',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp'
|
||||
'image/webp',
|
||||
'video/mp4',
|
||||
'video/mpeg',
|
||||
'video/webm'
|
||||
]
|
||||
export const INVOICE_ACTION_NOTIFICATION_TYPES = ['ITEM_CREATE', 'ZAP', 'DOWN_ZAP', 'POLL_VOTE']
|
||||
export const BOUNTY_MIN = 1000
|
||||
|
|
|
@ -87,7 +87,7 @@ export function middleware (request) {
|
|||
"font-src 'self' a.stacker.news",
|
||||
// we want to load images from everywhere but we can limit to HTTPS at least
|
||||
"img-src 'self' a.stacker.news m.stacker.news https: data: blob:" + devSrc,
|
||||
"media-src 'self' a.stacker.news m.stacker.news https:" + devSrc,
|
||||
"media-src 'self' a.stacker.news m.stacker.news https: blob:" + devSrc,
|
||||
// Using nonces and strict-dynamic deploys a strict CSP.
|
||||
// see https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html#strict-policy.
|
||||
// Old browsers will ignore nonce and strict-dynamic and fallback to host-based matching and unsafe-inline
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
-- rename image_fees_info to upload_fees
|
||||
-- also give stackers more free quota (50MB per 24 hours -> 250MB per 24 hours)
|
||||
DROP FUNCTION image_fees_info(user_id INTEGER, upload_ids INTEGER[]);
|
||||
CREATE OR REPLACE FUNCTION upload_fees(user_id INTEGER, upload_ids INTEGER[])
|
||||
RETURNS TABLE (
|
||||
"bytes24h" INTEGER,
|
||||
"bytesUnpaid" INTEGER,
|
||||
"nUnpaid" INTEGER,
|
||||
"uploadFeesMsats" BIGINT
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY SELECT
|
||||
uploadinfo.*,
|
||||
CASE
|
||||
-- anons always pay 100 sats per upload no matter the size
|
||||
WHEN user_id = 27 THEN 100000::BIGINT
|
||||
ELSE CASE
|
||||
-- 250MB are free per stacker and 24 hours
|
||||
WHEN uploadinfo."bytes24h" + uploadinfo."bytesUnpaid" <= 250 * 1024 * 1024 THEN 0::BIGINT
|
||||
-- 250MB-500MB: 10 sats per upload
|
||||
WHEN uploadinfo."bytes24h" + uploadinfo."bytesUnpaid" <= 500 * 1024 * 1024 THEN 10000::BIGINT
|
||||
-- 500MB-1GB: 100 sats per upload
|
||||
WHEN uploadinfo."bytes24h" + uploadinfo."bytesUnpaid" <= 1000 * 1024 * 1024 THEN 100000::BIGINT
|
||||
-- 1GB+: 1k sats per upload
|
||||
ELSE 1000000::BIGINT
|
||||
END
|
||||
END AS "uploadFeesMsats"
|
||||
FROM (
|
||||
SELECT
|
||||
-- how much bytes did stacker upload in last 24 hours?
|
||||
COALESCE(SUM(size) FILTER(WHERE paid = 't' AND created_at >= NOW() - interval '24 hours'), 0)::INTEGER AS "bytes24h",
|
||||
-- how much unpaid bytes do they want to upload now?
|
||||
COALESCE(SUM(size) FILTER(WHERE paid = 'f' AND id = ANY(upload_ids)), 0)::INTEGER AS "bytesUnpaid",
|
||||
-- how many unpaid images do they want to upload now?
|
||||
COALESCE(COUNT(id) FILTER(WHERE paid = 'f' AND id = ANY(upload_ids)), 0)::INTEGER AS "nUnpaid"
|
||||
FROM "Upload"
|
||||
WHERE "Upload"."userId" = user_id
|
||||
) uploadinfo;
|
||||
RETURN;
|
||||
END;
|
||||
$$;
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM13 12V16H11V12H8L12 8L16 12H13Z"></path></svg>
|
After Width: | Height: | Size: 298 B |
Loading…
Reference in New Issue