mirror of
https://github.com/yzernik/squeaknode.git
synced 2026-08-19 13:18:26 +02:00
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:
parent
348dd1164c
commit
6c40891ad8
60 changed files with 2336 additions and 19696 deletions
19722
frontend/package-lock.json
generated
19722
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "AddTwitterAccountDialog",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "AddTwitterAccountDialog.js"
|
||||
}
|
||||
43
frontend/src/components/AddTwitterAccountDialog/styles.js
Normal file
43
frontend/src/components/AddTwitterAccountDialog/styles.js
Normal 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)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "DeleteTwitterAccountDialog",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "DeleteTwitterAccountDialog.js"
|
||||
}
|
||||
43
frontend/src/components/DeleteTwitterAccountDialog/styles.js
Normal file
43
frontend/src/components/DeleteTwitterAccountDialog/styles.js
Normal 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)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "SetBearerTokenDialog",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "SetBearerTokenDialog.js"
|
||||
}
|
||||
43
frontend/src/components/SetBearerTokenDialog/styles.js
Normal file
43
frontend/src/components/SetBearerTokenDialog/styles.js
Normal 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)',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "TwitterAccountListItem",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "TwitterAccountListItem.js"
|
||||
}
|
||||
0
frontend/src/components/TwitterAccountListItem/styles.js
Normal file
0
frontend/src/components/TwitterAccountListItem/styles.js
Normal file
276
frontend/src/pages/twitter/Twitter.js
Normal file
276
frontend/src/pages/twitter/Twitter.js
Normal 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()}
|
||||
< />
|
||||
);
|
||||
}
|
||||
6
frontend/src/pages/twitter/package.json
Normal file
6
frontend/src/pages/twitter/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Twitter",
|
||||
"version": "0.0.0",
|
||||
"main": "Twitter.js",
|
||||
"private": true
|
||||
}
|
||||
9
frontend/src/pages/twitter/styles.js
Normal file
9
frontend/src/pages/twitter/styles.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export default makeStyles((theme) => ({
|
||||
root: {
|
||||
'& > *': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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",
|
||||
|
|
@ -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
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
|
|
@ -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):
|
||||
|
|
|
|||
30
squeaknode/core/tweet_stream.py
Normal file
30
squeaknode/core/tweet_stream.py
Normal 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]
|
||||
30
squeaknode/core/twitter_account.py
Normal file
30
squeaknode/core/twitter_account.py
Normal 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
|
||||
33
squeaknode/core/twitter_account_entry.py
Normal file
33
squeaknode/core/twitter_account_entry.py
Normal 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]
|
||||
25
squeaknode/core/update_twitter_stream_event.py
Normal file
25
squeaknode/core/update_twitter_stream_event.py
Normal 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."""
|
||||
29
squeaknode/core/user_config.py
Normal file
29
squeaknode/core/user_config.py
Normal 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
|
||||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
51
squeaknode/node/process_forward_tweets_worker.py
Normal file
51
squeaknode/node/process_forward_tweets_worker.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
53
squeaknode/node/update_twitter_stream_worker.py
Normal file
53
squeaknode/node/update_twitter_stream_worker.py
Normal 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()
|
||||
21
squeaknode/twitter/__init__.py
Normal file
21
squeaknode/twitter/__init__.py
Normal 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.
|
||||
141
squeaknode/twitter/twitter_forwarder.py
Normal file
141
squeaknode/twitter/twitter_forwarder.py
Normal 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)
|
||||
128
squeaknode/twitter/twitter_stream.py
Normal file
128
squeaknode/twitter/twitter_stream.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue