Add twitter forwarder (#1755)

* Add twitter stream class

* Got twitter forwarder subscription working

* Add config table in db with twitter bearer token

* Only run forward tweets task when twitter bearer token and twitter handles are not null

* Add twitter page to frontend and add button to set bearer token

* Reload bearer token in ui after setting value

* Add twitter accounts table to db

* Got get twitter accounts request working

* Display twitter account list items in twitter page

* Show twitter accounts in twitter page

* Got create squeak from tweet working

* Update twitter stream upon twitter accounts changes

* Got terminate twitter stream connection working

* Update frontend build

* Remove print statements from itest

* Remove main function from twitter stream module

* Make tweet forwarder worker run as daemon thread

* Use custom config for subscribe tweets retry interval
This commit is contained in:
Jonathan Zernik 2021-11-04 19:10:00 -07:00 committed by GitHub
parent 348dd1164c
commit 6c40891ad8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 2336 additions and 19696 deletions

19722
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,170 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
TextField,
DialogActions,
Button,
FormControl,
InputLabel,
Select,
CircularProgress,
MenuItem,
} from '@material-ui/core';
import { useHistory } from 'react-router-dom';
// styles
import useStyles from './styles';
import {
addTwitterAccountRequest,
getSigningProfilesRequest,
} from '../../squeakclient/requests';
import {
goToProfilePage,
} from '../../navigation/navigation';
export default function AddTwitterAccountDialog({
open,
handleClose,
reloadAccountsFn,
...props
}) {
const classes = useStyles();
const history = useHistory();
const [twitterHandle, setTwitterHandle] = useState('');
const [profileId, setProfileId] = useState(-1);
const [signingProfiles, setSigningProfiles] = useState([]);
const resetFields = () => {
setTwitterHandle('');
setProfileId(-1);
};
const handleChangeTwitterHandle = (event) => {
setTwitterHandle(event.target.value);
};
const handleChangeProfileId = (event) => {
setProfileId(event.target.value);
};
const handleResponse = (response) => {
reloadAccountsFn();
};
const handleErr = (err) => {
alert(`Error adding twitter account: ${err}`);
};
const addTwitterAccount = (twitterHandle, profileId) => {
addTwitterAccountRequest(twitterHandle, profileId, handleResponse, handleErr);
};
const loadSigningProfiles = () => {
getSigningProfilesRequest(setSigningProfiles);
};
function handleSubmit(event) {
event.preventDefault();
console.log('twitterHandle:', twitterHandle);
console.log('profileId:', profileId);
if (profileId === -1) {
alert('Signing profile must be selected.');
return;
}
if (!twitterHandle) {
alert('twitterHandle cannot be empty.');
return;
}
addTwitterAccount(twitterHandle, profileId);
handleClose();
}
function load(event) {
loadSigningProfiles();
}
function cancel(event) {
event.stopPropagation();
handleClose();
}
function ignore(event) {
event.stopPropagation();
}
function AccountHandleInput() {
return (
<TextField
id="standard-textarea"
label="Twitter Handle"
required
autoFocus
value={twitterHandle}
onChange={handleChangeTwitterHandle}
fullWidth
inputProps={{ maxLength: 128 }}
/>
);
}
function SelectSigningProfile() {
return (
<FormControl className={classes.formControl} required style={{ minWidth: 120 }}>
<InputLabel id="demo-simple-select-label">Signing Profile</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={profileId}
onChange={handleChangeProfileId}
>
{signingProfiles.map((p) => <MenuItem key={p.getProfileId()} value={p.getProfileId()}>{p.getProfileName()}</MenuItem>)}
</Select>
</FormControl>
);
}
function CancelButton() {
return (
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Cancel
</Button>
);
}
function CreateContactProfilButton() {
return (
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
Create Contact Profile
</Button>
);
}
return (
<Dialog open={open} onRendered={load} onEnter={resetFields} onClose={cancel} onClick={ignore} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Add Twitter Account</DialogTitle>
<form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off">
<DialogContent>
{AccountHandleInput()}
{SelectSigningProfile()}
</DialogContent>
<DialogActions>
{CancelButton()}
{CreateContactProfilButton()}
</DialogActions>
</form>
</Dialog>
);
}

View file

@ -0,0 +1,6 @@
{
"name": "AddTwitterAccountDialog",
"version": "0.0.0",
"private": true,
"main": "AddTwitterAccountDialog.js"
}

View file

@ -0,0 +1,43 @@
import { makeStyles } from '@material-ui/styles';
export default makeStyles((theme) => ({
widgetWrapper: {
display: 'flex',
minHeight: '100%',
},
widgetHeader: {
padding: theme.spacing(3),
paddingBottom: theme.spacing(1),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
widgetRoot: {
boxShadow: theme.customShadows.widget,
},
widgetBody: {
paddingBottom: theme.spacing(3),
paddingRight: theme.spacing(3),
paddingLeft: theme.spacing(3),
},
noPadding: {
padding: 0,
},
paper: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
},
moreButton: {
margin: -theme.spacing(1),
padding: 0,
width: 40,
height: 40,
color: theme.palette.text.hint,
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: 'rgba(255, 255, 255, 0.35)',
},
},
}));

View file

@ -0,0 +1,80 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
} from '@material-ui/core';
// styles
import useStyles from './styles';
import {
deleteTwitterAccountRequest,
} from '../../squeakclient/requests';
export default function DeleteTwitterAccountDialog({
open,
handleClose,
twitterAccount,
reloadAccountsFn,
...props
}) {
const classes = useStyles();
const deleteTwitterAccount = (twitterAccountId) => {
deleteTwitterAccountRequest(twitterAccountId, (response) => {
reloadAccountsFn();
});
};
function handleSubmit(event) {
event.preventDefault();
console.log('twitter account:', twitterAccount);
const twitterAccountId = twitterAccount.getTwitterAccountId();
console.log('twitterAccountId:', twitterAccountId);
deleteTwitterAccount(twitterAccountId);
handleClose();
}
function MakeCancelButton() {
return (
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Cancel
</Button>
);
}
function DeleteTwitterAccountButton() {
return (
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
Delete Twitter Account
</Button>
);
}
return (
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Delete Twitter Account Mapping</DialogTitle>
<form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off">
<DialogContent>
Are you sure you want to delete this twitter account mapping?
</DialogContent>
<DialogActions>
{MakeCancelButton()}
{DeleteTwitterAccountButton()}
</DialogActions>
</form>
</Dialog>
);
}

View file

@ -0,0 +1,6 @@
{
"name": "DeleteTwitterAccountDialog",
"version": "0.0.0",
"private": true,
"main": "DeleteTwitterAccountDialog.js"
}

View file

@ -0,0 +1,43 @@
import { makeStyles } from '@material-ui/styles';
export default makeStyles((theme) => ({
widgetWrapper: {
display: 'flex',
minHeight: '100%',
},
widgetHeader: {
padding: theme.spacing(3),
paddingBottom: theme.spacing(1),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
widgetRoot: {
boxShadow: theme.customShadows.widget,
},
widgetBody: {
paddingBottom: theme.spacing(3),
paddingRight: theme.spacing(3),
paddingLeft: theme.spacing(3),
},
noPadding: {
padding: 0,
},
paper: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
},
moreButton: {
margin: -theme.spacing(1),
padding: 0,
width: 40,
height: 40,
color: theme.palette.text.hint,
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: 'rgba(255, 255, 255, 0.35)',
},
},
}));

View file

