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, GetSigningProfilesReply,
GetContactProfilesReply, GetContactProfilesReply,
MakeSqueakReply, MakeSqueakReply,
MakeResqueakRequest,
MakeResqueakReply,
GetSqueakDisplayReply, GetSqueakDisplayReply,
GetAncestorSqueakDisplaysReply, GetAncestorSqueakDisplaysReply,
GetReplySqueakDisplaysReply, 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 = () => { export const getSigningProfiles = () => {
console.log('Calling getSigningProfiles'); console.log('Calling getSigningProfiles');
const request = new GetSigningProfilesRequest(); const request = new GetSigningProfilesRequest();

View file

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

View file

@ -13,6 +13,7 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
import SqueakCard from '../../components/SqueakCard' import SqueakCard from '../../components/SqueakCard'
import Loader from '../../components/Loader' import Loader from '../../components/Loader'
import MakeSqueak from '../squeaks/MakeSqueak' import MakeSqueak from '../squeaks/MakeSqueak'
import MakeResqueak from '../squeaks/MakeResqueak'
import DeleteSqueak from '../../features/squeaks/DeleteSqueak' import DeleteSqueak from '../../features/squeaks/DeleteSqueak'
import BuySqueak from '../squeaks/BuySqueak' import BuySqueak from '../squeaks/BuySqueak'
@ -66,6 +67,7 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
const dispatch = useDispatch(); const dispatch = useDispatch();
const [replyModalOpen, setModalOpen] = useState(false) const [replyModalOpen, setModalOpen] = useState(false)
const [resqueakModalOpen, setResqueakModalOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false) const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [buyModalOpen, setBuyModalOpen] = useState(false) const [buyModalOpen, setBuyModalOpen] = useState(false)
const [spendingModalOpen, setSpendingModalOpen] = useState(false) const [spendingModalOpen, setSpendingModalOpen] = useState(false)
@ -92,6 +94,15 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
setModalOpen(!replyModalOpen) 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) => { const toggleDeleteModal = (e, type) => {
if(e){ e.stopPropagation() } if(e){ e.stopPropagation() }
// if(param === 'edit'){setSaved(false)} // if(param === 'edit'){setSaved(false)}
@ -170,6 +181,32 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
// const squeakOffers = []; // 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 squeak = currentSqueak;
const author = currentSqueak && currentSqueak.getAuthor(); const author = currentSqueak && currentSqueak.getAuthor();
@ -199,19 +236,10 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
</div> </div>
</div> </div>
{squeak.getContentStr() ?
<div className="squeak-content"> {squeak.getIsResqueak() ?
{squeak.getContentStr()} getResqueakContent(squeak) :
</div> : getRegularSqueakContent(squeak)
<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>
} }
@ -238,10 +266,11 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
<div className="card-icon reply-icon"> <ICON_REPLY /> </div> <div className="card-icon reply-icon"> <ICON_REPLY /> </div>
{squeak.getNumReplies()} {squeak.getNumReplies()}
</div> </div>
<div onClick={()=>resqueak(squeak.getSqueakHash())} className="squeak-int-icon"> <div onClick={()=>toggleResqueakModal()} className="squeak-int-icon">
<div className="card-icon resqueak-icon"> <div className="card-icon resqueak-icon">
<ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/> <ICON_RETWEET styles={false ? {stroke: 'rgb(23, 191, 99)'} : {fill:'rgb(101, 119, 134)'}}/>
</div> </div>
{squeak.getNumResqueaks()}
</div> </div>
<div onClick={()=>{ <div onClick={()=>{
squeak.getLikedTimeMs() ? squeak.getLikedTimeMs() ?
@ -341,88 +370,106 @@ import { ICON_ARROWBACK, ICON_HEART, ICON_REPLY, ICON_RETWEET, ICON_HEARTFULL,
</div>:null} </div>:null}
{squeak ? {squeak ?
<div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit"> <div onClick={()=>toggleResqueakModal()} style={{display: resqueakModalOpen ? 'block' : 'none'}} className="modal-edit">
{deleteModalOpen ? {resqueakModalOpen ?
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content"> <div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header"> <div className="modal-header">
<div className="modal-closeIcon"> <div className="modal-closeIcon">
<div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap"> <div onClick={()=>toggleResqueakModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE /> <ICON_CLOSE />
</div> </div>
</div> </div>
<p className="modal-title">Delete Squeak</p> <p className="modal-title">Resqueak</p>
</div> </div>
<div style={{marginTop:'5px'}} className="modal-body"> <div style={{marginTop:'5px'}} className="modal-body">
<DeleteSqueak squeakHash={squeak.getSqueakHash()} submittedCallback={toggleDeleteModal} /> <MakeResqueak resqueakedSqueak={squeak} submittedCallback={toggleResqueakModal} />
</div> </div>
</div> : null} </div> : null}
</div>:null} </div>:null}
{squeak ? {squeak ?
<div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit"> <div onClick={()=>toggleDeleteModal()} style={{display: deleteModalOpen ? 'block' : 'none'}} className="modal-edit">
{buyModalOpen ? {deleteModalOpen ?
<div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content"> <div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div className="modal-header"> <div className="modal-header">
<div className="modal-closeIcon"> <div className="modal-closeIcon">
<div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap"> <div onClick={()=>toggleDeleteModal()} className="modal-closeIcon-wrap">
<ICON_CLOSE /> <ICON_CLOSE />
</div> </div>
</div> </div>
<p className="modal-title">Buy Squeak</p> <p className="modal-title">Delete Squeak</p>
</div> </div>
<div style={{marginTop:'5px'}} className="modal-body"> <div style={{marginTop:'5px'}} className="modal-body">
<BuySqueak squeak={squeak} submittedCallback={toggleBuyModal} /> <DeleteSqueak squeakHash={squeak.getSqueakHash()} submittedCallback={toggleDeleteModal} />
</div> </div>
</div> : null} </div> : null}
</div>:null} </div>:null}
{squeak ?
{/* Modal for sent payments and received payments */} <div onClick={()=>toggleBuyModal()} style={{display: buyModalOpen ? 'block' : 'none'}} className="modal-edit">
{squeak && {buyModalOpen ?
<div onClick={()=>toggleSpendingModal()} style={{display: spendingModalOpen ? 'block' : 'none'}} className="modal-edit"> <div style={{minHeight: '379px', height: 'initial'}} onClick={(e)=>handleModalClick(e)} className="modal-content">
<div onClick={(e)=>handleModalClick(e)} className="modal-content"> <div className="modal-header">
<div className="modal-header no-b-border"> <div className="modal-closeIcon">
<div className="modal-closeIcon"> <div onClick={()=>toggleBuyModal()} className="modal-closeIcon-wrap">
<div onClick={()=>toggleSpendingModal()} className="modal-closeIcon-wrap"> <ICON_CLOSE />
<ICON_CLOSE /> </div>
</div> </div>
<p className="modal-title">Buy Squeak</p>
</div> </div>
<p className="modal-title">{null}</p> <div style={{marginTop:'5px'}} className="modal-body">
</div> <BuySqueak squeak={squeak} submittedCallback={toggleBuyModal} />
<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>
<div className="modal-scroll"> </div> : null}
{tab === 'Sent Payments' ? </div>:null}
<>
<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
{/* 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>}
</div>
</div>}
</> </>
} }
export default withRouter(Squeak) export default withRouter(Squeak)

View file

@ -15,6 +15,7 @@ import {
getSearchSqueaks, getSearchSqueaks,
getProfileSqueaks, getProfileSqueaks,
makeSqueak, makeSqueak,
makeResqueak,
deleteSqueak, deleteSqueak,
getSqueakOffers, getSqueakOffers,
buySqueak, 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( export const fetchSqueakOffers = createAsyncThunk(
'squeaks/fetchSqueakOffers', 'squeaks/fetchSqueakOffers',
async (squeakHash) => { async (squeakHash) => {
@ -202,10 +220,32 @@ const updatedSqueakInArray = (squeakArr, newSqueak) => {
if (currentIndex != -1) { if (currentIndex != -1) {
squeakArr[currentIndex] = newSqueak; 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) => { const removeSqueakInArray = (squeakArr, squeakHash) => {
return squeakArr.filter(squeak => squeak.getSqueakHash() !== 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'; state.makeSqueakStatus = 'idle';
console.log('Go to new squeak'); 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) => { .addCase(fetchSqueakOffers.pending, (state, action) => {
state.squeakOffers = []; state.squeakOffers = [];
state.squeakOffersStatus = 'loading' state.squeakOffersStatus = 'loading'

View file

@ -128,6 +128,10 @@ service SqueakAdmin {
*/ */
rpc MakeSqueak (MakeSqueakRequest) returns (MakeSqueakReply) {} rpc MakeSqueak (MakeSqueakRequest) returns (MakeSqueakReply) {}
/** sqkadmin: `makeresqueak`
*/
rpc MakeResqueak (MakeResqueakRequest) returns (MakeResqueakReply) {}
/** sqkadmin: `getsqueakdisplay` /** sqkadmin: `getsqueakdisplay`
*/ */
rpc GetSqueakDisplay (GetSqueakDisplayRequest) returns (GetSqueakDisplayReply) {} rpc GetSqueakDisplay (GetSqueakDisplayRequest) returns (GetSqueakDisplayReply) {}
@ -588,6 +592,22 @@ message MakeSqueakReply {
string squeak_hash = 1; 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 { message GetSqueakDisplayRequest {
/// Hash of the squeak. /// Hash of the squeak.
string squeak_hash = 1; string squeak_hash = 1;
@ -658,6 +678,18 @@ message SqueakDisplayEntry {
/// Number of replies /// Number of replies
int32 num_replies = 20; 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 { message GetTimelineSqueakDisplaysRequest {

View file

@ -3,4 +3,4 @@ grpcio==1.39.0
grpcio-tools==1.39.0 grpcio-tools==1.39.0
importlib_resources==1.4.0 importlib_resources==1.4.0
pytest==6.2.5 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 pyzmq==22.3.0
requests[socks]==2.26.0 requests[socks]==2.26.0
SQLAlchemy==1.4.25 SQLAlchemy==1.4.25
squeaklib==0.12.0 squeaklib==0.13.1
typed-config==0.2.5 typed-config==0.2.5
Werkzeug==2.0.3 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), if squeak_entry.squeak_profile else None),
liked_time_ms=squeak_entry.liked_time_ms, # type: ignore liked_time_ms=squeak_entry.liked_time_ms, # type: ignore
num_replies=squeak_entry.num_replies, num_replies=squeak_entry.num_replies,
num_resqueaks=squeak_entry.num_resqueaks,
is_private=(squeak_entry.recipient_public_key is not None), is_private=(squeak_entry.recipient_public_key is not None),
recipient_pubkey=(squeak_entry.recipient_public_key.to_bytes( recipient_pubkey=(squeak_entry.recipient_public_key.to_bytes(
).hex() if squeak_entry.recipient_public_key else None), ).hex() if squeak_entry.recipient_public_key else None),
is_recipient_known=(squeak_entry.recipient_squeak_profile is not None), is_recipient_known=(squeak_entry.recipient_squeak_profile is not None),
recipient=(squeak_profile_to_message(squeak_entry.recipient_squeak_profile) recipient=(squeak_profile_to_message(squeak_entry.recipient_squeak_profile)
if squeak_entry.recipient_squeak_profile else None), 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 recipient_squeak_profile=None, # TODO: message to squeak profile
liked_time_ms=(msg.liked_time_ms if msg.liked_time_ms > 0 else None), liked_time_ms=(msg.liked_time_ms if msg.liked_time_ms > 0 else None),
num_replies=0, num_replies=0,
num_resqueaks=0,
content=(msg.content_str if len(msg.content_str) > 0 else None), 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, 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): def handle_get_squeak_display_entry(self, request):
squeak_hash_str = request.squeak_hash squeak_hash_str = request.squeak_hash
squeak_hash = bytes.fromhex(squeak_hash_str) 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): def MakeSqueak(self, request, context):
return self.handler.handle_make_squeak(request) return self.handler.handle_make_squeak(request)
def MakeResqueak(self, request, context):
return self.handler.handle_make_resqueak(request)
def GetSqueakDisplay(self, request, context): def GetSqueakDisplay(self, request, context):
return self.handler.handle_get_squeak_display_entry(request) return self.handler.handle_get_squeak_display_entry(request)

View file

@ -313,6 +313,12 @@ def create_app(handler, username, password):
def makesqueakrequest(msg): def makesqueakrequest(msg):
return handler.handle_make_squeak(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"]) @app.route("/getsqueakdisplay", methods=["POST"])
@login_required @login_required
@protobuf_serialized(squeak_admin_pb2.GetSqueakDisplayRequest()) @protobuf_serialized(squeak_admin_pb2.GetSqueakDisplayRequest())

View file

@ -1,7 +1,7 @@
{ {
"files": { "files": {
"main.css": "/static/css/main.206389cd.css", "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/css/306.7f7715a0.chunk.css": "/static/css/306.7f7715a0.chunk.css",
"static/js/306.90b2a66c.chunk.js": "/static/js/306.90b2a66c.chunk.js", "static/js/306.90b2a66c.chunk.js": "/static/js/306.90b2a66c.chunk.js",
"static/css/900.b66ac5fb.chunk.css": "/static/css/900.b66ac5fb.chunk.css", "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", "static/media/icon.svg": "/static/media/icon.982210ec9275956e4d3cc77fa90167fd.svg",
"index.html": "/index.html", "index.html": "/index.html",
"main.206389cd.css.map": "/static/css/main.206389cd.css.map", "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.7f7715a0.chunk.css.map": "/static/css/306.7f7715a0.chunk.css.map",
"306.90b2a66c.chunk.js.map": "/static/js/306.90b2a66c.chunk.js.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", "900.b66ac5fb.chunk.css.map": "/static/css/900.b66ac5fb.chunk.css.map",
@ -21,6 +21,6 @@
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.206389cd.css", "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 from typing import Optional
import requests import requests
from squeak.core import CResqueak
from squeak.core import CSqueak from squeak.core import CSqueak
from squeak.core.keys import SqueakPublicKey from squeak.core.keys import SqueakPublicKey
@ -97,8 +98,19 @@ class PeerClient:
) )
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
return None return None
squeak_bytes = r.content logger.info(r)
return CSqueak.deserialize(squeak_bytes) 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]: def get_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
squeak_hash_str = squeak_hash.hex() squeak_hash_str = squeak_hash.hex()

View file

@ -26,6 +26,7 @@ from typing import Tuple
import grpc import grpc
from bitcoin.core import CBlockHeader from bitcoin.core import CBlockHeader
from squeak.core import CBaseSqueak
from squeak.core import CSqueak from squeak.core import CSqueak
from squeaknode.bitcoin.bitcoin_client import BitcoinClient 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_decrypted_content
from squeaknode.core.squeaks import get_hash from squeaknode.core.squeaks import get_hash
from squeaknode.core.squeaks import get_payment_point_of_secret_key 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.core.squeaks import make_squeak_with_block
from squeaknode.lightning.lightning_client import LightningClient from squeaknode.lightning.lightning_client import LightningClient
@ -71,8 +73,6 @@ class SqueakCore:
) -> Tuple[CSqueak, bytes]: ) -> Tuple[CSqueak, bytes]:
"""Create a new squeak. """Create a new squeak.
TODO: Include the block header in the result tuple.
Args: Args:
signing_profile: The profile of the author of the squeak. signing_profile: The profile of the author of the squeak.
content_str: The content of the squeak as a string. content_str: The content of the squeak as a string.
@ -100,7 +100,40 @@ class SqueakCore:
) )
return squeak, secret_key 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. """Checks if the squeak is valid and has a valid signature.
Args: Args:
@ -112,9 +145,9 @@ class SqueakCore:
Raises: Raises:
Exception: If the squeak is not valid. 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 """Checks if the embedded block hash in the squeak is valid for its
block height and return the associtated block header. block height and return the associtated block header.
@ -128,8 +161,8 @@ class SqueakCore:
Exception: If the block hash is not valid. Exception: If the block hash is not valid.
""" """
block_info = self.bitcoin_client.get_block_info_by_height( block_info = self.bitcoin_client.get_block_info_by_height(
squeak.nBlockHeight) base_squeak.nBlockHeight)
if squeak.hashBlock != block_info.block_hash: if base_squeak.hashBlock != block_info.block_hash:
raise Exception("Block hash incorrect.") raise Exception("Block hash incorrect.")
return block_info.block_header return block_info.block_header

View file

@ -42,5 +42,8 @@ class SqueakEntry(NamedTuple):
squeak_profile: Optional[SqueakProfile] squeak_profile: Optional[SqueakProfile]
recipient_squeak_profile: Optional[SqueakProfile] recipient_squeak_profile: Optional[SqueakProfile]
num_replies: int num_replies: int
num_resqueaks: int
liked_time_ms: Optional[int] = None liked_time_ms: Optional[int] = None
content: Optional[str] = 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 typing import Tuple
from squeak.core import CheckSqueak from squeak.core import CheckSqueak
from squeak.core import CResqueak
from squeak.core import CSqueak from squeak.core import CSqueak
from squeak.core import MakeResqueak
from squeak.core import MakeSqueakFromStr from squeak.core import MakeSqueakFromStr
from squeak.core.elliptic import payment_point_bytes_from_scalar_bytes from squeak.core.elliptic import payment_point_bytes_from_scalar_bytes
from squeak.core.keys import SqueakPrivateKey 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: def check_squeak(squeak: CSqueak) -> None:
"""Checks if the squeak is valid and has a valid signature. """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("block_time_s", Integer, nullable=False),
Column("liked_time_ms", SLBigInteger, default=None, nullable=True), Column("liked_time_ms", SLBigInteger, default=None, nullable=True),
Column("content", String(280), nullable=True), Column("content", String(280), nullable=True),
Column("resqueak_hash", LargeBinary(32), nullable=True),
) )
self.profiles = Table( self.profiles = Table(

View file

@ -28,12 +28,15 @@ from typing import Optional
import sqlalchemy import sqlalchemy
from bitcoin.core import CBlockHeader from bitcoin.core import CBlockHeader
from sqlalchemy import distinct
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy import literal from sqlalchemy import literal
from sqlalchemy import not_ from sqlalchemy import not_
from sqlalchemy import or_ from sqlalchemy import or_
from sqlalchemy.sql import select from sqlalchemy.sql import select
from sqlalchemy.sql import tuple_ from sqlalchemy.sql import tuple_
from squeak.core import CBaseSqueak
from squeak.core import CResqueak
from squeak.core import CSqueak from squeak.core import CSqueak
from squeak.core.keys import SqueakPrivateKey from squeak.core.keys import SqueakPrivateKey
from squeak.core.keys import SqueakPublicKey from squeak.core.keys import SqueakPublicKey
@ -86,11 +89,16 @@ class SqueakDb:
# Create aliases for profiles # Create aliases for profiles
self.author_profiles = self.profiles.alias() self.author_profiles = self.profiles.alias()
self.resqueaked_author_profiles = self.profiles.alias()
self.recipient_profiles = self.profiles.alias() self.recipient_profiles = self.profiles.alias()
# Create aliases for squeaks # Create aliases for squeaks
self.display_squeaks = self.squeaks.alias() self.display_squeaks = self.squeaks.alias()
self.reply_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( def init_with_retries(
self, self,
@ -242,7 +250,36 @@ class SqueakDb:
logger.debug("Failed to insert squeak.", exc_info=True) logger.debug("Failed to insert squeak.", exc_info=True)
return None 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. """ """ Get a squeak. """
s = select([self.squeaks]).where( s = select([self.squeaks]).where(
self.squeaks.c.hash == squeak_hash) self.squeaks.c.hash == squeak_hash)
@ -273,8 +310,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
self.squeaks self.squeaks
@ -290,11 +336,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
) )
.where(self.squeaks.c.hash == squeak_hash) .where(self.squeaks.c.hash == squeak_hash)
) )
@ -334,8 +402,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
# self.squeaks.outerjoin( # self.squeaks.outerjoin(
@ -355,11 +432,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
) )
.where(self.profile_is_following(self.author_profiles)) .where(self.profile_is_following(self.author_profiles))
.where( .where(
@ -407,8 +506,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
# self.squeaks.outerjoin( # self.squeaks.outerjoin(
@ -428,11 +536,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
) )
.where( .where(
self.squeak_is_liked, self.squeak_is_liked,
@ -485,8 +615,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
# self.squeaks.outerjoin( # self.squeaks.outerjoin(
@ -506,11 +645,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
) )
.where(self.squeaks.c.author_public_key == public_key.to_bytes()) .where(self.squeaks.c.author_public_key == public_key.to_bytes())
.where( .where(
@ -564,8 +725,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
# self.squeaks.outerjoin( # self.squeaks.outerjoin(
@ -585,11 +755,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
) )
.where(self.squeaks.c.content.ilike(f'%{search_text}%')) .where(self.squeaks.c.content.ilike(f'%{search_text}%'))
.where( .where(
@ -645,8 +837,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
self.squeaks.join( self.squeaks.join(
@ -668,11 +869,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
ancestors.c.depth, ancestors.c.depth,
) )
.order_by( .order_by(
@ -731,8 +954,17 @@ class SqueakDb:
select([ select([
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_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( .select_from(
# self.squeaks.outerjoin( # self.squeaks.outerjoin(
@ -752,11 +984,33 @@ class SqueakDb:
self.reply_squeaks, self.reply_squeaks,
self.reply_squeaks.c.reply_hash == self.squeaks.c.hash, 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( .group_by(
self.squeaks, self.squeaks,
self.author_profiles, self.author_profiles,
self.resqueaked_author_profiles,
self.recipient_profiles, self.recipient_profiles,
self.resqueaked_squeaks,
) )
.where(self.squeaks.c.reply_hash == squeak_hash) .where(self.squeaks.c.reply_hash == squeak_hash)
.where( .where(
@ -2044,8 +2298,11 @@ class SqueakDb:
with self.get_connection() as connection: with self.get_connection() as connection:
connection.execute(delete_twitter_account_stmt) connection.execute(delete_twitter_account_stmt)
def _parse_squeak(self, row) -> CSqueak: def _parse_squeak(self, row) -> CBaseSqueak:
return CSqueak.deserialize(row["squeak"]) 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, author_profiles_table=None, recipient_profiles_table=None) -> SqueakEntry:
def _parse_squeak_entry(self, row) -> SqueakEntry: def _parse_squeak_entry(self, row) -> SqueakEntry:
@ -2060,6 +2317,8 @@ class SqueakDb:
row, profiles_table=self.author_profiles) row, profiles_table=self.author_profiles)
recipient_profile = self._try_parse_squeak_profile( recipient_profile = self._try_parse_squeak_profile(
row, profiles_table=self.recipient_profiles) 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( return SqueakEntry(
squeak_hash=(row["hash"]), squeak_hash=(row["hash"]),
serialized_squeak=(row["squeak"]), serialized_squeak=(row["squeak"]),
@ -2075,7 +2334,45 @@ class SqueakDb:
secret_key=(row["secret_key"]), secret_key=(row["secret_key"]),
liked_time_ms=liked_time_ms, liked_time_ms=liked_time_ms,
num_replies=row["num_replies"], num_replies=row["num_replies"],
num_resqueaks=row["num_resqueaks"],
content=row["content"], 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, squeak_profile=profile,
recipient_squeak_profile=recipient_profile, recipient_squeak_profile=recipient_profile,
) )

View file

@ -84,6 +84,18 @@ class SqueakController:
recipient_profile_id, 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: def pay_offer(self, received_offer_id: int) -> int:
return self.squeak_store.pay_offer(received_offer_id) 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 List
from typing import Optional from typing import Optional
from squeak.core import CBaseSqueak
from squeak.core import CheckSqueak from squeak.core import CheckSqueak
from squeak.core import CheckSqueakSecretKey from squeak.core import CheckSqueakSecretKey
from squeak.core import CResqueak
from squeak.core import CSqueak from squeak.core import CSqueak
from squeak.core.keys import SqueakPrivateKey from squeak.core.keys import SqueakPrivateKey
from squeak.core.keys import SqueakPublicKey from squeak.core.keys import SqueakPublicKey
@ -119,32 +121,59 @@ class SqueakStore:
) )
return inserted_squeak_hash 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. # Check if the squeak is valid context free.
CheckSqueak(squeak) CheckSqueak(base_squeak)
# Get the block header. # 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. # Check if limit exceeded.
if self.squeak_db.get_number_of_squeaks() >= self.max_squeaks: if self.squeak_db.get_number_of_squeaks() >= self.max_squeaks:
raise Exception("Exceeded max number of squeaks.") raise Exception("Exceeded max number of squeaks.")
# TODO: Check if limit per public key per block is exceeded. # TODO: Check if limit per public key per block is exceeded.
if self.squeak_db.number_of_squeaks_with_public_key_with_block_height( if self.squeak_db.number_of_squeaks_with_public_key_with_block_height(
squeak.GetPubKey(), base_squeak.GetPubKey(),
squeak.nBlockHeight, base_squeak.nBlockHeight,
) >= self.max_squeaks_per_public_key_per_block: ) >= self.max_squeaks_per_public_key_per_block:
raise Exception( raise Exception(
"Exceeded max number of squeaks per public key per block.") "Exceeded max number of squeaks per public key per block.")
# Insert the squeak in db. # Insert the squeak in db.
inserted_squeak_hash = self.squeak_db.insert_squeak( if isinstance(base_squeak, CSqueak):
squeak, inserted_squeak_hash = self.squeak_db.insert_squeak(
block_header, 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: if inserted_squeak_hash is None:
return None return None
logger.info("Saved squeak: {}".format( logger.info("Saved squeak: {}".format(
inserted_squeak_hash.hex(), 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 return inserted_squeak_hash
def save_secret_key(self, squeak_hash: bytes, secret_key: bytes): def save_secret_key(self, squeak_hash: bytes, secret_key: bytes):
@ -230,7 +259,11 @@ class SqueakStore:
return sent_payment_id return sent_payment_id
def get_squeak(self, squeak_hash: bytes) -> Optional[CSqueak]: 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]: def get_squeak_secret_key(self, squeak_hash: bytes) -> Optional[bytes]:
return self.squeak_db.get_squeak_secret_key(squeak_hash) return self.squeak_db.get_squeak_secret_key(squeak_hash)

View file

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

View file

@ -22,6 +22,7 @@
import logging import logging
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple
from squeak.core.keys import SqueakPublicKey from squeak.core.keys import SqueakPublicKey
@ -54,12 +55,17 @@ class SqueakPeerServerHandler(object):
self.node_settings = node_settings self.node_settings = node_settings
self.config = config 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_hash = bytes.fromhex(squeak_hash_str)
squeak = self.squeak_controller.get_squeak(squeak_hash) squeak = self.squeak_controller.get_squeak(squeak_hash)
if not squeak: if not squeak:
raise NotFoundError() 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: def handle_get_secret_key(self, squeak_hash_str) -> bytes:
squeak_hash = bytes.fromhex(squeak_hash_str) 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_entry import SqueakEntry
from squeaknode.core.squeak_peer import SqueakPeer from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeaks import get_hash 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.squeaks import make_squeak_with_block
from squeaknode.core.user_config import UserConfig from squeaknode.core.user_config import UserConfig
from tests.utils import gen_contact_profile from tests.utils import gen_contact_profile
@ -216,6 +217,21 @@ def private_squeak(private_squeak_and_secret_key):
yield squeak 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 @pytest.fixture
def peer_address(): def peer_address():
yield PeerAddress( yield PeerAddress(
@ -308,6 +324,7 @@ def squeak_entry_locked(
recipient_squeak_profile=recipient_contact_profile, recipient_squeak_profile=recipient_contact_profile,
liked_time_ms=None, liked_time_ms=None,
num_replies=0, num_replies=0,
num_resqueaks=0,
content=None, content=None,
) )

View file

@ -344,6 +344,21 @@ def test_make_private_squeak(
assert author_decrypted_content == squeak_content 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( def test_get_block_header(
squeak_core, squeak_core,
squeak, 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) 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 @pytest.fixture
def unlocked_squeak_hash(squeak_db, squeak, inserted_squeak_hash, secret_key, squeak_content): def unlocked_squeak_hash(squeak_db, squeak, inserted_squeak_hash, secret_key, squeak_content):
squeak_db.set_squeak_secret_key( 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 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( def test_get_squeak_entry(
squeak_db, squeak_db,
squeak, squeak,
@ -584,6 +595,34 @@ def test_get_secret_key_missing_squeak(squeak_db, squeak, squeak_hash):
assert retrieved_secret_key is None 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): def test_get_timeline_squeak_entries(squeak_db, followed_squeak_hashes):
timeline_squeak_entries = squeak_db.get_timeline_squeak_entries( timeline_squeak_entries = squeak_db.get_timeline_squeak_entries(
limit=2, limit=2,