Upgrade squeaklib to v013 (#2165)

* Upgrade to squeaklib v0.13

* Allow different squeak types in server reponse

* Add core method for create resqueak.

* Add make resqueak method for squeak store

* Update db migration to include resqueak hash

* Add db method to insert and get resqueak

* Return resqueak as squeak entry in db query method

* Add RPC method for make resqueak

* Got make resqueak modal working in frontend

* Show different content for resqueak in frontend

* Show resqueaed squeak in display

* Show resqueak correctly

* Got resqueak display working on squeak card component

* Fix display for missing resqueaked squeak.

* Fix display for missing resqueaked squeak in squeak card.

* Got resqueak from squeak card working

* Fix prevent default action on resqueak unlock click

* Fix prevent default on make reply squeak in squeak card

* Got number of replies to resqueaked squeak working

* Fix click on squeak card

* Fix too many characters warning message

* Fix click on squeak card image and name and pubkey

* Rename set reply modal state function

* Fix toggle modals for resqueaked squeak

* Fix form submit action for squeak modals

* Include num resqueaks and num resqueaked resqueaks in get squeak entry db query

* Show number of resqueaks in frontend

* Fix distinct count in query for num replies and num resqueaks

* Update resqueaked squeak in slice state

* Update frontend build
This commit is contained in:
Jonathan Zernik 2022-04-24 17:54:07 -07:00 committed by GitHub
parent ef3d49578f
commit 7658a4d95c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1301 additions and 319 deletions

View file

@ -88,6 +88,8 @@ import {
GetSigningProfilesReply,
GetContactProfilesReply,
MakeSqueakReply,
MakeResqueakRequest,
MakeResqueakReply,
GetSqueakDisplayReply,
GetAncestorSqueakDisplaysReply,
GetReplySqueakDisplaysReply,
@ -340,6 +342,20 @@ export const makeSqueak = (profileId, content, replyTo, hasRecipient, recipientP
});
}
export const makeResqueak = (profileId, resqueakedHash, replyTo) => {
console.log('Calling makeResqueak');
const request = new MakeResqueakRequest();
request.setProfileId(profileId);
request.setResqueakedHash(resqueakedHash);
request.setReplyto(replyTo);
const deser = MakeResqueakReply.deserializeBinary;
return baseRequest({
url: '/makeresqueak',
req: request,
deser: deser,
});
}
export const getSigningProfiles = () => {
console.log('Calling getSigningProfiles');
const request = new GetSigningProfilesRequest();

View file

@ -6,23 +6,25 @@ import moment from 'moment'
import { getProfileImageSrcString } from '../../squeakimages/images';
import { Link, withRouter } from 'react-router-dom'
import { ICON_REPLY, ICON_RETWEET,
ICON_HEART, ICON_HEARTFULL, ICON_DELETE, ICON_CLOSE,ICON_IMGUPLOAD, ICON_LOCKFILL} from '../../Icons'
import axios from 'axios'
import {API_URL} from '../../config'
import MakeSqueak from '../../features/squeaks/MakeSqueak'
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
import BuySqueak from '../../features/squeaks/BuySqueak'
import ContentEditable from 'react-contenteditable'
ICON_HEART, ICON_HEARTFULL, ICON_DELETE, ICON_CLOSE,ICON_IMGUPLOAD, ICON_LOCKFILL} from '../../Icons'
import axios from 'axios'
import {API_URL} from '../../config'
import MakeSqueak from '../../features/squeaks/MakeSqueak'
import MakeResqueak from '../../features/squeaks/MakeResqueak'
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
import BuySqueak from '../../features/squeaks/BuySqueak'
import ContentEditable from 'react-contenteditable'
import {
setLikeSqueak,
setUnlikeSqueak,
fetchSqueakOffers,
} from '../../features/squeaks/squeaksSlice'
import {
setLikeSqueak,
setUnlikeSqueak,
fetchSqueakOffers,
} from '../../features/squeaks/squeaksSlice'
const SqueakCard = React.memo(function SqueakCard(props) {
const [replyModalOpen, setModalOpen] = useState(false)
const SqueakCard = React.memo(function SqueakCard(props) {
const [replyModalOpen, setReplyModalOpen] = useState(false)
const [resqueakModalOpen, setResqueakModalOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [buyModalOpen, setBuyModalOpen] = useState(false)
const [parent, setParent] = useState(false)
@ -32,252 +34,308 @@ const SqueakCard = React.memo(function SqueakCard(props) {
let info
const likeSqueak = (e,id) => {
if(e){ e.stopPropagation() }
console.log('Clicked like with id', id);
dispatch(setLikeSqueak(id));
if(e){
e.preventDefault();
e.stopPropagation();
}
console.log('Clicked like with id', id);
dispatch(setLikeSqueak(id));
}
const unlikeSqueak = (e,id) => {
if(e){ e.stopPropagation() }
console.log('Clicked unlike with id', id);
dispatch(setUnlikeSqueak(id));
if(e){
e.preventDefault();
e.stopPropagation();
}
console.log('Clicked unlike with id', id);
dispatch(setUnlikeSqueak(id));
}
const resqueak = (e,id, resqueakId) => {
e.preventDefault();
e.stopPropagation()
if(props.history.location.pathname.slice(1,5) === 'prof'){
info = { dest: "profile", id, resqueakId }
}else{ info = { id, resqueakId } }
alert('Re-Squeak not yet implemented!');
e.preventDefault();
e.stopPropagation()
if(props.history.location.pathname.slice(1,5) === 'prof'){
info = { dest: "profile", id, resqueakId }
}else{ info = { id, resqueakId } }
alert('Re-Squeak not yet implemented!');
}
const toggleReplyModal = (e, type) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setModalOpen(!replyModalOpen) },20)
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setReplyModalOpen(!replyModalOpen) },20)
}
const toggleResqueakModal = (e, type) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setResqueakModalOpen(!resqueakModalOpen) },20)
}
const toggleDeleteModal = (e, type) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setDeleteModalOpen(!deleteModalOpen) },20)
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setDeleteModalOpen(!deleteModalOpen) },20)
}
const toggleBuyModal = (e, type) => {
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setBuyModalOpen(!buyModalOpen) },20)
if(e){
e.preventDefault();
e.stopPropagation();
}
setStyleBody(!styleBody)
if(type === 'parent'){setParent(true)}else{setParent(false)}
setTimeout(()=>{ setBuyModalOpen(!buyModalOpen) },20)
}
const handleModalClick = (e) => {
e.stopPropagation()
e.preventDefault();
e.stopPropagation();
}
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current){ isInitialMount.current = false }
else {
document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
}
}, [styleBody])
if (isInitialMount.current){ isInitialMount.current = false }
else {
document.getElementsByTagName("body")[0].style.cssText = styleBody && "overflow-y: hidden; margin-right: 17px"
}
}, [styleBody])
useEffect( () => () => document.getElementsByTagName("body")[0].style.cssText = "", [] )
useEffect(()=> {
if (isInitialMount.current){ isInitialMount.current = false;}
else if(document.getElementById("replyBox")) {
document.getElementById("replyBox").focus(); }
if (isInitialMount.current){ isInitialMount.current = false;}
else if(document.getElementById("replyBox")) {
document.getElementById("replyBox").focus(); }
}, [replyModalOpen])
const goToUser = (e,username) => {
e.stopPropagation()
props.history.push(`/app/profile/${username}`)
const getRegularSqueakContent = (squeak) => {
return squeak.getContentStr() ?
<div className="card-content-info">
{squeak.getContentStr()}
</div> :
<div onClick={(e)=> {
e.preventDefault();
dispatch(fetchSqueakOffers(props.id));
toggleBuyModal();
}
}
className="card-content-info card-content-locked-content">
<ICON_LOCKFILL styles={{width:'36px', height:"36px", padding: "5px"}} />
<div>
Locked content
</div>
</div>
}
const getResqueakContent = (squeak) => {
const resqueakedHash = squeak.getResqueakedHash();
const resqueakedSqueak = squeak.getResqueakedSqueak();
const resqueakedAuthor = resqueakedSqueak && resqueakedSqueak.getAuthor();
return <div className="squeak-content">
<SqueakCard squeak={resqueakedSqueak} key={resqueakedHash} id={resqueakedHash} user={resqueakedAuthor}
replies={[]} hasReply={false} />
</div>
}
moment.updateLocale('en', {
relativeTime: { future: 'in %s', past: '%s ago', s: 'few seconds ago', ss: '%ss',
m: '1m', mm: '%dm', h: '1h', hh: '%dh', d: 'a day', dd: '%dd', M: 'a month',
MM: '%dM', y: 'a year', yy: '%dY' }
relativeTime: { future: 'in %s', past: '%s ago', s: 'few seconds ago', ss: '%ss',
m: '1m', mm: '%dm', h: '1h', hh: '%dh', d: 'a day', dd: '%dd', M: 'a month',
MM: '%dM', y: 'a year', yy: '%dY' }
});
const author = props.user;
const author = props.user;
return (
return (
<div>
<Link onClick={(e)=>e.stopPropagation()} to={`/app/squeak/${props.id}`} key={props.id} className={props.squeak ? "Squeak-card-wrapper" : "Squeak-card-wrapper missing-squeak"} >
<Link onClick={(e)=>{
e.stopPropagation()
}
} to={`/app/squeak/${props.id}`} key={props.id} className={props.squeak ? "Squeak-card-wrapper" : "Squeak-card-wrapper missing-squeak"} >
{props.squeak ?
<>
<div className="card-userPic-wrapper">
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={author ? `${getProfileImageSrcString(author)}` : null}/>
</Link>
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
</div>
<div className="card-content-wrapper">
<div className="card-content-header">
<div className="card-header-detail">
<span className="card-header-user">
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{author ? author.getProfileName(): 'Unknown Author'}</Link>
</span>
<span className="card-header-username">
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{'@'+ props.squeak.getAuthorPubkey()}</Link>
</span>
<span className="card-header-dot">·</span>
<span className="card-header-date">
{moment(props.squeak.getBlockTime() * 1000).fromNow(true)}
</span>
</div>
<div className="card-header-more">
{props.squeak ?
<>
<div className="card-userPic-wrapper">
<Link onClick={(e)=>{
e.stopPropagation();
}} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={author ? `${getProfileImageSrcString(author)}` : null}/>
</Link>
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
</div>
<div className="card-content-wrapper">
<div className="card-content-header">
<div className="card-header-detail">
<span className="card-header-user">
<Link onClick={(e)=>{
e.stopPropagation();
}} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{author ? author.getProfileName(): 'Unknown Author'}</Link>
</span>
<span className="card-header-username">
<Link onClick={(e)=>{
e.stopPropagation();
}} to={`/app/profile/${props.squeak.getAuthorPubkey()}`}>{'@'+ props.squeak.getAuthorPubkey()}</Link>
</span>
<span className="card-header-dot">·</span>
<span className="card-header-date">
{moment(props.squeak.getBlockTime() * 1000).fromNow(true)}
</span>
</div>
<div className="card-header-more">
</div>
</div>
</div>
{props.squeak.getContentStr() ?
<div className="card-content-info">
{props.squeak.getContentStr()}
</div> :
<div onClick={(e)=> {
e.preventDefault();
dispatch(fetchSqueakOffers(props.id));
toggleBuyModal();
}
}
className="card-content-info card-content-locked-content">
<ICON_LOCKFILL styles={{width:'36px', height:"36px", padding: "5px"}} />
<div>
Locked content
</div>
</div>
{props.squeak.getIsResqueak() ?
getResqueakContent(props.squeak) :
getRegularSqueakContent(props.squeak)
}
<div className="card-buttons-wrapper">
<div onClick={(e)=>toggleReplyModal(e)} className="card-button-wrap reply-wrap">
<div className="card-icon reply-icon">
<ICON_REPLY styles={{fill:'rgb(101, 119, 134)'}}/>
</div>
<div className="card-icon-value">
{props.squeak.getNumReplies()}
</div>
<div onClick={(e)=>toggleReplyModal(e)} className="card-button-wrap reply-wrap">
<div className="card-icon reply-icon">
<ICON_REPLY styles={{fill:'rgb(101, 119, 134)'}}/>
</div>
<div onClick={(e)=>resqueak(e,props.id)} className="card-button-wrap resqueak-wrap">
<div className="card-icon resqueak-icon">
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
</div>
<div className="card-icon-value">
0
</div>
<div className="card-icon-value">
{props.squeak.getNumReplies()}
</div>
<div onClick={(e)=> {
</div>
<div onClick={(e)=>toggleResqueakModal(e)} className="card-button-wrap resqueak-wrap">
<div className="card-icon resqueak-icon">
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
</div>
<div className="card-icon-value">
{props.squeak.getNumResqueaks()}
</div>
</div>
<div onClick={(e)=> {
e.preventDefault();
props.squeak.getLikedTimeMs() ?
unlikeSqueak(e, props.squeak.getSqueakHash()) :
likeSqueak(e, props.squeak.getSqueakHash())
}} className="card-button-wrap heart-wrap">
<div className="card-icon heart-icon">
{props.squeak.getLikedTimeMs() ?
<ICON_HEARTFULL styles={{fill:'rgb(224, 36, 94)'}}/> :
<ICON_HEART styles={{fill:'rgb(101, 119, 134)'}}/>}
<div className="card-icon heart-icon">
{props.squeak.getLikedTimeMs() ?
<ICON_HEARTFULL styles={{fill:'rgb(224, 36, 94)'}}/> :
<ICON_HEART styles={{fill:'rgb(101, 119, 134)'}}/>}
</div>
</div>
</div>
<div onClick={(e)=>toggleDeleteModal(e)} className="card-button-wrap">
<div className="card-icon share-icon">
<div onClick={(e)=>toggleDeleteModal(e)} className="card-button-wrap">
<div className="card-icon share-icon">
<ICON_DELETE styles={{fill:'rgb(101, 119, 134)'}} />
</div>
</div>
</div>
</div>
</div>
</div>
</> :
<>
<div className="card-userPic-wrapper">
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={null}/>
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
</div>
<div className="card-content-wrapper">
<div className="card-content-info">
Missing Squeak
</div>
</div>
</>
}
</Link>
{/* reply modal */}
{props.squeak ?
<div onClick={()=>toggleReplyModal()} style={{display: replyModalOpen ? 'block' : 'none'}} className="modal-edit">
{replyModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleReplyModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</> :
<>
<div className="card-userPic-wrapper">
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={null}/>
{props.hasReply? <div className="squeak-reply-thread"></div> : null}
</div>
<div className="card-content-wrapper">
<div className="card-content-info">
Missing Squeak
</div>
</div>
<p className="modal-title">Reply</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<MakeSqueak replyToSqueak={props.squeak} submittedCallback={toggleReplyModal} />
</div>
</div> : null}
</div> : null}
</div>
</>
}
{/* delete modal */}
{props.squeak ?
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
{deleteModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</Link>
{/* reply modal */}
{props.squeak ?
<div onClick={(e)=>toggleReplyModal(e)} style={{display: replyModalOpen ? 'block' : 'none'}} className="modal-edit">
{replyModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={(e)=>toggleReplyModal(e)} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Reply</p>
</div>
</div>
<p className="modal-title">Delete Squeak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<DeleteSqueak squeakHash={props.id} submittedCallback={toggleDeleteModal} />
</div>
</div> : null}
</div> : null}
{/* buy modal */}
{props.squeak ?
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
{buyModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
<div style={{marginTop:'5px'}} className="modal-body">
<MakeSqueak replyToSqueak={props.squeak} submittedCallback={toggleReplyModal} />
</div>
</div>
<p className="modal-title">Buy Squeak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<BuySqueak squeak={props.squeak} submittedCallback={toggleBuyModal} />
</div>
</div> : null}
</div> : null}
</div> : null}
</div> : null}
{/* resqueak modal */}
{props.squeak ?
<div onClick={(e)=>toggleResqueakModal(e)} style={{display: resqueakModalOpen ? 'block' : 'none'}} className="modal-edit">
{resqueakModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={(e)=>toggleResqueakModal(e)} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Resqueak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<MakeResqueak resqueakedSqueak={props.squeak} submittedCallback={toggleResqueakModal} />
</div>
</div> : null}
</div> : null}
{/* delete modal */}
{props.squeak ?
<div onClick={(e)=>toggleDeleteModal(e)} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
{deleteModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={(e)=>toggleDeleteModal(e)} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Delete Squeak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<DeleteSqueak squeakHash={props.id} submittedCallback={toggleDeleteModal} />
</div>
</div> : null}
</div> : null}
{/* buy modal */}
{props.squeak ?
<div onClick={(e)=>toggleBuyModal(e)} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
{buyModalOpen ?
<div style={{minHeight: '350px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={(e)=>toggleBuyModal(e)} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Buy Squeak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<BuySqueak squeak={props.squeak} submittedCallback={toggleBuyModal} />
</div>
</div> : null}
</div> : null}
</div>
)
});
</div>
)
});
export default withRouter(SqueakCard)
export default withRouter(SqueakCard)

View file

@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'
import { getProfileImageSrcString } from '../../squeakimages/images';
import Loader from '../../components/Loader'
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option } from 'informed';
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, useFormApi } from 'informed';
import { useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'
@ -36,6 +36,17 @@ const DeleteSqueak = (props) => {
const author = props.replyToSqueak && props.replyToSqueak.getAuthor();
const SubmitButton = () => {
const formApi = useFormApi();
return <button
type="submit"
className={'squeak-btn-side squeak-btn-active'}
onClick={formApi.submitForm}>
Delete
</button>
};
const DeleteSqueakForm = () => (
<Form onSubmit={deleteSqueak} className="Squeak-input-side">
<div className="inner-input-links">
@ -44,9 +55,7 @@ const DeleteSqueak = (props) => {
<div className="squeak-btn-holder">
<div style={{ fontSize: '13px', color: null }}>
</div>
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
Delete
</button>
<SubmitButton />
</div>
</div>
</Form>

View file

@ -0,0 +1,165 @@
import React, { useEffect, useState, useContext, useRef } from 'react'
import { withRouter } from 'react-router-dom'
import { unwrapResult } from '@reduxjs/toolkit'
import moment from 'moment'
import ContentEditable from 'react-contenteditable'
import { Link } from 'react-router-dom'
import { getProfileImageSrcString } from '../../squeakimages/images';
import Loader from '../../components/Loader'
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, FormStateAccessor, useFormApi } from 'informed';
import { useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'
import {
setMakeResqueak,
selectMakeResqueakStatus,
selectMakeSqueakStatus,
} from '../squeaks/squeaksSlice'
import {
selectSigningProfiles,
fetchSigningProfiles,
} from '../../features/profiles/profilesSlice'
const MakeResqueak = (props) => {
const signingProfiles = useSelector(selectSigningProfiles);
const makeSqueakStatus = useSelector(selectMakeSqueakStatus);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchSigningProfiles());
}, [])
const submitSqueak = ({ values }) => {
// TODO: toggle modal off here.
console.log(values);
console.log(values.signingProfileId);
if (!values.signingProfileId) {
alert('Signing Profile must be selected.');
return;
}
if (!props.resqueakedSqueak) {
alert('Resqueaked squeak cannot be empty.');
return;
}
const makeValues = {
signingProfile: values.signingProfileId,
resqueakedHash: props.resqueakedSqueak.getSqueakHash(),
replyTo: props.replyToSqueak ? props.replyToSqueak.getSqueakHash() : null,
}
console.log('makeResqueak');
dispatch(setMakeResqueak(makeValues))
.then(unwrapResult)
.then((squeakHash) => {
props.history.push(`/app/squeak/${squeakHash}`);
})
.catch((err) => {
alert(err.message);
});
if (props.submittedCallback) {
props.submittedCallback();
}
}
// TODO: Show profile image for selected signing profile.
// <div className="Squeak-profile-wrapper">
// {signingProfile && <img alt="" style={{ borderRadius: '50%', minWidth: '49px' }} width="100%" height="49px" src={`${getProfileImageSrcString(signingProfile)}`} />}
// </div>
const author = props.resqueakedSqueak && props.resqueakedSqueak.getAuthor();
const validateContent = value => {
if (!value || value.length > 280)
return 'Content must be less than 280 characters';
};
const SubmitButton = () => {
const formApi = useFormApi();
return <button
type="submit"
className={'squeak-btn-side squeak-btn-active'}
onClick={formApi.submitForm}>
Resqueak
</button>
};
const MakeResqueakForm = () => (
<Form onSubmit={submitSqueak} className="Squeak-input-side">
<Select class="informed-select" name="signingProfileId" initialValue="">
<Option value="" disabled>
Select Signing Profile...
</Option>
{signingProfiles.map(p => {
return <option value={p.getProfileId()}>{p.getProfileName()}</option>
})}
</Select>
<div className="inner-input-links">
<div className="input-links-side">
</div>
<div className="squeak-btn-holder">
<SubmitButton />
</div>
</div>
</Form>
);
return (
<>
{/* Squeak being resqueaked. */}
{props.resqueakedSqueak ?
<div className="reply-content-wrapper">
<div className="card-userPic-wrapper">
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.resqueakedSqueak.getAuthorPubkey()}`}>
<img alt="" style={{borderRadius:'50%', minWidth:'49px'}} width="100%" height="49px" src={author ? `${getProfileImageSrcString(props.resqueakedSqueak.getAuthor())}`: null}/>
</Link>
</div>
<div className="card-content-wrapper">
<div className="card-content-header">
<div className="card-header-detail">
<span className="card-header-user">
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.resqueakedSqueak.getAuthorPubkey()}`}>{author ? author.getProfileName(): 'Unknown Author'}</Link>
</span>
<span className="card-header-username">
<Link onClick={(e)=>e.stopPropagation()} to={`/app/profile/${props.resqueakedSqueak.getAuthorPubkey()}`}>{'@'+props.resqueakedSqueak.getAuthorPubkey()}</Link>
</span>
<span className="card-header-dot">·</span>
<span className="card-header-date">
{moment(props.resqueakedSqueak.getBlockTime() * 1000).fromNow()}
</span>
</div>
</div>
<div className="card-content-info">
{props.resqueakedSqueak.getContentStr()}
</div>
<div className="reply-to-user">
<span className="reply-squeak-username">
Replying to
</span>
<span className="main-squeak-user">
@{props.resqueakedSqueak.getAuthorPubkey()}
</span>
</div>
</div>
</div> : null }
{/* New squeak content input. */}
<div className="Squeak-input-wrapper">
<MakeResqueakForm />
</div>
</>
)
}
export default withRouter(MakeResqueak)