@ -36,6 +36,7 @@ import Charts from '../../pages/charts';
import Peers from '../../pages/peers';
import Peer from '../../pages/peer';
import PeerAddress from '../../pages/peeraddress';
import Twitter from '../../pages/twitter';
// context
import { useLayoutState } from '../../context/LayoutContext';
@ -79,6 +80,7 @@ function Layout(props) {
<Route path="/app/peer/:id" component={Peer} />
<Route path="/app/peeraddress/:network/:host/:port" component={PeerAddress} />
<Route path="/app/notifications" component={Notifications} />
<Route path="/app/twitter" component={Twitter} />
<Route
exact
path="/app/ui"

View file

@ -0,0 +1,116 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
TextField,
DialogActions,
Button,
} from '@material-ui/core';
// styles
import useStyles from './styles';
import {
setTwitterBearerTokenRequest,
} from '../../squeakclient/requests';
export default function SetBearerTokenDialog({
open,
handleClose,
reloadBearerTokenFn,
...props
}) {
const classes = useStyles();
const [bearerToken, setBearerToken] = useState('');
const resetFields = () => {
setBearerToken('');
};
const handleChangeBearerToken = (event) => {
setBearerToken(event.target.value);
};
const handleResponse = (response) => {
// goToProfilePage(history, response.getProfileId());
// TODO
reloadBearerTokenFn();
};
const handleErr = (err) => {
alert(`Error setting bearer token: ${err}`);
};
const setBearerTokenRequest = (bearerToken) => {
setTwitterBearerTokenRequest(bearerToken, handleResponse, handleErr);
};
function handleSubmit(event) {
event.preventDefault();
console.log('bearerToken:', bearerToken);
if (!bearerToken) {
alert('Bearer Token cannot be empty.');
return;
}
setBearerTokenRequest(bearerToken);
handleClose();
}
function SetBearerTokenInput() {
return (
<TextField
id="standard-textarea"
label="Bearer Token"
required
autoFocus
value={bearerToken}
onChange={handleChangeBearerToken}
fullWidth
inputProps={{ maxLength: 256 }}
/>
);
}
function CancelButton() {
return (
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Cancel
</Button>
);
}
function SetBearerTokenButton() {
return (
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
Set Bearer Token
</Button>
);
}
return (
<Dialog open={open} onEnter={resetFields} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Set Bearer Token</DialogTitle>
<form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off">
<DialogContent>
{SetBearerTokenInput()}
</DialogContent>
<DialogActions>
{CancelButton()}
{SetBearerTokenButton()}
</DialogActions>
</form>
</Dialog>
);
}

View file

@ -0,0 +1,6 @@
{
"name": "SetBearerTokenDialog",
"version": "0.0.0",
"private": true,
"main": "SetBearerTokenDialog.js"
}

View file

@ -0,0 +1,43 @@
import { makeStyles } from '@material-ui/styles';
export default makeStyles((theme) => ({
widgetWrapper: {
display: 'flex',
minHeight: '100%',
},
widgetHeader: {
padding: theme.spacing(3),
paddingBottom: theme.spacing(1),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
widgetRoot: {
boxShadow: theme.customShadows.widget,
},
widgetBody: {
paddingBottom: theme.spacing(3),
paddingRight: theme.spacing(3),
paddingLeft: theme.spacing(3),
},
noPadding: {
padding: 0,
},
paper: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
},
moreButton: {
margin: -theme.spacing(1),
padding: 0,
width: 40,
height: 40,
color: theme.palette.text.hint,
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: 'rgba(255, 255, 255, 0.35)',
},
},
}));

View file

@ -8,6 +8,7 @@ import {
CloudDownload as PeerIcon,
History as HistoryIcon,
Favorite as LikedIcon,
Twitter as TwitterIcon,
} from '@material-ui/icons';
import { useTheme } from '@material-ui/styles';
import { withRouter } from 'react-router-dom';
@ -45,6 +46,9 @@ const structure = [
{
id: 5, label: 'Payments', link: '/app/payments', icon: <HistoryIcon />,
},
{
id: 6, label: 'Twitter', link: '/app/twitter', icon: <TwitterIcon />,
},
];
function Sidebar({ location }) {

View file

@ -0,0 +1,156 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
IconButton,
Menu,
MenuItem,
Typography,
Button,
} from '@material-ui/core';
import Card from '@material-ui/core/Card';
import Box from '@material-ui/core/Box';
import CardHeader from '@material-ui/core/CardHeader';
// icons
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import useStyles from '../../pages/wallet/styles';
import SqueakUserAvatar from '../SqueakUserAvatar';
import SqueakProfileFollowingIndicator from '../SqueakProfileFollowingIndicator';
import DeleteTwitterAccountDialog from '../DeleteTwitterAccountDialog';
export default function TwitterAccountListItem({
accountEntry,
reloadAccountsFn,
...props
}) {
const classes = useStyles({
clickable: true,
});
const [anchorEl, setAnchorEl] = useState(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const handleClickMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleCloseMenu = () => {
setAnchorEl(null);
};
const onDeleteClick = () => {
console.log('Handling delete click...');
handleCloseMenu();
setDeleteDialogOpen(true);
};
const handleCloseDeleteDialog = () => {
setDeleteDialogOpen(false);
};
// const onProfileClick = (event) => {
// event.preventDefault();
// console.log('Handling profile click...');
// const profileId = profile.getProfileId();
// // const host = getPeerHost();
// // const port = getPeerPort();
// goToProfilePage(history, profileId);
// };
// const getPeerHost = () => {
// const address = peer.getAddress();
// if (address == null) {
// return null;
// }
// const pieces = address.split(":");
// return pieces[0];
// }
//
// const getPeerPort = () => {
// const address = peer.getAddress();
// if (address == null) {
// return null;
// }
// const pieces = address.split(":");
// if (pieces.length < 2) {
// return null;
// }
// return pieces[1];
// }
const twitterHandle = accountEntry.getHandle();
const profile = accountEntry.getProfile();
function DeleteTwitterAccountDialogContent() {
return (
<>
<DeleteTwitterAccountDialog
open={deleteDialogOpen}
handleClose={handleCloseDeleteDialog}
twitterAccount={accountEntry}
reloadAccountsFn={reloadAccountsFn}
/>
</>
);
}
function AccountCardContent() {
const name = profile.getProfileName();
const address = profile.getAddress();
return (
<>
<Box>
{`Profile Name: ${name}`}
</Box>
<Box>
{`Address: ${address}`}
</Box>
</>
);
}
return (
<>
<Card
className={classes.root}
// onClick={alert('do something')}
>
<CardHeader
action={(
<>
<IconButton aria-label="settings" onClick={handleClickMenu}>
<MoreVertIcon />
</IconButton>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleCloseMenu}
>
<MenuItem onClick={onDeleteClick}>Delete</MenuItem>
</Menu>
</>
)}
avatar={(
<SqueakUserAvatar
squeakAddress={(profile && profile.getAddress())}
squeakProfile={profile}
/>
)}
title={`Twitter handle: ${twitterHandle}`}
subheader={AccountCardContent()}
/>
</Card>
{DeleteTwitterAccountDialogContent()}
</>
);
}

View file

@ -0,0 +1,6 @@
{
"name": "TwitterAccountListItem",
"version": "0.0.0",
"private": true,
"main": "TwitterAccountListItem.js"
}

View file

@ -0,0 +1,276 @@
import React, { useState, useEffect } from 'react';
import {
Grid,
Button,
Tabs,
Tab,
AppBar,
Box,
CircularProgress,
Typography,
} from '@material-ui/core';
// styles
// styles
import useStyles from './styles';
// components
import Widget from '../../components/Widget';
import SetBearerTokenDialog from '../../components/SetBearerTokenDialog';
import AddTwitterAccountDialog from '../../components/AddTwitterAccountDialog';
import TwitterAccountListItem from '../../components/TwitterAccountListItem';
import {
getTwitterBearerTokenRequest,
getTwitterAccountsRequest,
} from '../../squeakclient/requests';
export default function Twitter() {
const classes = useStyles();
const [bearerToken, setBearerToken] = useState('');
const [accounts, setAccounts] = useState([]);
const [waitingForBearerToken, setWaitingForBearerToken] = useState(false);
const [waitingForAccounts, setWaitingForAccounts] = useState(false);
const [setBearerTokenDialogOpen, setSetBearerTokenDialogOpen] = useState(false);
const [addAccountDialogOpen, setAddAccountDialogOpen] = useState(false);
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
const getBearerToken = () => {
setWaitingForBearerToken(true);
getTwitterBearerTokenRequest((resp) => {
setWaitingForBearerToken(false);
setBearerToken(resp);
});
};
const getAccounts = () => {
setWaitingForAccounts(true);
getTwitterAccountsRequest((resp) => {
setWaitingForAccounts(false);
setAccounts(resp);
});
};
const handleClickOpenSetBearerTokenDialog = () => {
setSetBearerTokenDialogOpen(true);
};
const handleCloseSetBearerTokenDialog = () => {
setSetBearerTokenDialogOpen(false);
};
const handleClickOpenAddAccountDialog = () => {
setAddAccountDialogOpen(true);
};
const handleCloseAddAccountDialog = () => {
setAddAccountDialogOpen(false);
};
useEffect(() => {
getBearerToken();
}, []);
useEffect(() => {
getAccounts();
}, []);
function TabPanel(props) {
const {
children, value, index, ...other
} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<div>{children}</div>
)}
</div>
);
}
function BearerTokenSummary() {
const bearerTokenText = (bearerToken ? bearerToken : 'not configured')
return (
<Grid item xs={12}>
<Box
p={1}
>
<Typography variant="h5" component="h5">
{`Bearer Token: ${bearerTokenText}`}
</Typography>
</Box>
</Grid>
);
}
function AccountsGridItem(accounts) {
return (
<Grid item xs={12}>
{accounts.map((account) => (
<Box
p={1}
key={account.getTwitterAccountId()}
>
<TwitterAccountListItem
key={account.getTwitterAccountId()}
handlePeerClick={() => console.log('clicked account')}
accountEntry={account}
reloadAccountsFn={getAccounts}
/>
</Box>
))}
</Grid>
);
}
function AccountsContent() {
return (
<Grid item xs={12}>
<Box
p={1}
>
<Typography variant="h5" component="h5">
{`Number of accounts: ${accounts.length}`}
</Typography>
{AccountsGridItem(accounts)}
</Box>
</Grid>
);
}
function SetBearerTokenButton() {
return (
<>
<Grid item xs={12}>
<div className={classes.root}>
<Button
variant="contained"
onClick={() => {
handleClickOpenSetBearerTokenDialog();
}}
>
Set Bearer Token
</Button>
</div>
</Grid>
</>
);
}
function AddAccountButton() {
return (
<>
<Grid item xs={12}>
<div className={classes.root}>
<Button
variant="contained"
onClick={() => {
handleClickOpenAddAccountDialog();
}}
>
Add Twitter Account
</Button>
</div>
</Grid>
</>
);
}
function WaitingIndicator() {
return (
<CircularProgress size={48} className={classes.buttonProgress} />
);
}
function TwitterAccountsContent() {
return (
<>
<Grid container spacing={4}>
<Grid item xs={12}>
<Widget disableWidgetMenu>
{SetBearerTokenButton()}
{AddAccountButton()}
{BearerTokenSummary()}
{AccountsContent()}
</Widget>
</Grid>
</Grid>
</>
);
}
function TwitterTabs() {
return (
<>
<AppBar position="static" color="default">
<Tabs value={0} aria-label="simple tabs example">
<Tab label="Twitter Accounts" {...a11yProps(0)} />
</Tabs>
</AppBar>
<TabPanel value={0} index={0}>
{(waitingForBearerToken || waitingForAccounts)
? WaitingIndicator()
: TwitterAccountsContent()}
</TabPanel>
</>
);
}
function SetBearerTokenDialogContent() {
return (
<>
<SetBearerTokenDialog
open={setBearerTokenDialogOpen}
handleClose={handleCloseSetBearerTokenDialog}
reloadBearerTokenFn={getBearerToken}
/>
</>
);
}
function AddAccountDialogContent() {
return (
<>
<AddTwitterAccountDialog
open={addAccountDialogOpen}
handleClose={handleCloseAddAccountDialog}
reloadAccountsFn={getAccounts}
/>
</>
);
}
function GridContent() {
return (
<Grid container spacing={0}>
<Grid item xs={12} sm={9}>
{TwitterTabs()}
</Grid>
<Grid item xs={12} sm={3} />
</Grid>
);
}
return (
<>
{GridContent()}
{SetBearerTokenDialogContent()}
{AddAccountDialogContent()}
< />
);
}

View file

@ -0,0 +1,6 @@
{
"name": "Twitter",
"version": "0.0.0",
"main": "Twitter.js",
"private": true
}

View file

@ -0,0 +1,9 @@
import { makeStyles } from '@material-ui/styles';
export default makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
},
},
}));

View file

@ -131,6 +131,16 @@ import {
GetPeerByAddressReply,
GetDefaultPeerPortRequest,
GetDefaultPeerPortReply,
GetTwitterBearerTokenRequest,
GetTwitterBearerTokenReply,
SetTwitterBearerTokenRequest,
SetTwitterBearerTokenReply,
GetTwitterAccountsRequest,
GetTwitterAccountsReply,
AddTwitterAccountRequest,
AddTwitterAccountReply,
DeleteTwitterAccountRequest,
DeleteTwitterAccountReply,
} from '../proto/squeak_admin_pb';
console.log('The value of REACT_APP_DEV_MODE_ENABLED is:', Boolean(process.env.REACT_APP_DEV_MODE_ENABLED));
@ -1179,6 +1189,64 @@ export function getDefaultPeerPortRequest(handleResponse) {
);
}
export function setTwitterBearerTokenRequest(bearerToken, handleResponse) {
const request = new SetTwitterBearerTokenRequest();
request.setBearerToken(bearerToken);
makeRequest(
'settwitterbearertoken',
request,
SetTwitterBearerTokenReply.deserializeBinary,
handleResponse,
);
}
export function getTwitterBearerTokenRequest(handleResponse) {
const request = new GetTwitterBearerTokenRequest();
makeRequest(
'gettwitterbearertoken',
request,
GetTwitterBearerTokenReply.deserializeBinary,
(response) => {
handleResponse(response.getBearerToken());
},
);
}
export function getTwitterAccountsRequest(handleResponse) {
const request = new GetTwitterAccountsRequest();
makeRequest(
'gettwitteraccounts',
request,
GetTwitterAccountsReply.deserializeBinary,
(response) => {
handleResponse(response.getTwitterAccountsList());
},
);
}
export function addTwitterAccountRequest(twitterHandle, profileId, handleResponse) {
const request = new AddTwitterAccountRequest();
request.setHandle(twitterHandle);
request.setProfileId(profileId);
makeRequest(
'addtwitteraccount',
request,
AddTwitterAccountReply.deserializeBinary,
handleResponse,
);
}
export function deleteTwitterAccountRequest(twitterAccountId, handleResponse) {
const request = new DeleteTwitterAccountRequest();
request.setTwitterAccountId(twitterAccountId);
makeRequest(
'deletetwitteraccount',
request,
DeleteTwitterAccountReply.deserializeBinary,
handleResponse,
);
}
// export function subscribeConnectedPeersRequest(handleResponse) {
// const request = new SubscribeConnectedPeersRequest();
// const stream = client.subscribeConnectedPeers(request);

View file

@ -51,11 +51,13 @@ from tests.util import get_peer_by_address
from tests.util import get_search_squeaks
from tests.util import get_squeak_display
from tests.util import get_squeak_profile
from tests.util import get_twitter_bearer_token
from tests.util import import_signing_profile
from tests.util import make_squeak
from tests.util import open_peer_connection
from tests.util import peer_connection
from tests.util import send_coins
from tests.util import set_twitter_bearer_token
from tests.util import subscribe_connected_peers
from tests.util import subscribe_squeak_ancestor_entries
from tests.util import subscribe_squeak_entry
@ -69,6 +71,18 @@ def test_get_network(admin_stub):
assert network == "simnet"
def test_get_twitter_bearer_token(admin_stub):
# Get the twitter bearer token
bearer_token = get_twitter_bearer_token(admin_stub)
assert bearer_token == ''
set_twitter_bearer_token(admin_stub, "new_bearer_token")
bearer_token = get_twitter_bearer_token(admin_stub)
assert bearer_token == "new_bearer_token"
def test_get_external_address(admin_stub):
# Get the external address
external_address = get_external_address(admin_stub)

View file

@ -307,6 +307,21 @@ def get_network(node_stub):
return get_network_response.network
def set_twitter_bearer_token(node_stub, bearer_token):
node_stub.SetTwitterBearerToken(
squeak_admin_pb2.SetTwitterBearerTokenRequest(
bearer_token=bearer_token,
)
)
def get_twitter_bearer_token(node_stub):
get_twitter_bearer_token_response = node_stub.GetTwitterBearerToken(
squeak_admin_pb2.GetTwitterBearerTokenRequest()
)
return get_twitter_bearer_token_response.bearer_token
def get_external_address(node_stub):
get_external_address_response = node_stub.GetExternalAddress(
squeak_admin_pb2.GetExternalAddressRequest()

View file

@ -332,6 +332,26 @@ service SqueakAdmin {
*/
rpc GetDefaultPeerPort (GetDefaultPeerPortRequest) returns (GetDefaultPeerPortReply) {}
/** sqkadmin: `settwitterbearertoken`
*/
rpc SetTwitterBearerToken (SetTwitterBearerTokenRequest) returns (SetTwitterBearerTokenReply) {}
/** sqkadmin: `gettwitterbearertoken`
*/
rpc GetTwitterBearerToken (GetTwitterBearerTokenRequest) returns (GetTwitterBearerTokenReply) {}
/** sqkadmin: `addtwitteraccount`
*/
rpc AddTwitterAccount (AddTwitterAccountRequest) returns (AddTwitterAccountReply) {}
/** sqkadmin: `gettwitteraccounts`
*/
rpc GetTwitterAccounts (GetTwitterAccountsRequest) returns (GetTwitterAccountsReply) {}
/** sqkadmin: `deletetwitteraccount`
*/
rpc DeleteTwitterAccount (DeleteTwitterAccountRequest) returns (DeleteTwitterAccountReply) {}
}
message CreateSigningProfileRequest {
@ -1224,3 +1244,64 @@ message GetDefaultPeerPortReply {
int32 port = 1;
}
message SetTwitterBearerTokenRequest {
/// The bearer token string.
string bearer_token = 1;
}
message SetTwitterBearerTokenReply {
}
message GetTwitterBearerTokenRequest {
}
message GetTwitterBearerTokenReply {
/// The bearer token string.
string bearer_token = 1;
}
message TwitterAccount {
/// The twitter account id
int32 twitter_account_id = 1;
/// The twitter account handle.
string handle = 2;
/// The profile id
int32 profile_id = 3;
/// Is profile_id known
bool is_profile_known = 4;
/// The profile
SqueakProfile profile = 5;
}
message AddTwitterAccountRequest {
/// The twitter account handle.
string handle = 1;
/// The profile id
int32 profile_id = 2;
}
message AddTwitterAccountReply {
/// The twitter account id
int32 twitter_account_id = 1;
}
message GetTwitterAccountsRequest {
}
message GetTwitterAccountsReply {
/// The twitter accounts
repeated TwitterAccount twitter_accounts = 1;
}
message DeleteTwitterAccountRequest {
/// The twitter account id
int32 twitter_account_id = 1;
}
message DeleteTwitterAccountReply {
}

View file

@ -40,6 +40,7 @@ from squeaknode.core.sent_payment_summary import SentPaymentSummary
from squeaknode.core.squeak_entry import SqueakEntry
from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeak_profile import SqueakProfile
from squeaknode.core.twitter_account_entry import TwitterAccountEntry
logger = logging.getLogger(__name__)
@ -268,6 +269,20 @@ def download_result_to_message(download_result: DownloadResult) -> squeak_admin_
)
def twitter_account_to_message(twitter_account_entry: TwitterAccountEntry) -> squeak_admin_pb2.TwitterAccount:
twitter_account_id = twitter_account_entry.twitter_account_id or 0
squeak_profile = twitter_account_entry.profile
profile_msg = None
if squeak_profile is not None:
profile_msg = squeak_profile_to_message(squeak_profile)
return squeak_admin_pb2.TwitterAccount(
twitter_account_id=twitter_account_id,
handle=twitter_account_entry.handle,
profile_id=twitter_account_entry.profile_id,
profile=profile_msg,
)
def optional_squeak_profile_to_message(squeak_profile: Optional[SqueakProfile]) -> Optional[squeak_admin_pb2.SqueakProfile]:
if squeak_profile is None:
return None

View file

@ -45,6 +45,7 @@ from squeaknode.admin.messages import sent_payment_to_message
from squeaknode.admin.messages import squeak_entry_to_message
from squeaknode.admin.messages import squeak_peer_to_message
from squeaknode.admin.messages import squeak_profile_to_message
from squeaknode.admin.messages import twitter_account_to_message
from squeaknode.admin.profile_image_util import base64_string_to_bytes
from squeaknode.lightning.lnd_lightning_client import LNDLightningClient
from squeaknode.node.squeak_controller import SqueakController
@ -1053,3 +1054,56 @@ class SqueakAdminServerHandler(object):
return squeak_admin_pb2.GetDefaultPeerPortReply(
port=default_peer_port,
)
def handle_set_twitter_bearer_token(self, request):
twitter_bearer_token = request.bearer_token
logger.info("Handle set twitter bearer token with value: {}".format(
twitter_bearer_token,
))
self.squeak_controller.set_twitter_bearer_token(twitter_bearer_token)
return squeak_admin_pb2.SetTwitterBearerTokenReply()
def handle_get_twitter_bearer_token(self, request):
logger.info("Handle get twitter bearer token")
twitter_bearer_token = self.squeak_controller.get_twitter_bearer_token()
return squeak_admin_pb2.GetTwitterBearerTokenReply(
bearer_token=twitter_bearer_token,
)
def handle_add_twitter_account(self, request):
handle = request.handle
profile_id = request.profile_id
logger.info("Handle add twitter account with handle: {} and profile_id: {}".format(
handle,
profile_id,
))
twitter_account_id = self.squeak_controller.add_twitter_account(
handle,
profile_id,
)
return squeak_admin_pb2.AddTwitterAccountReply(
twitter_account_id=twitter_account_id,
)
def handle_get_twitter_accounts(self, request):
logger.info("Handle get twitter accounts")
twitter_accounts = self.squeak_controller.get_twitter_accounts()
logger.info("Got number of twitter accounts: {}".format(
len(twitter_accounts)))
twitter_account_msgs = [
twitter_account_to_message(twitter_account)
for twitter_account in twitter_accounts
]
return squeak_admin_pb2.GetTwitterAccountsReply(
twitter_accounts=twitter_account_msgs,
)
def handle_delete_twitter_account(self, request):
twitter_account_id = request.twitter_account_id
logger.info("Handle delete twitter account with id: {}".format(
twitter_account_id,
))
self.squeak_controller.delete_twitter_account(
twitter_account_id,
)
return squeak_admin_pb2.DeleteTwitterAccountReply()

View file

@ -382,3 +382,18 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
def GetDefaultPeerPort(self, request, context):
return self.handler.handle_get_default_peer_port(request)
def SetTwitterBearerToken(self, request, context):
return self.handler.handle_set_twitter_bearer_token(request)
def GetTwitterBearerToken(self, request, context):
return self.handler.handle_get_twitter_bearer_token(request)
def AddTwitterAccount(self, request, context):
return self.handler.handle_add_twitter_account(request)
def GetTwitterAccounts(self, request, context):
return self.handler.handle_get_twitter_accounts(request)
def DeleteTwitterAccount(self, request, context):
return self.handler.handle_delete_twitter_account(request)

View file

@ -523,6 +523,36 @@ def create_app(handler, username, password):
def getdefaultpeerport(msg):
return handler.handle_get_default_peer_port(msg)
@app.route("/settwitterbearertoken", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.SetTwitterBearerTokenRequest())
def settwitterbearertoken(msg):
return handler.handle_set_twitter_bearer_token(msg)
@app.route("/gettwitterbearertoken", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.GetTwitterBearerTokenRequest())
def gettwitterbearertoken(msg):
return handler.handle_get_twitter_bearer_token(msg)
@app.route("/addtwitteraccount", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.AddTwitterAccountRequest())
def addtwitteraccount(msg):
return handler.handle_add_twitter_account(msg)
@app.route("/gettwitteraccounts", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.GetTwitterAccountsRequest())
def gettwitteraccounts(msg):
return handler.handle_get_twitter_accounts(msg)
@app.route("/deletetwitteraccount", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.DeleteTwitterAccountRequest())
def deletetwitteraccount(msg):
return handler.handle_delete_twitter_account(msg)
return app

View file

@ -1,25 +1,25 @@
{
"files": {
"main.js": "/static/js/main.33cb64aa.chunk.js",
"main.js.map": "/static/js/main.33cb64aa.chunk.js.map",
"main.js": "/static/js/main.e3853aca.chunk.js",
"main.js.map": "/static/js/main.e3853aca.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.9f0ba400.js",
"runtime-main.js.map": "/static/js/runtime-main.9f0ba400.js.map",
"static/css/2.15110fae.chunk.css": "/static/css/2.15110fae.chunk.css",
"static/js/2.da272a6c.chunk.js": "/static/js/2.da272a6c.chunk.js",
"static/js/2.da272a6c.chunk.js.map": "/static/js/2.da272a6c.chunk.js.map",
"static/css/2.ea4ba2f0.chunk.css": "/static/css/2.ea4ba2f0.chunk.css",
"static/js/2.fec45849.chunk.js": "/static/js/2.fec45849.chunk.js",
"static/js/2.fec45849.chunk.js.map": "/static/js/2.fec45849.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.d9c6b835185cb9ce4c390fec016bcf26.js": "/precache-manifest.d9c6b835185cb9ce4c390fec016bcf26.js",
"precache-manifest.32e383b1f0c27e9043189a2e742324b8.js": "/precache-manifest.32e383b1f0c27e9043189a2e742324b8.js",
"service-worker.js": "/service-worker.js",
"static/css/2.15110fae.chunk.css.map": "/static/css/2.15110fae.chunk.css.map",
"static/js/2.da272a6c.chunk.js.LICENSE.txt": "/static/js/2.da272a6c.chunk.js.LICENSE.txt",
"static/css/2.ea4ba2f0.chunk.css.map": "/static/css/2.ea4ba2f0.chunk.css.map",
"static/js/2.fec45849.chunk.js.LICENSE.txt": "/static/js/2.fec45849.chunk.js.LICENSE.txt",
"static/media/font-awesome.min.css": "/static/media/fontawesome-webfont.fee66e71.woff",
"static/media/google.svg": "/static/media/google.695a3160.svg",
"static/media/logo.svg": "/static/media/logo.a0185b04.svg"
},
"entrypoints": [
"static/js/runtime-main.9f0ba400.js",
"static/css/2.15110fae.chunk.css",
"static/js/2.da272a6c.chunk.js",
"static/js/main.33cb64aa.chunk.js"
"static/css/2.ea4ba2f0.chunk.css",
"static/js/2.fec45849.chunk.js",
"static/js/main.e3853aca.chunk.js"
]
}

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><meta name="description" content="Squeaknode is a frontend for accessing a squeak node"><meta name="keywords" content="squeak, bitcoin, lightning"><meta name="author" content="Flatlogic LLC."><link href="/static/css/2.15110fae.chunk.css" rel="stylesheet"></head><body style="font-family:Roboto,sans-serif"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,f,l=r[0],a=r[1],i=r[2],c=0,s=[];c<l.length;c++)f=l[c],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var a=t[l];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var l=this["webpackJsonpsqueak-node-frontend"]=this["webpackJsonpsqueak-node-frontend"]||[],a=l.push.bind(l);l.push=r,l=l.slice();for(var i=0;i<l.length;i++)r(l[i]);var p=a;t()}([])</script><script src="/static/js/2.da272a6c.chunk.js"></script><script src="/static/js/main.33cb64aa.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><meta name="description" content="Squeaknode is a frontend for accessing a squeak node"><meta name="keywords" content="squeak, bitcoin, lightning"><meta name="author" content="Flatlogic LLC."><link href="/static/css/2.ea4ba2f0.chunk.css" rel="stylesheet"></head><body style="font-family:Roboto,sans-serif"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,f,l=r[0],a=r[1],i=r[2],c=0,s=[];c<l.length;c++)f=l[c],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var a=t[l];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var l=this["webpackJsonpsqueak-node-frontend"]=this["webpackJsonpsqueak-node-frontend"]||[],a=l.push.bind(l);l.push=r,l=l.slice();for(var i=0;i<l.length;i++)r(l[i]);var p=a;t()}([])</script><script src="/static/js/2.fec45849.chunk.js"></script><script src="/static/js/main.e3853aca.chunk.js"></script></body></html>

View file

@ -1,23 +1,23 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "8c86c0a3e6d774d7efbdd2fc9d0cc659",
"revision": "22fee2d84ebba1478ff826f4cd80b752",
"url": "/index.html"
},
{
"revision": "b2d231408fcfce520381",
"url": "/static/css/2.15110fae.chunk.css"
"revision": "3d3c427e1e3db9959c13",
"url": "/static/css/2.ea4ba2f0.chunk.css"
},
{
"revision": "b2d231408fcfce520381",
"url": "/static/js/2.da272a6c.chunk.js"
"revision": "3d3c427e1e3db9959c13",
"url": "/static/js/2.fec45849.chunk.js"
},
{
"revision": "ce334ad48c4dc2f747f813c26790dfba",
"url": "/static/js/2.da272a6c.chunk.js.LICENSE.txt"
"url": "/static/js/2.fec45849.chunk.js.LICENSE.txt"
},
{
"revision": "1c5b4c4722319d719adb",
"url": "/static/js/main.33cb64aa.chunk.js"
"revision": "ca3dda5ae21cec8eb125",
"url": "/static/js/main.e3853aca.chunk.js"
},
{
"revision": "cc9816de5a8639d377ea",

View file

@ -14,7 +14,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"/precache-manifest.d9c6b835185cb9ce4c390fec016bcf26.js"
"/precache-manifest.32e383b1f0c27e9043189a2e742324b8.js"
);
self.addEventListener('message', (event) => {

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -65,6 +65,7 @@ DEFAULT_OFFER_DELETION_INTERVAL_S = 10
DEFAULT_SUBSCRIBE_INVOICES_RETRY_S = 10
DEFAULT_SQUEAK_RETENTION_S = 604800
DEFAULT_SQUEAK_DELETION_INTERVAL_S = 10
DEFAULT_FORWARD_TWEETS_RETRY_S = 10
@section('bitcoin')
@ -157,6 +158,12 @@ class DbConfig(Config):
connection_string = key(cast=str, required=False, default="")
@section('twitter')
class TwitterConfig(Config):
forward_tweets_retry_s = key(
cast=int, required=False, default=DEFAULT_FORWARD_TWEETS_RETRY_S)
class SqueaknodeConfig(Config):
bitcoin = group_key(BitcoinConfig)
lnd = group_key(LndConfig)
@ -166,6 +173,7 @@ class SqueaknodeConfig(Config):
webadmin = group_key(WebadminConfig)
node = group_key(NodeConfig)
db = group_key(DbConfig)
twitter = group_key(TwitterConfig)
# description = key(cast=str, section_name="general")
def __init__(self, config_path=None, dict_config=None):

View file

@ -0,0 +1,30 @@
# 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.
from typing import Callable
from typing import Iterator
from typing import NamedTuple
class TweetStream(NamedTuple):
"""Represents the result of a twitter subscription."""
cancel_fn: Callable[[], None]
result_stream: Iterator[dict]

View file

@ -0,0 +1,30 @@
# 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.
from typing import NamedTuple
from typing import Optional
class TwitterAccount(NamedTuple):
"""Represents a twitter account being mirrored to a squeak profile."""
twitter_account_id: Optional[int]
handle: str
profile_id: int

View file

@ -0,0 +1,33 @@
# 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.
from typing import NamedTuple
from typing import Optional
from squeaknode.core.squeak_profile import SqueakProfile
class TwitterAccountEntry(NamedTuple):
"""Represents a twitter account being mirrored to a squeak profile."""
twitter_account_id: Optional[int]
handle: str
profile_id: int
profile: Optional[SqueakProfile]

View file

@ -0,0 +1,25 @@
# 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.
class UpdateTwitterStreamEvent():
"""Represents an event that requires an update of the twitter stream."""

View file

@ -0,0 +1,29 @@
# 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.
from typing import NamedTuple
from typing import Optional
class UserConfig(NamedTuple):
"""Represents a config for a user."""
username: str
twitter_bearer_token: Optional[str] = None

View file

@ -0,0 +1,52 @@
# 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 twitter account table.
Revision ID: 12d38bbf7a87
Revises: f7e2c66a9188
Create Date: 2021-11-04 02:02:25.596692
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '12d38bbf7a87'
down_revision = 'f7e2c66a9188'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('twitter_account',
sa.Column('twitter_account_id',
sa.Integer(), nullable=False),
sa.Column('handle', sa.String(), nullable=False),
sa.Column('profile_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('twitter_account_id'),
sa.UniqueConstraint('handle')
)
def downgrade():
op.drop_table('twitter_account')

View file

@ -0,0 +1,50 @@
# 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 configs table
Revision ID: f7e2c66a9188
Revises: 6ad4a9483880
Create Date: 2021-11-03 21:57:44.706370
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'f7e2c66a9188'
down_revision = '6ad4a9483880'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('config',
sa.Column('username', sa.String(), nullable=False),
sa.Column('twitter_bearer_token',
sa.String(), nullable=True),
sa.PrimaryKeyConstraint('username')
)
def downgrade():
op.drop_table('config')

View file

@ -179,3 +179,18 @@ class Models:
Column("peer_port", Integer, nullable=False),
sqlite_autoincrement=True,
)
self.configs = Table(
"config",
self.metadata,
Column("username", String, primary_key=True),
Column("twitter_bearer_token", String, nullable=True),
)
self.twitter_accounts = Table(
"twitter_account",
self.metadata,
Column("twitter_account_id", Integer, primary_key=True),
Column("handle", String, unique=True, nullable=False),
Column("profile_id", Integer, nullable=False),
)

View file

@ -49,6 +49,9 @@ from squeaknode.core.squeak_entry import SqueakEntry
from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeak_profile import SqueakProfile
from squeaknode.core.squeaks import get_hash
from squeaknode.core.twitter_account import TwitterAccount
from squeaknode.core.twitter_account_entry import TwitterAccountEntry
from squeaknode.core.user_config import UserConfig
from squeaknode.db.exception import SqueakDatabaseError
from squeaknode.db.migrations import run_migrations
from squeaknode.db.models import Models
@ -128,6 +131,14 @@ class SqueakDb:
def sent_offers(self):
return self.models.sent_offers
@property
def configs(self):
return self.models.configs
@property
def twitter_accounts(self):
return self.models.twitter_accounts
@property
def squeak_has_secret_key(self):
return self.squeaks.c.secret_key != None # noqa: E711
@ -1342,6 +1353,91 @@ class SqueakDb:
sent_payment_summary = self._parse_sent_payment_summary(row)
return sent_payment_summary
def insert_config(self, user_config: UserConfig) -> Optional[str]:
""" Insert a new config.
Return the name (str) of the inserted config user.
Return None if config already exists.
"""
ins = self.configs.insert().values(
username=user_config.username,
twitter_bearer_token=user_config.twitter_bearer_token,
)
with self.get_connection() as connection:
try:
res = connection.execute(ins)
username = res.inserted_primary_key[0]
return username
except sqlalchemy.exc.IntegrityError:
logger.debug("Failed to insert config.", exc_info=True)
return None
def get_config(self, username: str) -> Optional[UserConfig]:
""" Get a config. """
s = select([self.configs]).where(self.configs.c.username == username)
with self.get_connection() as connection:
result = connection.execute(s)
row = result.fetchone()
if row is None:
return None
return self._parse_user_config(row)
def set_config_twitter_bearer_token(self, username: str, twitter_bearer_token: str) -> None:
""" Set a config twitter bearer token. """
stmt = (
self.configs.update()
.where(self.configs.c.username == username)
.values(twitter_bearer_token=twitter_bearer_token)
)
with self.get_connection() as connection:
connection.execute(stmt)
def insert_twitter_account(self, twitter_account: TwitterAccount) -> Optional[int]:
""" Insert a new twitter account mapping to a squeak profile.
Return the id (int) of the inserted twitter account.
Return None if twitter account already exists.
"""
ins = self.twitter_accounts.insert().values(
handle=twitter_account.handle,
profile_id=twitter_account.profile_id,
)
with self.get_connection() as connection:
try:
res = connection.execute(ins)
twitter_account_id = res.inserted_primary_key[0]
return twitter_account_id
except sqlalchemy.exc.IntegrityError:
logger.debug(
"Failed to insert twitter account.", exc_info=True)
return None
def get_twitter_accounts(self) -> List[TwitterAccountEntry]:
""" Get all twitter accounts. """
s = (
select([self.twitter_accounts, self.profiles])
.select_from(
self.twitter_accounts.outerjoin(
self.profiles,
self.profiles.c.profile_id == self.twitter_accounts.c.profile_id,
)
)
)
with self.get_connection() as connection:
result = connection.execute(s)
rows = result.fetchall()
twitter_accounts = [
self._parse_twitter_account_entry(row) for row in rows]
return twitter_accounts
def delete_twitter_account(self, twitter_account_id: int) -> None:
""" Delete a twitter_account. """
delete_twitter_account_stmt = self.twitter_accounts.delete().where(
self.twitter_accounts.c.twitter_account_id == twitter_account_id
)
with self.get_connection() as connection:
connection.execute(delete_twitter_account_stmt)
def _parse_squeak(self, row) -> CSqueak:
return CSqueak.deserialize(row["squeak"])
@ -1482,3 +1578,18 @@ class SqueakDb:
num_sent_payments=row["num_payments_sent"],
total_amount_sent_msat=row["total_amount_sent_msat"],
)
def _parse_user_config(self, row) -> UserConfig:
return UserConfig(
username=row["username"],
twitter_bearer_token=row["twitter_bearer_token"],
)
def _parse_twitter_account_entry(self, row) -> TwitterAccountEntry:
profile = self._try_parse_squeak_profile(row)
return TwitterAccountEntry(
twitter_account_id=row["twitter_account_id"],
handle=row["handle"],
profile_id=row["profile_id"],
profile=profile,
)

View file

@ -0,0 +1,51 @@
# 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.
import logging
import threading
from squeaknode.node.squeak_controller import SqueakController
logger = logging.getLogger(__name__)
class ProcessForwardTweetsWorker:
def __init__(self, twitter_forwarder):
self.twitter_forwarder = twitter_forwarder
self.stopped = threading.Event()
def start_running(self, squeak_controller: SqueakController):
threading.Thread(
target=self.forward_tweets,
args=(squeak_controller,),
daemon=True,
name="process_forward_tweets_thread",
).start()
def stop_running(self):
self.stopped.set()
def forward_tweets(self, squeak_controller: SqueakController):
logger.info("Starting ProcessForwardTweetsWorker...")
self.twitter_forwarder.start_processing(squeak_controller)
self.stopped.wait()
logger.info("Stopping ProcessForwardTweetsWorker...")
self.twitter_forwarder.stop_processing()

View file

@ -59,7 +59,11 @@ from squeaknode.core.squeak_entry import SqueakEntry
from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeak_profile import SqueakProfile
from squeaknode.core.squeaks import get_hash
from squeaknode.core.twitter_account import TwitterAccount
from squeaknode.core.twitter_account_entry import TwitterAccountEntry
from squeaknode.core.update_subscriptions_event import UpdateSubscriptionsEvent
from squeaknode.core.update_twitter_stream_event import UpdateTwitterStreamEvent
from squeaknode.core.user_config import UserConfig
from squeaknode.node.active_download_manager import ActiveDownload
from squeaknode.node.listener_subscription_client import EventListener
from squeaknode.node.received_payments_subscription_client import ReceivedPaymentsSubscriptionClient
@ -77,6 +81,7 @@ class SqueakController:
payment_processor,
network_manager,
download_manager,
tweet_forwarder,
config,
):
self.squeak_db = squeak_db
@ -87,8 +92,10 @@ class SqueakController:
self.new_received_offer_listener = EventListener()
self.new_secret_key_listener = EventListener()
self.new_follow_listener = EventListener()
self.twitter_stream_change_listener = EventListener()
# self.temporary_interest_manager = TemporaryInterestManager()
self.active_download_manager = download_manager
self.tweet_forwarder = tweet_forwarder
self.config = config
def save_squeak(self, squeak: CSqueak) -> Optional[bytes]:
@ -130,7 +137,7 @@ class SqueakController:
# Notify the listener
self.new_secret_key_listener.handle_new_item(squeak)
def make_squeak(self, profile_id: int, content_str: str, replyto_hash: bytes) -> Optional[bytes]:
def make_squeak(self, profile_id: int, content_str: str, replyto_hash: Optional[bytes]) -> Optional[bytes]:
squeak_profile = self.squeak_db.get_profile(profile_id)
squeak, decryption_key = self.squeak_core.make_squeak(
squeak_profile, content_str, replyto_hash)
@ -806,6 +813,9 @@ class SqueakController:
def subscribe_follows(self, stopped: threading.Event):
yield from self.new_follow_listener.yield_items(stopped)
def subscribe_twitter_stream_changes(self, stopped: threading.Event):
yield from self.twitter_stream_change_listener.yield_items(stopped)
def update_subscriptions(self):
locator = self.get_interested_locator()
self.network_manager.update_local_subscriptions(locator)
@ -813,6 +823,11 @@ class SqueakController:
def create_update_subscriptions_event(self):
self.new_follow_listener.handle_new_item(UpdateSubscriptionsEvent())
def create_update_twitter_stream_event(self):
self.twitter_stream_change_listener.handle_new_item(
UpdateTwitterStreamEvent()
)
def subscribe_received_offers_for_squeak(self, squeak_hash: bytes, stopped: threading.Event):
for received_offer in self.new_received_offer_listener.yield_items(stopped):
if received_offer.squeak_hash == squeak_hash:
@ -887,3 +902,43 @@ class SqueakController:
inv_msg = msg_inv(inv=[inv])
peer.send_msg(inv_msg)
logger.debug("Finished checking peers to forward.")
def insert_user_config(self) -> Optional[str]:
user_config = UserConfig(username=self.config.webadmin.username)
return self.squeak_db.insert_config(user_config)
def set_twitter_bearer_token(self, twitter_bearer_token: str) -> None:
self.insert_user_config()
self.squeak_db.set_config_twitter_bearer_token(
username=self.config.webadmin.username,
twitter_bearer_token=twitter_bearer_token,
)
self.create_update_twitter_stream_event()
def get_twitter_bearer_token(self) -> Optional[str]:
user_config = self.squeak_db.get_config(
username=self.config.webadmin.username,
)
if user_config is None:
return None
return user_config.twitter_bearer_token
def add_twitter_account(self, handle: str, profile_id: int) -> Optional[int]:
twitter_account = TwitterAccount(
twitter_account_id=None,
handle=handle,
profile_id=profile_id,
)
account_id = self.squeak_db.insert_twitter_account(twitter_account)
self.create_update_twitter_stream_event()
return account_id
def get_twitter_accounts(self) -> List[TwitterAccountEntry]:
return self.squeak_db.get_twitter_accounts()
def delete_twitter_account(self, twitter_account_id: int) -> None:
self.squeak_db.delete_twitter_account(twitter_account_id)
self.create_update_twitter_stream_event()
def update_twitter_stream(self) -> None:
self.tweet_forwarder.start_processing(self)

View file

@ -39,6 +39,7 @@ from squeaknode.node.active_download_manager import ActiveDownloadManager
from squeaknode.node.payment_processor import PaymentProcessor
from squeaknode.node.peer_connection_worker import PeerConnectionWorker
from squeaknode.node.peer_subscription_update_worker import PeerSubscriptionUpdateWorker
from squeaknode.node.process_forward_tweets_worker import ProcessForwardTweetsWorker
from squeaknode.node.process_received_payments_worker import ProcessReceivedPaymentsWorker
from squeaknode.node.squeak_controller import SqueakController
from squeaknode.node.squeak_deletion_worker import SqueakDeletionWorker
@ -46,6 +47,8 @@ from squeaknode.node.squeak_offer_expiry_worker import SqueakOfferExpiryWorker
from squeaknode.node.update_follows_worker import UpdateFollowsWorker
from squeaknode.node.update_subscribed_secret_key_worker import UpdateSubscribedSecretKeysWorker
from squeaknode.node.update_subscribed_squeak_worker import UpdateSubscribedSqueaksWorker
from squeaknode.node.update_twitter_stream_worker import UpdateTwitterStreamWorker
from squeaknode.twitter.twitter_forwarder import TwitterForwarder
logger = logging.getLogger(__name__)
@ -63,6 +66,7 @@ class SqueakNode:
self.initialize_bitcoin_block_subscription_client()
self.initialize_squeak_core()
self.initialize_payment_processor()
self.initialize_twitter_forwarder()
self.initialize_network_manager()
self.initialize_download_manager()
self.initialize_squeak_controller()
@ -70,12 +74,14 @@ class SqueakNode:
self.initialize_admin_rpc_server()
self.initialize_admin_web_server()
self.initialize_received_payment_processor_worker()
self.initialize_forward_tweets_processor_worker()
self.initialize_peer_connection_worker()
self.initialize_squeak_deletion_worker()
self.initialize_offer_expiry_worker()
self.initialize_new_squeak_worker()
self.initialize_new_secret_key_worker()
self.initialize_new_follow_worker()
self.initialize_twitter_stream_change_worker()
self.initialize_peer_subscription_update_worker()
def start_running(self):
@ -87,12 +93,15 @@ class SqueakNode:
if self.config.webadmin.enabled:
self.admin_web_server.start()
self.received_payment_processor_worker.start_running()
self.forward_tweets_processor_worker.start_running(
self.squeak_controller)
self.peer_connection_worker.start()
self.squeak_deletion_worker.start()
self.offer_expiry_worker.start()
self.new_squeak_worker.start_running()
self.new_secret_key_worker.start_running()
self.new_follow_worker.start_running()
self.twitter_stream_change_worker.start_running()
self.new_bitcoin_block_worker.start_running()
self.download_manager.start(
self.squeak_controller.broadcast_msg,
@ -104,6 +113,7 @@ class SqueakNode:
self.admin_rpc_server.stop()
self.network_manager.stop()
self.received_payment_processor_worker.stop_running()
self.forward_tweets_processor_worker.stop_running()
self.new_squeak_worker.stop_running()
def initialize_network(self):
@ -162,6 +172,11 @@ class SqueakNode:
self.config.node.subscribe_invoices_retry_s,
)
def initialize_twitter_forwarder(self):
self.twitter_forwarder = TwitterForwarder(
self.config.twitter.forward_tweets_retry_s,
)
def initialize_network_manager(self):
self.network_manager = NetworkManager(self.config)
@ -172,6 +187,7 @@ class SqueakNode:
self.payment_processor,
self.network_manager,
self.download_manager,
self.twitter_forwarder,
self.config,
)
@ -205,6 +221,11 @@ class SqueakNode:
self.payment_processor,
)
def initialize_forward_tweets_processor_worker(self):
self.forward_tweets_processor_worker = ProcessForwardTweetsWorker(
self.twitter_forwarder,
)
def initialize_peer_connection_worker(self):
self.peer_connection_worker = PeerConnectionWorker(
self.squeak_controller,
@ -238,6 +259,11 @@ class SqueakNode:
self.squeak_controller,
)
def initialize_twitter_stream_change_worker(self):
self.twitter_stream_change_worker = UpdateTwitterStreamWorker(
self.squeak_controller,
)
def initialize_peer_subscription_update_worker(self):
self.new_bitcoin_block_worker = PeerSubscriptionUpdateWorker(
self.squeak_controller,

View file

@ -0,0 +1,53 @@
# 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.
import logging
import threading
from squeaknode.node.squeak_controller import SqueakController
logger = logging.getLogger(__name__)
class UpdateTwitterStreamWorker:
def __init__(self, squeak_controller: SqueakController):
self.squeak_controller = squeak_controller
self.stopped = threading.Event()
def start_running(self):
threading.Thread(
target=self.handle_update_twitter_stream,
name="update_twitter_stream_thread",
daemon=True,
).start()
def stop_running(self):
self.stopped.set()
def handle_update_twitter_stream(self):
logger.debug("Starting UpdateTwitterStreamWorker...")
for _ in self.squeak_controller.subscribe_twitter_stream_changes(
self.stopped,
):
logger.debug("Handling update twitter stream event.")
self.squeak_controller.update_twitter_stream()

View file

@ -0,0 +1,21 @@
# 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.

View file

@ -0,0 +1,141 @@
# 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.
import logging
import threading
from typing import List
from typing import Optional
from squeaknode.core.twitter_account_entry import TwitterAccountEntry
from squeaknode.node.squeak_controller import SqueakController
from squeaknode.twitter.twitter_stream import TwitterStream
logger = logging.getLogger(__name__)
class TwitterForwarder:
def __init__(
self,
retry_s: int,
):
self.retry_s = retry_s
self.lock = threading.Lock()
self.current_task: Optional[TwitterForwarderTask] = None
def start_processing(self, squeak_controller: SqueakController):
with self.lock:
if self.current_task is not None:
self.current_task.stop_processing()
self.current_task = TwitterForwarderTask(
squeak_controller,
self.retry_s,
)
self.current_task.start_processing()
def stop_processing(self):
with self.lock:
if self.current_task is not None:
self.current_task.stop_processing()
class TwitterForwarderTask:
def __init__(
self,
squeak_controller: SqueakController,
retry_s: int,
):
self.squeak_controller = squeak_controller
self.retry_s = retry_s
self.stopped = threading.Event()
self.tweet_stream = None
def start_processing(self):
logger.info("Starting twitter forwarder task.")
threading.Thread(
target=self.process_forward_tweets,
daemon=True,
).start()
def stop_processing(self):
logger.info("Stopping twitter forwarder task.")
self.stopped.set()
if self.tweet_stream is not None:
self.tweet_stream.cancel_fn()
def process_forward_tweets(self):
while not self.stopped.is_set():
try:
bearer_token = self.get_bearer_token()
handles = self.get_twitter_handles()
if not bearer_token:
return
if not handles:
return
logger.info("Starting forward tweets with bearer token: {} and twitter handles: {}".format(
bearer_token,
handles,
))
twitter_stream = TwitterStream(bearer_token, handles)
self.tweet_stream = twitter_stream.get_tweets()
if self.stopped.is_set():
return
for tweet in self.tweet_stream.result_stream:
self.handle_tweet(tweet)
# TODO: use more specific error.
except Exception:
logger.exception(
"Unable to subscribe tweet stream. Retrying in {} seconds...".format(
self.retry_s,
),
)
self.stopped.wait(self.retry_s)
def get_bearer_token(self) -> str:
return self.squeak_controller.get_twitter_bearer_token() or ''
def get_twitter_handles(self) -> List[str]:
twitter_accounts = self.squeak_controller.get_twitter_accounts()
handles = [account.handle for account in twitter_accounts]
return handles
def is_tweet_a_match(self, tweet: dict, account: TwitterAccountEntry) -> bool:
for rule in tweet['matching_rules']:
if rule['tag'] == account.handle:
return True
return False
def forward_tweet(self, tweet: dict, account: TwitterAccountEntry) -> None:
self.squeak_controller.make_squeak(
profile_id=account.profile_id,
content_str=tweet['data']['text'],
replyto_hash=None,
)
def handle_tweet(self, tweet: dict):
logger.info(
"Got tweet: {}".format(tweet))
twitter_accounts = self.squeak_controller.get_twitter_accounts()
for account in twitter_accounts:
if self.is_tweet_a_match(tweet, account):
self.forward_tweet(tweet, account)

View file

@ -0,0 +1,128 @@
# 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.
import json
from typing import List
import requests
from squeaknode.core.tweet_stream import TweetStream
class TwitterStream:
TWITTER_STREAM_URL = "https://api.twitter.com/2/tweets/search/stream"
TWITTER_STREAM_RULES_URL = "https://api.twitter.com/2/tweets/search/stream/rules"
def __init__(self, bearer_token: str, handles: List[str]):
self.bearer_token = bearer_token
self.handles = handles
def get_tweets(self) -> TweetStream:
rules = self.get_rules()
delete = self.delete_all_rules(rules)
set = self.set_rules(delete)
return self.get_stream(set)
@property
def bearer_oauth_fn(self):
def bearer_oauth(r):
"""
Method required by bearer token authentication.
"""
r.headers["Authorization"] = f"Bearer {self.bearer_token}"
r.headers["User-Agent"] = "v2FilteredStreamPython"
return r
return bearer_oauth
def get_rules(self):
response = requests.get(
self.TWITTER_STREAM_RULES_URL,
auth=self.bearer_oauth_fn
)
if response.status_code != 200:
raise Exception(
"Cannot get rules (HTTP {}): {}".format(
response.status_code, response.text)
)
print(json.dumps(response.json()))
return response.json()
def delete_all_rules(self, rules):
if rules is None or "data" not in rules:
return None
ids = list(map(lambda rule: rule["id"], rules["data"]))
payload = {"delete": {"ids": ids}}
response = requests.post(
self.TWITTER_STREAM_RULES_URL,
auth=self.bearer_oauth_fn,
json=payload
)
if response.status_code != 200:
raise Exception(
"Cannot delete rules (HTTP {}): {}".format(
response.status_code, response.text
)
)
print(json.dumps(response.json()))
def set_rules(self, delete):
sample_rules = [
{"value": f"from:{handle}", "tag": handle}
for handle in self.handles
]
payload = {"add": sample_rules}
response = requests.post(
self.TWITTER_STREAM_RULES_URL,
auth=self.bearer_oauth_fn,
json=payload,
)
if response.status_code != 201:
raise Exception(
"Cannot add rules (HTTP {}): {}".format(
response.status_code, response.text)
)
print(json.dumps(response.json()))
def get_stream(self, set) -> TweetStream:
response = requests.get(
self.TWITTER_STREAM_URL,
auth=self.bearer_oauth_fn,
stream=True,
)
print(response.status_code)
if response.status_code != 200:
raise Exception(
"Cannot get stream (HTTP {}): {}".format(
response.status_code, response.text
)
)
result_stream = (
json.loads(response_line)
for response_line in response.iter_lines()
if response_line
)
return TweetStream(
cancel_fn=response.close,
result_stream=result_stream,
)

View file

@ -44,6 +44,7 @@ from squeaknode.core.squeak_entry import SqueakEntry
from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeaks import get_hash
from squeaknode.core.squeaks import make_squeak_with_block
from squeaknode.core.user_config import UserConfig
from squeaknode.network.peer import Peer
from tests.utils import gen_contact_profile
from tests.utils import gen_signing_profile
@ -529,3 +530,15 @@ def download_result():
elapsed_time_ms=56789,
number_peers=4,
)
@pytest.fixture
def user_config():
yield UserConfig(
username="default_user",
)
@pytest.fixture
def twitter_bearer_token():
yield 'abcdefg987654321'