View file

@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'
import { getProfileImageSrcString } from '../../squeakimages/images';
import Loader from '../../components/Loader'
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, FormStateAccessor } from 'informed';
import { Form, Input, Select, Checkbox, Relevant, Debug, TextArea, Option, FormStateAccessor, useFormApi } from 'informed';
import { useSelector } from 'react-redux'
import { useDispatch } from 'react-redux'
@ -78,9 +78,20 @@ const MakeSqueak = (props) => {
const author = props.replyToSqueak && props.replyToSqueak.getAuthor();
const validateContent = value => {
if (!value || value.length > 280)
if (value && value.length > 280)
return 'Content must be less than 280 characters';
};
};
const SubmitButton = () => {
const formApi = useFormApi();
return <button
type="submit"
className={'squeak-btn-side squeak-btn-active'}
onClick={formApi.submitForm}>
Squeak
</button>
};
const MakeSqueakForm = () => (
<Form onSubmit={submitSqueak} className="Squeak-input-side">
@ -104,9 +115,7 @@ const MakeSqueak = (props) => {
</div>
)}
</FormStateAccessor>
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
Squeak
</button>
<SubmitButton />
</div>
</div>
</Form>

View file

@ -13,6 +13,7 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
import SqueakCard from '../../components/SqueakCard'
import Loader from '../../components/Loader'
import MakeSqueak from '../squeaks/MakeSqueak'
import MakeResqueak from '../squeaks/MakeResqueak'
import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
import BuySqueak from '../squeaks/BuySqueak'
@ -66,6 +67,7 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
const dispatch = useDispatch();
const [replyModalOpen, setModalOpen] = useState(false)
const [resqueakModalOpen, setResqueakModalOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [buyModalOpen, setBuyModalOpen] = useState(false)
const [spendingModalOpen, setSpendingModalOpen] = useState(false)
@ -92,6 +94,15 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
setModalOpen(!replyModalOpen)
}
const toggleResqueakModal = (e, type) => {
if(e){ e.stopPropagation() }
console.log('Toggling resqueak modal: ', resqueakModalOpen);
// if(param === 'edit'){setSaved(false)}
// if(type === 'parent'){setParent(true)}else{setParent(false)}
setResqueakModalOpen(!resqueakModalOpen)
}
const toggleDeleteModal = (e, type) => {
if(e){ e.stopPropagation() }
// if(param === 'edit'){setSaved(false)}
@ -170,6 +181,32 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
// const squeakOffers = [];
const getRegularSqueakContent = (squeak) => {
return squeak.getContentStr() ?
<div className="squeak-content">
{squeak.getContentStr()}
</div> :
<Link>
<div onClick={()=>toggleBuyModal(props.match.params.id)}
className="squeak-content locked-content">
<ICON_LOCKFILL styles={{width:'48px', height:"48px", padding: "5px"}} />
<div>
Locked content
</div>
</div>
</Link>
}
const getResqueakContent = (squeak) => {
const resqueakedHash = squeak.getResqueakedHash();
const resqueakedSqueak = squeak.getResqueakedSqueak();
const resqueakedAuthor = resqueakedSqueak && resqueakedSqueak.getAuthor();
return <div className="squeak-content">
<SqueakCard squeak={resqueakedSqueak} key={resqueakedHash} id={resqueakedHash} user={resqueakedAuthor}
replies={[]} hasReply={false} />
</div>
}
const squeak = currentSqueak;
const author = currentSqueak && currentSqueak.getAuthor();
@ -199,19 +236,10 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
</div>
</div>
{squeak.getContentStr() ?
<div className="squeak-content">
{squeak.getContentStr()}
</div> :
<Link>
<div onClick={()=>toggleBuyModal(props.match.params.id)}
className="squeak-content locked-content">
<ICON_LOCKFILL styles={{width:'48px', height:"48px", padding: "5px"}} />
<div>
Locked content
</div>
</div>
</Link>
{squeak.getIsResqueak() ?
getResqueakContent(squeak) :
getRegularSqueakContent(squeak)
}
@ -238,10 +266,11 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
<div className="card-icon reply-icon"> <ICON_REPLY /> </div>
{squeak.getNumReplies()}
</div>
<div onClick={()=>resqueak(squeak.getSqueakHash())} className="squeak-int-icon">
<div onClick={()=>toggleResqueakModal()} className="squeak-int-icon">
<div className="card-icon resqueak-icon">
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
</div>
{squeak.getNumResqueaks()}
</div>
<div onClick={()=>{
squeak.getLikedTimeMs() ?
@ -341,88 +370,106 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
</div>:null}
{squeak ?
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
{deleteModalOpen ?
<div onClick={()=>toggleResqueakModal()} style={{display: resqueakModalOpen ? 'block' : 'none'}} className="modal-edit">
{resqueakModalOpen ?
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
<div onClick={()=>toggleResqueakModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Delete Squeak</p>
<p className="modal-title">Resqueak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<DeleteSqueak squeakHash={squeak.getSqueakHash()} submittedCallback={toggleDeleteModal} />
<MakeResqueak resqueakedSqueak={squeak} submittedCallback={toggleResqueakModal} />
</div>
</div> : null}
</div>:null}
{squeak ?
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
{buyModalOpen ?
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
{deleteModalOpen ?
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Buy Squeak</p>
<p className="modal-title">Delete Squeak</p>
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<BuySqueak squeak={squeak} submittedCallback={toggleBuyModal} />
<DeleteSqueak squeakHash={squeak.getSqueakHash()} submittedCallback={toggleDeleteModal} />
</div>
</div> : null}
</div>:null}
{/* Modal for sent payments and received payments */}
{squeak &&
<div onClick={()=>toggleSpendingModal()} style={{display: spendingModalOpen ? 'block' : 'none'}} className="modal-edit">
<div onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header no-b-border">
<div className="modal-closeIcon">
<div onClick={()=>toggleSpendingModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
{squeak ?
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
{buyModalOpen ?
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header">
<div className="modal-closeIcon">
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">Buy Squeak</p>
</div>
<p className="modal-title">{null}</p>
</div>
<div className="modal-body">
<div className="explore-nav-menu">
<div onClick={()=>setTab('Sent Payments')} className={tab =='Sent Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
Sent Payments
</div>
<div onClick={()=>setTab('Received Payments')} className={tab =='Received Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
Received Payments
</div>
<div style={{marginTop:'5px'}} className="modal-body">
<BuySqueak squeak={squeak} submittedCallback={toggleBuyModal} />
</div>
<div className="modal-scroll">
{tab === 'Sent Payments' ?
<>
<SentPayments squeakHash={props.id} />
</>
</div> : null}
</div>:null}
:
tab === 'Received Payments' ?
<>
<ReceivedPayments squeakHash={props.id} />
</>
: <div className="try-searching">
Nothing to see here ..
<div/>
Try searching for people, usernames, or keywords
{/* Modal for sent payments and received payments */}
{squeak &&
<div onClick={()=>toggleSpendingModal()} style={{display: spendingModalOpen ? 'block' : 'none'}} className="modal-edit">
<div onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header no-b-border">
<div className="modal-closeIcon">
<div onClick={()=>toggleSpendingModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE />
</div>
</div>
<p className="modal-title">{null}</p>
</div>
<div className="modal-body">
<div className="explore-nav-menu">
<div onClick={()=>setTab('Sent Payments')} className={tab =='Sent Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
Sent Payments
</div>
<div onClick={()=>setTab('Received Payments')} className={tab =='Received Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
Received Payments
</div>
</div>
<div className="modal-scroll">
{tab === 'Sent Payments' ?
<>
<SentPayments squeakHash={props.id} />
</>
:
tab === 'Received Payments' ?
<>
<ReceivedPayments squeakHash={props.id} />
</>
: <div className="try-searching">
Nothing to see here ..
<div/>
Try searching for people, usernames, or keywords
</div>
}
</div>
}
</div>
</div>
</div>
</div>
</div>}
</div>}
</>
}
</>
}
export default withRouter(Squeak)
export default withRouter(Squeak)

View file

@ -15,6 +15,7 @@ import {
getSearchSqueaks,
getProfileSqueaks,
makeSqueak,
makeResqueak,
deleteSqueak,
getSqueakOffers,
buySqueak,
@ -165,6 +166,23 @@ export const setMakeSqueak = createAsyncThunk(
}
)
export const setMakeResqueak = createAsyncThunk(
'squeaks/setMakeResqueak',
async (values) => {
console.log('Making resqueak');
let profileId = values.signingProfile;
let resqueakedHash = values.resqueakedHash;
let replyTo = values.replyTo;
const response = await makeResqueak(
profileId,
resqueakedHash,
replyTo,
);
return response.getSqueakHash();
}
)
export const fetchSqueakOffers = createAsyncThunk(
'squeaks/fetchSqueakOffers',
async (squeakHash) => {
@ -202,10 +220,32 @@ const updatedSqueakInArray = (squeakArr, newSqueak) => {
if (currentIndex != -1) {
squeakArr[currentIndex] = newSqueak;
}
// Update resqueaked squeaks
for (let i = 0; i < squeakArr.length; i++) {
const currentSqueak = squeakArr[i];
const currentResqueakedSqueak = currentSqueak.getResqueakedSqueak();
if (currentResqueakedSqueak && currentResqueakedSqueak.getSqueakHash() === newSqueak.getSqueakHash()) {
const modifiedSqueak = currentSqueak.clone();
modifiedSqueak.setResqueakedSqueak(newSqueak);
squeakArr[i] = modifiedSqueak;
}
}
}
const removeSqueakInArray = (squeakArr, squeakHash) => {
return squeakArr.filter(squeak => squeak.getSqueakHash() !== squeakHash);
// Remove resqueaked squeaks
for (let i = 0; i < squeakArr.length; i++) {
const currentSqueak = squeakArr[i];
const currentResqueakedSqueak = currentSqueak.getResqueakedSqueak();
if (currentResqueakedSqueak && currentResqueakedSqueak.getSqueakHash() === squeakHash) {
const modifiedSqueak = currentSqueak.clone();
modifiedSqueak.setResqueakedSqueak(null);
squeakArr[i] = modifiedSqueak;
}
}
}
@ -342,6 +382,20 @@ const squeaksSlice = createSlice({
state.makeSqueakStatus = 'idle';
console.log('Go to new squeak');
})
.addCase(setMakeResqueak.pending, (state, action) => {
console.log('setMakeResqueak pending');
state.makeSqueakStatus = 'loading'
})
.addCase(setMakeResqueak.rejected, (state, action) => {
state.makeSqueakStatus = 'idle'
})
.addCase(setMakeResqueak.fulfilled, (state, action) => {
console.log('setMakeResqueak fulfilled');
console.log(action);
const newSqueakHash = action.payload;
state.makeSqueakStatus = 'idle';
console.log('Go to new squeak');
})
.addCase(fetchSqueakOffers.pending, (state, action) => {
state.squeakOffers = [];
state.squeakOffersStatus = 'loading'

View file

@ -128,6 +128,10 @@ service SqueakAdmin {
*/
rpc MakeSqueak (MakeSqueakRequest) returns (MakeSqueakReply) {}
/** sqkadmin: `makeresqueak`
*/
rpc MakeResqueak (MakeResqueakRequest) returns (MakeResqueakReply) {}
/** sqkadmin: `getsqueakdisplay`
*/
rpc GetSqueakDisplay (GetSqueakDisplayRequest) returns (GetSqueakDisplayReply) {}
@ -588,6 +592,22 @@ message MakeSqueakReply {
string squeak_hash = 1;
}
message MakeResqueakRequest {
/// The profile id
int32 profile_id = 1;
/// The content
string resqueaked_hash = 2;
/// The replyto hash
string replyto = 3;
}
message MakeResqueakReply {
/// Hash of the created resqueak.
string squeak_hash = 1;
}
message GetSqueakDisplayRequest {
/// Hash of the squeak.
string squeak_hash = 1;
@ -658,6 +678,18 @@ message SqueakDisplayEntry {
/// Number of replies
int32 num_replies = 20;
/// Is resqueak
bool is_resqueak = 21;
/// Resqueaked hash
string resqueaked_hash = 22;
/// Resqueaked squeak
SqueakDisplayEntry resqueaked_squeak = 23;
/// Number of resqueaks
int32 num_resqueaks = 24;
}
message GetTimelineSqueakDisplaysRequest {

View file

@ -3,4 +3,4 @@ grpcio==1.39.0
grpcio-tools==1.39.0
importlib_resources==1.4.0
pytest==6.2.5
squeaklib==0.12.0
squeaklib==0.13.1

View file

@ -12,6 +12,6 @@ python-bitcoinlib==0.11.0
pyzmq==22.3.0
requests[socks]==2.26.0
SQLAlchemy==1.4.25
squeaklib==0.12.0
squeaklib==0.13.1
typed-config==0.2.5
Werkzeug==2.0.3

View file

@ -67,12 +67,18 @@ def squeak_entry_to_message(squeak_entry: SqueakEntry) -> squeak_admin_pb2.Squea
if squeak_entry.squeak_profile else None),
liked_time_ms=squeak_entry.liked_time_ms, # type: ignore
num_replies=squeak_entry.num_replies,
num_resqueaks=squeak_entry.num_resqueaks,
is_private=(squeak_entry.recipient_public_key is not None),
recipient_pubkey=(squeak_entry.recipient_public_key.to_bytes(
).hex() if squeak_entry.recipient_public_key else None),
is_recipient_known=(squeak_entry.recipient_squeak_profile is not None),
recipient=(squeak_profile_to_message(squeak_entry.recipient_squeak_profile)
if squeak_entry.recipient_squeak_profile else None),
is_resqueak=(squeak_entry.resqueaked_hash is not None),
resqueaked_hash=(squeak_entry.resqueaked_hash.hex()
if squeak_entry.resqueaked_hash else None), # type: ignore
resqueaked_squeak=(squeak_entry_to_message(squeak_entry.resqueaked_squeak)
if squeak_entry.resqueaked_squeak else None), # type: ignore
)
@ -194,6 +200,7 @@ def message_to_squeak_entry(msg: squeak_admin_pb2.SqueakDisplayEntry) -> SqueakE
recipient_squeak_profile=None, # TODO: message to squeak profile
liked_time_ms=(msg.liked_time_ms if msg.liked_time_ms > 0 else None),
num_replies=0,
num_resqueaks=0,
content=(msg.content_str if len(msg.content_str) > 0 else None),
)

View file

@ -309,6 +309,30 @@ class SqueakAdminServerHandler(object):
squeak_hash=inserted_squeak_hash_str,
)
def handle_make_resqueak(self, request):
profile_id = request.profile_id
resqueaked_hash_str = request.resqueaked_hash
resqueaked_hash = bytes.fromhex(resqueaked_hash_str)
replyto_hash_str = request.replyto
replyto_hash = bytes.fromhex(
replyto_hash_str) if replyto_hash_str else None
logger.info(
"Handle make resqueak with author profile id: {}".format(
profile_id,
)
)
inserted_resqueak_hash = self.squeak_controller.make_resqueak(
profile_id,
resqueaked_hash,
replyto_hash,
)
inserted_resqueak_hash_str = optional_squeak_hash_to_hex(
inserted_resqueak_hash,
)
return squeak_admin_pb2.MakeResqueakReply(
squeak_hash=inserted_resqueak_hash_str,
)
def handle_get_squeak_display_entry(self, request):
squeak_hash_str = request.squeak_hash
squeak_hash = bytes.fromhex(squeak_hash_str)

View file

@ -140,6 +140,9 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
def MakeSqueak(self, request, context):
return self.handler.handle_make_squeak(request)
def MakeResqueak(self, request, context):
return self.handler.handle_make_resqueak(request)
def GetSqueakDisplay(self, request, context):
return self.handler.handle_get_squeak_display_entry(request)

View file

@ -313,6 +313,12 @@ def create_app(handler, username, password):
def makesqueakrequest(msg):
return handler.handle_make_squeak(msg)
@app.route("/makeresqueak", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.MakeResqueakRequest())
def makeresqueak(msg):
return handler.handle_make_resqueak(msg)
@app.route("/getsqueakdisplay", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.GetSqueakDisplayRequest())

View file

@ -1,7 +1,7 @@
{
"files": {
"main.css": "/static/css/main.206389cd.css",
"main.js": "/static/js/main.618916f4.js",
"main.js": "/static/js/main.bda102e3.js",
"static/css/306.7f7715a0.chunk.css": "/static/css/306.7f7715a0.chunk.css",
"static/js/306.90b2a66c.chunk.js": "/static/js/306.90b2a66c.chunk.js",
"static/css/900.b66ac5fb.chunk.css": "/static/css/900.b66ac5fb.chunk.css",
@ -11,7 +11,7 @@
"static/media/icon.svg": "/static/media/icon.982210ec9275956e4d3cc77fa90167fd.svg",
"index.html": "/index.html",
"main.206389cd.css.map": "/static/css/main.206389cd.css.map",
"main.618916f4.js.map": "/static/js/main.618916f4.js.map",
"main.bda102e3.js.map": "/static/js/main.bda102e3.js.map",
"306.7f7715a0.chunk.css.map": "/static/css/306.7f7715a0.chunk.css.map",
"306.90b2a66c.chunk.js.map": "/static/js/306.90b2a66c.chunk.js.map",
"900.b66ac5fb.chunk.css.map": "/static/css/900.b66ac5fb.chunk.css.map",
@ -21,6 +21,6 @@
},
"entrypoints": [
"static/css/main.206389cd.css",
"static/js/main.618916f4.js"
"static/js/main.bda102e3.js"
]
}

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#1da1f2"/><meta nam="description" content="Squeaknode!"/><meta name="og:title" content="Squeaknode"/><meta name="og:description" content="check out my the Squeaknode I made"/><link href="https://fonts.googleapis.com/css2?family=Assistant&display=swap" rel="stylesheet"><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><script defer="defer" src="/static/js/main.618916f4.js"></script><link href="/static/css/main.206389cd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#1da1f2"/><meta nam="description" content="Squeaknode!"/><meta name="og:title" content="Squeaknode"/><meta name="og:description" content="check out my the Squeaknode I made"/><link href="https://fonts.googleapis.com/css2?family=Assistant&display=swap" rel="stylesheet"><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><script defer="defer" src="/static/js/main.bda102e3.js"></script><link href="/static/css/main.206389cd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -24,6 +24,7 @@ from typing import List
from typing import Optional
import requests
from squeak.core import CResqueak
from squeak.core import CSqueak
from squeak.core.keys import SqueakPublicKey
@ -97,8 +98,19 @@ class PeerClient:
)
if r.status_code != requests.codes.ok:
return None
squeak_bytes = r.content
return CSqueak.deserialize(squeak_bytes)
logger.info(r)
squeak_json = r.json()
# squeak_bytes = r.content
squeak_type = squeak_json['squeak_type']
squeak_bytes_hex = squeak_json['squeak_bytes']
squeak_bytes = bytes.fromhex(squeak_bytes_hex)
logger.info('Client downloaded: {}'.format(squeak_json))
if squeak_type == 'squeak':
return CSqueak.deserialize(squeak_bytes)
elif squeak_type == 'resqueak':
return CResqueak.deserialize(squeak_bytes)
else:
return None
def get_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
squeak_hash_str = squeak_hash.hex()

View file

@ -26,6 +26,7 @@ from typing import Tuple
import grpc
from bitcoin.core import CBlockHeader
from squeak.core import CBaseSqueak
from squeak.core import CSqueak
from squeaknode.bitcoin.bitcoin_client import BitcoinClient
@ -46,6 +47,7 @@ from squeaknode.core.squeaks import check_squeak
from squeaknode.core.squeaks import get_decrypted_content
from squeaknode.core.squeaks import get_hash
from squeaknode.core.squeaks import get_payment_point_of_secret_key
from squeaknode.core.squeaks import make_resqueak_with_block
from squeaknode.core.squeaks import make_squeak_with_block
from squeaknode.lightning.lightning_client import LightningClient
@ -71,8 +73,6 @@ class SqueakCore:
) -> Tuple[CSqueak, bytes]:
"""Create a new squeak.
TODO: Include the block header in the result tuple.
Args:
signing_profile: The profile of the author of the squeak.
content_str: The content of the squeak as a string.
@ -100,7 +100,40 @@ class SqueakCore:
)
return squeak, secret_key
def check_squeak(self, squeak: CSqueak) -> None:
def make_resqueak(
self,
signing_profile: SqueakProfile,
resqueak_hash: bytes,
replyto_hash: Optional[bytes] = None,
) -> Tuple[CSqueak, bytes]:
"""Create a new resqueak.
Args:
signing_profile: The profile of the author of the squeak.
resqueak_hash: The hash of the squeak to resqueak.
replyto_hash: The hash of the squeak to which this one is replying.
Returns:
CResqueak: the resqueak that was created.
Raises:
Exception: If the profile does not have a signing key.
"""
if signing_profile.private_key is None:
raise Exception("Can't make squeak with a contact profile.")
block_info = self.bitcoin_client.get_best_block_info()
block_height = block_info.block_height
block_hash = block_info.block_hash
squeak = make_resqueak_with_block(
signing_profile.private_key,
resqueak_hash,
block_height,
block_hash,
replyto_hash=replyto_hash,
)
return squeak
def check_squeak(self, base_squeak: CBaseSqueak) -> None:
"""Checks if the squeak is valid and has a valid signature.
Args:
@ -112,9 +145,9 @@ class SqueakCore:
Raises:
Exception: If the squeak is not valid.
"""
check_squeak(squeak)
check_squeak(base_squeak)
def get_block_header(self, squeak: CSqueak) -> CBlockHeader:
def get_block_header(self, base_squeak: CBaseSqueak) -> CBlockHeader:
"""Checks if the embedded block hash in the squeak is valid for its
block height and return the associtated block header.
@ -128,8 +161,8 @@ class SqueakCore:
Exception: If the block hash is not valid.
"""
block_info = self.bitcoin_client.get_block_info_by_height(
squeak.nBlockHeight)
if squeak.hashBlock != block_info.block_hash:
base_squeak.nBlockHeight)
if base_squeak.hashBlock != block_info.block_hash:
raise Exception("Block hash incorrect.")
return block_info.block_header

View file

@ -42,5 +42,8 @@ class SqueakEntry(NamedTuple):
squeak_profile: Optional[SqueakProfile]
recipient_squeak_profile: Optional[SqueakProfile]
num_replies: int
num_resqueaks: int
liked_time_ms: Optional[int] = None
content: Optional[str] = None
resqueaked_hash: Optional[bytes] = None
resqueaked_squeak: Optional['SqueakEntry'] = None # type: ignore

View file

@ -24,7 +24,9 @@ from typing import Optional
from typing import Tuple
from squeak.core import CheckSqueak
from squeak.core import CResqueak
from squeak.core import CSqueak
from squeak.core import MakeResqueak
from squeak.core import MakeSqueakFromStr
from squeak.core.elliptic import payment_point_bytes_from_scalar_bytes
from squeak.core.keys import SqueakPrivateKey
@ -87,6 +89,37 @@ def make_squeak_with_block(
)
def make_resqueak_with_block(
private_key: SqueakPrivateKey,
resqueak_hash: bytes,
block_height: int,
block_hash: bytes,
replyto_hash: Optional[bytes] = None,
) -> CResqueak:
"""Create a new resqueak.
Args:
private_key: The private key to sign the squeak.
resqueak_hash: The hash of the squeak to resqueak.
block_height: The height of the latest block in the bitcoin blockchain.
block_hash: The hash of the latest block in the bitcoin blockchain.
replyto_hash: The hash of the squeak to which this one is replying.
recipient_public_key: The public key of the recipient of a private squeak.
Returns:
CResqueak: the resqueak that was created.
"""
timestamp = int(time.time())
return MakeResqueak(
private_key,
resqueak_hash,
block_height,
block_hash,
timestamp,
reply_to=replyto_hash,
)
def check_squeak(squeak: CSqueak) -> None:
"""Checks if the squeak is valid and has a valid signature.

View file

@ -0,0 +1,48 @@
# MIT License
#
# Copyright (c) 2020 Jonathan Zernik
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Add resqueak hash column in squeak table
Revision ID: b3d0395263c4
Revises: 0e3b79a31b58
Create Date: 2022-04-23 21:41:39.063229
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'b3d0395263c4'
down_revision = '0e3b79a31b58'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('squeak', schema=None) as batch_op:
batch_op.add_column(
sa.Column('resqueak_hash', sa.LargeBinary(length=32), nullable=True))
def downgrade():
with op.batch_alter_table('squeak', schema=None) as batch_op:
batch_op.drop_column('resqueak_hash')

View file

@ -82,6 +82,7 @@ class Models:
Column("block_time_s", Integer, nullable=False),
Column("liked_time_ms", SLBigInteger, default=None, nullable=True),
Column("content", String(280), nullable=True),
Column("resqueak_hash", LargeBinary(32), nullable=True),
)
self.profiles = Table(

View file

@ -28,12 +28,15 @@ from typing import Optional
import sqlalchemy
from bitcoin.core import CBlockHeader
from sqlalchemy import distinct
from sqlalchemy import func
from sqlalchemy import literal
from sqlalchemy import not_
from sqlalchemy import or_
from sqlalchemy.sql import select
from sqlalchemy.sql import tuple_
from squeak.core import CBaseSqueak
from squeak.core import CResqueak
from squeak.core import CSqueak
from squeak.core.keys import SqueakPrivateKey
from squeak.core.keys import SqueakPublicKey
@ -86,11 +89,16 @@ class SqueakDb:
# Create aliases for profiles
self.author_profiles = self.profiles.alias()
self.resqueaked_author_profiles = self.profiles.alias()
self.recipient_profiles = self.profiles.alias()
# Create aliases for squeaks
self.display_squeaks = self.squeaks.alias()
self.reply_squeaks = self.squeaks.alias()
self.resqueaked_squeaks = self.squeaks.alias()
self.resqueaked_reply_squeaks = self.squeaks.alias()
self.resqueaks = self.squeaks.alias()
self.resqueaked_resqueaks = self.squeaks.alias()
def init_with_retries(
self,
@ -242,7 +250,36 @@ class SqueakDb:
logger.debug("Failed to insert squeak.", exc_info=True)
return None
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
def insert_resqueak(self, resqueak: CResqueak, block_header: CBlockHeader) -> Optional[bytes]:
""" Insert a new resqueak.
Return the hash (bytes) of the inserted resqueak.
Return None if resqueak already exists.
"""
ins = self.squeaks.insert().values(
created_time_ms=self.timestamp_now_ms,
hash=get_hash(resqueak),
squeak=resqueak.serialize(),
reply_hash=(resqueak.hashReplySqk
if resqueak.is_reply
else None),
block_hash=resqueak.hashBlock,
block_height=resqueak.nBlockHeight,
time_s=resqueak.nTime,
author_public_key=resqueak.GetPubKey().to_bytes(),
resqueak_hash=resqueak.hashResqueakSqk,
block_time_s=block_header.nTime,
)
with self.get_connection() as connection:
try:
res = connection.execute(ins)
squeak_hash = res.inserted_primary_key[0]
return squeak_hash
except sqlalchemy.exc.IntegrityError:
logger.debug("Failed to insert squeak.", exc_info=True)
return None
def get_squeak(self, squeak_hash: bytes) -> Optional[CBaseSqueak]:
""" Get a squeak. """
s = select([self.squeaks]).where(
self.squeaks.c.hash == squeak_hash)
@ -273,8 +310,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
self.squeaks
@ -290,11 +336,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
)
.where(self.squeaks.c.hash == squeak_hash)
)
@ -334,8 +402,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
# self.squeaks.outerjoin(
@ -355,11 +432,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
)
.where(self.profile_is_following(self.author_profiles))
.where(
@ -407,8 +506,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
# self.squeaks.outerjoin(
@ -428,11 +536,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
)
.where(
self.squeak_is_liked,
@ -485,8 +615,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
# self.squeaks.outerjoin(
@ -506,11 +645,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
)
.where(self.squeaks.c.author_public_key == public_key.to_bytes())
.where(
@ -564,8 +725,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
# self.squeaks.outerjoin(
@ -585,11 +755,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
)
.where(self.squeaks.c.content.ilike(f'%{search_text}%'))
.where(
@ -645,8 +837,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
self.squeaks.join(
@ -668,11 +869,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
ancestors.c.depth,
)
.order_by(
@ -731,8 +954,17 @@ class SqueakDb:
select([
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
func.count(self.reply_squeaks.c.hash).label("num_replies"),
self.resqueaked_squeaks,
func.count(distinct(self.reply_squeaks.c.hash)
).label("num_replies"),
func.count(distinct(self.resqueaked_reply_squeaks.c.hash)).label(
"num_resqueak_replies"),
func.count(distinct(self.resqueaks.c.hash)
).label("num_resqueaks"),
func.count(distinct(self.resqueaked_resqueaks.c.hash)).label(
"num_resqueak_resqueaks"),
])
.select_from(
# self.squeaks.outerjoin(
@ -752,11 +984,33 @@ class SqueakDb:
self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaks,
self.resqueaks.c.resqueak_hash == self.squeaks.c.hash,
)
.outerjoin(
self.resqueaked_squeaks,
self.resqueaked_squeaks.c.hash == self.squeaks.c.resqueak_hash,
)
.outerjoin(
self.resqueaked_author_profiles,
self.resqueaked_author_profiles.c.public_key == self.resqueaked_squeaks.c.author_public_key,
)
.outerjoin(
self.resqueaked_reply_squeaks,
self.resqueaked_reply_squeaks.c.reply_hash == self.resqueaked_squeaks.c.hash,
)
.outerjoin(
self.resqueaked_resqueaks,
self.resqueaked_resqueaks.c.resqueak_hash == self.resqueaked_squeaks.c.hash,
)
)
.group_by(
self.squeaks,
self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles,
self.resqueaked_squeaks,
)
.where(self.squeaks.c.reply_hash == squeak_hash)
.where(
@ -2044,8 +2298,11 @@ class SqueakDb:
with self.get_connection() as connection:
connection.execute(delete_twitter_account_stmt)
def _parse_squeak(self, row) -> CSqueak:
return CSqueak.deserialize(row["squeak"])
def _parse_squeak(self, row) -> CBaseSqueak:
if row["resqueak_hash"]:
return CResqueak.deserialize(row["squeak"])
else:
return CSqueak.deserialize(row["squeak"])
# def _parse_squeak_entry(self, row, author_profiles_table=None, recipient_profiles_table=None) -> SqueakEntry:
def _parse_squeak_entry(self, row) -> SqueakEntry:
@ -2060,6 +2317,8 @@ class SqueakDb:
row, profiles_table=self.author_profiles)
recipient_profile = self._try_parse_squeak_profile(
row, profiles_table=self.recipient_profiles)
resqueaked_squeak = self._parse_resqueaked_squeak_entry(
row) if row[self.resqueaked_squeaks.c.author_public_key] else None
return SqueakEntry(
squeak_hash=(row["hash"]),
serialized_squeak=(row["squeak"]),
@ -2075,7 +2334,45 @@ class SqueakDb:
secret_key=(row["secret_key"]),
liked_time_ms=liked_time_ms,
num_replies=row["num_replies"],
num_resqueaks=row["num_resqueaks"],
content=row["content"],
resqueaked_hash=row["resqueak_hash"],
resqueaked_squeak=resqueaked_squeak,
squeak_profile=profile,
recipient_squeak_profile=recipient_profile,
)
def _parse_resqueaked_squeak_entry(self, row) -> SqueakEntry:
public_key_bytes = row[self.resqueaked_squeaks.c.author_public_key]
recipient_public_key_bytes = row[self.resqueaked_squeaks.c.recipient_public_key]
secret_key_column = row[self.resqueaked_squeaks.c.secret_key]
is_locked = bool(secret_key_column)
reply_to = (
row[self.resqueaked_squeaks.c.reply_hash]) if row[self.resqueaked_squeaks.c.reply_hash] else None
liked_time_ms = row[self.resqueaked_squeaks.c.liked_time_ms]
profile = self._try_parse_squeak_profile(
row, profiles_table=self.resqueaked_author_profiles)
recipient_profile = self._try_parse_squeak_profile(
row, profiles_table=self.recipient_profiles)
return SqueakEntry(
squeak_hash=(row[self.resqueaked_squeaks.c.hash]),
serialized_squeak=(row[self.resqueaked_squeaks.c.squeak]),
public_key=SqueakPublicKey.from_bytes(public_key_bytes),
recipient_public_key=SqueakPublicKey.from_bytes(
recipient_public_key_bytes) if recipient_public_key_bytes else None,
block_height=row[self.resqueaked_squeaks.c.block_height],
block_hash=(row[self.resqueaked_squeaks.c.block_hash]),
block_time=row[self.resqueaked_squeaks.c.block_time_s],
squeak_time=row[self.resqueaked_squeaks.c.time_s],
reply_to=reply_to,
is_unlocked=is_locked,
secret_key=(row[self.resqueaked_squeaks.c.secret_key]),
liked_time_ms=liked_time_ms,
# TODO: needs another join. row[self.resqueaked_squeaks.c.num_replies],
num_replies=row["num_resqueak_replies"],
num_resqueaks=row["num_resqueak_resqueaks"],
content=row[self.resqueaked_squeaks.c.content],
resqueaked_hash=row[self.resqueaked_squeaks.c.resqueak_hash],
squeak_profile=profile,
recipient_squeak_profile=recipient_profile,
)

View file

@ -84,6 +84,18 @@ class SqueakController:
recipient_profile_id,
)
def make_resqueak(
self,
profile_id: int,
resqueaked_hash: bytes,
replyto_hash: Optional[bytes],
) -> Optional[bytes]:
return self.squeak_store.make_resqueak(
profile_id,
resqueaked_hash,
replyto_hash,
)
def pay_offer(self, received_offer_id: int) -> int:
return self.squeak_store.pay_offer(received_offer_id)

View file

@ -25,8 +25,10 @@ from typing import Iterator
from typing import List
from typing import Optional
from squeak.core import CBaseSqueak
from squeak.core import CheckSqueak
from squeak.core import CheckSqueakSecretKey
from squeak.core import CResqueak
from squeak.core import CSqueak
from squeak.core.keys import SqueakPrivateKey
from squeak.core.keys import SqueakPublicKey
@ -119,32 +121,59 @@ class SqueakStore:
)
return inserted_squeak_hash
def save_squeak(self, squeak: CSqueak) -> Optional[bytes]:
def make_resqueak(
self,
profile_id: int,
resqueaked_hash: bytes,
replyto_hash: Optional[bytes],
) -> Optional[bytes]:
squeak_profile = self.get_squeak_profile(profile_id)
if squeak_profile is None:
raise Exception("Profile with id {} not found.".format(
profile_id,
))
resqueak = self.squeak_core.make_resqueak(
squeak_profile,
resqueaked_hash,
replyto_hash,
)
inserted_resqueak_hash = self.save_squeak(resqueak)
if inserted_resqueak_hash is None:
raise Exception("Failed to save resqueak.")
return inserted_resqueak_hash
def save_squeak(self, base_squeak: CBaseSqueak) -> Optional[bytes]:
# Check if the squeak is valid context free.
CheckSqueak(squeak)
CheckSqueak(base_squeak)
# Get the block header.
block_header = self.squeak_core.get_block_header(squeak)
block_header = self.squeak_core.get_block_header(base_squeak)
# Check if limit exceeded.
if self.squeak_db.get_number_of_squeaks() >= self.max_squeaks:
raise Exception("Exceeded max number of squeaks.")
# TODO: Check if limit per public key per block is exceeded.
if self.squeak_db.number_of_squeaks_with_public_key_with_block_height(
squeak.GetPubKey(),
squeak.nBlockHeight,
base_squeak.GetPubKey(),
base_squeak.nBlockHeight,
) >= self.max_squeaks_per_public_key_per_block:
raise Exception(
"Exceeded max number of squeaks per public key per block.")
# Insert the squeak in db.
inserted_squeak_hash = self.squeak_db.insert_squeak(
squeak,
block_header,
)
if isinstance(base_squeak, CSqueak):
inserted_squeak_hash = self.squeak_db.insert_squeak(
base_squeak,
block_header,
)
elif isinstance(base_squeak, CResqueak):
inserted_squeak_hash = self.squeak_db.insert_resqueak(
base_squeak,
block_header,
)
if inserted_squeak_hash is None:
return None
logger.info("Saved squeak: {}".format(
inserted_squeak_hash.hex(),
))
self.new_squeak_listener.handle_new_item(squeak)
self.new_squeak_listener.handle_new_item(base_squeak)
return inserted_squeak_hash
def save_secret_key(self, squeak_hash: bytes, secret_key: bytes):
@ -230,7 +259,11 @@ class SqueakStore:
return sent_payment_id
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]:
return self.squeak_db.get_squeak(squeak_hash)
# TODO: remove this after squeak protocol struct stabilizes.
try:
return self.squeak_db.get_squeak(squeak_hash)
except Exception:
return None
def get_squeak_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
return self.squeak_db.get_squeak_secret_key(squeak_hash)

View file

@ -59,10 +59,13 @@ def create_app(handler):
@app.route('/squeak/<hash>')
def squeak(hash):
try:
squeak_bytes = handler.handle_get_squeak_bytes(hash)
squeak_type, squeak_bytes = handler.handle_get_squeak_bytes(hash)
except NotFoundError:
return "Not found", 404
return squeak_bytes
return jsonify({
'squeak_type': squeak_type,
'squeak_bytes': squeak_bytes.hex(),
})
@app.route('/secretkey/<hash>')
def secret_key(hash):

View file

@ -22,6 +22,7 @@
import logging
from typing import List
from typing import Optional
from typing import Tuple
from squeak.core.keys import SqueakPublicKey
@ -54,12 +55,17 @@ class SqueakPeerServerHandler(object):
self.node_settings = node_settings
self.config = config
def handle_get_squeak_bytes(self, squeak_hash_str) -> bytes:
def handle_get_squeak_bytes(self, squeak_hash_str) -> Tuple[str, bytes]:
"""Return a tuple with the squeak type and the squeak bytes.
Returns: (str, bytes)
"""
squeak_hash = bytes.fromhex(squeak_hash_str)
squeak = self.squeak_controller.get_squeak(squeak_hash)
if not squeak:
raise NotFoundError()
return squeak.serialize()
squeak_type = 'resqueak' if squeak.is_resqueak else 'squeak'
return squeak_type, squeak.serialize()
def handle_get_secret_key(self, squeak_hash_str) -> bytes:
squeak_hash = bytes.fromhex(squeak_hash_str)

View file

@ -42,6 +42,7 @@ from squeaknode.core.sent_payment_summary import SentPaymentSummary
from squeaknode.core.squeak_entry import SqueakEntry
from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeaks import get_hash
from squeaknode.core.squeaks import make_resqueak_with_block
from squeaknode.core.squeaks import make_squeak_with_block
from squeaknode.core.user_config import UserConfig
from tests.utils import gen_contact_profile
@ -216,6 +217,21 @@ def private_squeak(private_squeak_and_secret_key):
yield squeak
@pytest.fixture
def resqueak(private_key, squeak_hash, block_info):
yield make_resqueak_with_block(
private_key,
squeak_hash,
block_info.block_height,
block_info.block_hash,
)
@pytest.fixture
def resqueak_hash(resqueak):
yield get_hash(resqueak)
@pytest.fixture
def peer_address():
yield PeerAddress(
@ -308,6 +324,7 @@ def squeak_entry_locked(
recipient_squeak_profile=recipient_contact_profile,
liked_time_ms=None,
num_replies=0,
num_resqueaks=0,
content=None,
)

View file

@ -344,6 +344,21 @@ def test_make_private_squeak(
assert author_decrypted_content == squeak_content
def test_make_resqueak(
squeak_core,
signing_profile,
squeak_hash,
block_header
):
created_resqueak = squeak_core.make_resqueak(
signing_profile,
squeak_hash,
)
assert created_resqueak.hashResqueakSqk == squeak_hash
squeak_core.check_squeak(created_resqueak)
def test_get_block_header(
squeak_core,
squeak,

View file

@ -61,6 +61,11 @@ def inserted_reply_squeak_hash(squeak_db, reply_squeak, block_header):
yield squeak_db.insert_squeak(reply_squeak, block_header)
@pytest.fixture
def inserted_resqueak_hash(squeak_db, resqueak, block_header):
yield squeak_db.insert_resqueak(resqueak, block_header)
@pytest.fixture
def unlocked_squeak_hash(squeak_db, squeak, inserted_squeak_hash, secret_key, squeak_content):
squeak_db.set_squeak_secret_key(
@ -488,6 +493,12 @@ def test_get_missing_squeak(squeak_db, squeak, squeak_hash):
assert retrieved_squeak is None
def test_get_resqueak(squeak_db, resqueak, inserted_resqueak_hash):
retrieved_resqueak = squeak_db.get_squeak(inserted_resqueak_hash)
assert retrieved_resqueak == resqueak
def test_get_squeak_entry(
squeak_db,
squeak,
@ -584,6 +595,34 @@ def test_get_secret_key_missing_squeak(squeak_db, squeak, squeak_hash):
assert retrieved_secret_key is None
def test_get_resqueak_entry(
squeak_db,
resqueak,
squeak,
block_header,
public_key,
signing_profile,
inserted_resqueak_hash,
inserted_squeak_hash,
inserted_signing_profile_id,
):
retrieved_resqueak_entry = squeak_db.get_squeak_entry(
inserted_resqueak_hash)
retrieved_squeak_entry = squeak_db.get_squeak_entry(inserted_squeak_hash)
assert retrieved_resqueak_entry.squeak_hash == inserted_resqueak_hash
assert retrieved_resqueak_entry.public_key == public_key
assert retrieved_resqueak_entry.content is None
assert retrieved_resqueak_entry.block_time == block_header.nTime
assert retrieved_resqueak_entry.squeak_profile._replace(
profile_id=None) == signing_profile
assert retrieved_resqueak_entry.resqueaked_hash == inserted_squeak_hash
assert retrieved_resqueak_entry.resqueaked_squeak == retrieved_squeak_entry
assert retrieved_resqueak_entry.resqueaked_squeak.public_key == public_key
assert retrieved_resqueak_entry.num_resqueaks == 0
assert retrieved_resqueak_entry.resqueaked_squeak.num_resqueaks == 1
def test_get_timeline_squeak_entries(squeak_db, followed_squeak_hashes):
timeline_squeak_entries = squeak_db.get_timeline_squeak_entries(
limit=2,