View file

@ -25,6 +25,7 @@ import mock
import pytest
from sqlalchemy import create_engine
from squeaknode.core.twitter_account import TwitterAccount
from squeaknode.db.exception import SqueakDatabaseError
from squeaknode.db.squeak_db import SqueakDb
from tests.utils import gen_address
@ -398,6 +399,57 @@ def inserted_received_payment_ids(
yield ret
@pytest.fixture
def inserted_user_config_username(squeak_db, user_config):
yield squeak_db.insert_config(user_config)
@pytest.fixture
def duplicate_inserted_user_config_username(squeak_db, user_config, inserted_user_config_username):
yield squeak_db.insert_config(user_config)
@pytest.fixture
def user_config_with_twitter_bearer_token_username(
squeak_db,
user_config,
inserted_user_config_username,
twitter_bearer_token,
):
squeak_db.set_config_twitter_bearer_token(
inserted_user_config_username,
twitter_bearer_token,
)
yield inserted_user_config_username
@pytest.fixture
def twitter_account(inserted_signing_profile_id):
yield TwitterAccount(
twitter_account_id=None,
handle="fake_twitter_handle",
profile_id=inserted_signing_profile_id,
)
@pytest.fixture
def inserted_twitter_account_id(squeak_db, twitter_account):
yield squeak_db.insert_twitter_account(twitter_account)
@pytest.fixture
def duplicate_inserted_twitter_account_id(squeak_db, twitter_account, inserted_twitter_account_id):
yield squeak_db.insert_twitter_account(twitter_account)
@pytest.fixture
def deleted_twitter_account_id(squeak_db, inserted_twitter_account_id):
squeak_db.delete_twitter_account(
inserted_twitter_account_id,
)
yield inserted_twitter_account_id
def test_init_with_retries(squeak_db):
with mock.patch.object(squeak_db, 'init', autospec=True) as mock_init, \
mock.patch('squeaknode.db.squeak_db.time.sleep', autospec=True) as mock_sleep:
@ -1527,3 +1579,54 @@ def test_get_sent_payment_summary(squeak_db, inserted_sent_payment_ids, price_ms
inserted_sent_payment_ids)
assert sent_payment_summary.total_amount_sent_msat == price_msat * \
len(inserted_sent_payment_ids)
def test_get_config(squeak_db, user_config, inserted_user_config_username):
retrieved_config = squeak_db.get_config(inserted_user_config_username)
assert retrieved_config == user_config
def test_duplicate_inserted_config(squeak_db, duplicate_inserted_user_config_username):
assert duplicate_inserted_user_config_username is None
def test_get_config_missing(squeak_db):
retrieved_config = squeak_db.get_config("fake_username")
assert retrieved_config is None
def test_set_twitter_bearer_token(
squeak_db,
user_config_with_twitter_bearer_token_username,
twitter_bearer_token,
):
retrieved_config = squeak_db.get_config(
user_config_with_twitter_bearer_token_username)
assert retrieved_config.twitter_bearer_token == twitter_bearer_token
def test_get_twitter_account(
squeak_db,
twitter_account,
inserted_twitter_account_id,
signing_profile,
):
retrieved_twitter_accounts = squeak_db.get_twitter_accounts()
assert retrieved_twitter_accounts[0].handle == twitter_account.handle
assert retrieved_twitter_accounts[0].profile_id == twitter_account.profile_id
assert retrieved_twitter_accounts[0].profile._replace(profile_id=None) == \
signing_profile
def test_duplicate_twitter_account(squeak_db, duplicate_inserted_twitter_account_id):
assert duplicate_inserted_twitter_account_id is None
def test_get_twitter_account_all_deleted(squeak_db, twitter_account, deleted_twitter_account_id):
retrieved_twitter_accounts = squeak_db.get_twitter_accounts()
assert len(retrieved_twitter_accounts) == 0

View file

@ -33,6 +33,7 @@ from squeaknode.network.network_manager import NetworkManager
from squeaknode.node.active_download_manager import ActiveDownloadManager
from squeaknode.node.payment_processor import PaymentProcessor
from squeaknode.node.squeak_controller import SqueakController
from squeaknode.twitter.twitter_forwarder import TwitterForwarder
@pytest.fixture
@ -105,6 +106,11 @@ def download_manager():
return mock.Mock(spec=ActiveDownloadManager)
@pytest.fixture
def twitter_forwarder():
return mock.Mock(spec=TwitterForwarder)
@pytest.fixture
def squeak_controller(
squeak_db,
@ -112,6 +118,7 @@ def squeak_controller(
payment_processor,
network_manager,
download_manager,
twitter_forwarder,
config,
):
return SqueakController(
@ -120,6 +127,7 @@ def squeak_controller(
payment_processor,
network_manager,
download_manager,
twitter_forwarder,
config,
)
@ -131,6 +139,7 @@ def regtest_squeak_controller(
payment_processor,
network_manager,
download_manager,
twitter_forwarder,
regtest_config,
):
return SqueakController(
@ -139,6 +148,7 @@ def regtest_squeak_controller(
payment_processor,
network_manager,
download_manager,
twitter_forwarder,
regtest_config,
)