Skip to content

Commit

Permalink
Merge pull request #2430 from StateVoicesNational/stage-main-14.0.2
Browse files Browse the repository at this point in the history
Stage main 14.0.2
  • Loading branch information
engelhartrueben authored Aug 21, 2024
2 parents 58c5562 + 1a54275 commit adb6064
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 57 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Spoke was created by Saikat Chakrabarti and Sheena Pakanati.

On November 19th, 2023, the repo Spoke was transfered from MoveOn to StateVoices.

The latest version is [14.0.1](https://github.com/StateVoicesNational/Spoke/tree/v14.0.1) (see [release notes](https://github.com/StateVoicesNational/Spoke/blob/main/docs/RELEASE_NOTES.md#v1401))
The latest version is [14.0.2](https://github.com/StateVoicesNational/Spoke/tree/v14.0.2) (see [release notes](https://github.com/StateVoicesNational/Spoke/blob/main/docs/RELEASE_NOTES.md#v1402))


## Setting up Spoke
Expand All @@ -25,7 +25,7 @@ Want to know more?
### Quick Start with Heroku
This version of Spoke suitable for testing and, potentially, for small campaigns. This won't cost any money and will not support production(aka large-scale) usage. It's a great way to practice deploying Spoke or see it in action.

<a href="https://heroku.com/deploy?template=https://github.com/StateVoicesNational/Spoke/tree/v14.0.1">
<a href="https://heroku.com/deploy?template=https://github.com/StateVoicesNational/Spoke/tree/v14.0.2">

<img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
</a>
Expand Down
13 changes: 13 additions & 0 deletions docs/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Release Notes

## v14.0.2
_August 2024:_ Version 14.0.2

14.0.2 is a patch release.

### Bug Fixes
- [#2410](https://github.com/StateVoicesNational/Spoke/pull/2410) - Data exports bug
- [#2424](https://github.com/StateVoicesNational/Spoke/pull/2424) - Reset Password Bug
- [#2425](https://github.com/StateVoicesNational/Spoke/pull/2425) - Message Review Reassignment Bug

### Appreciations
[Maureen Zitouni](https://github.com/mau11), [Ruby Engelhart](https://github.com/engelhartrueben)

## v14.0.1
_July 2024:_ Version 14.0.1

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "spoke",
"version": "14.0.1",
"version": "14.0.2",
"description": "Spoke",
"main": "src/server",
"engines": {
Expand Down
35 changes: 18 additions & 17 deletions src/containers/AdminCampaignStats.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,8 @@ class AdminCampaignStats extends React.Component {
{campaign.exportResults.error && (
<div>Export failed: {campaign.exportResults.error}</div>
)}
{campaign.exportResults.campaignExportUrl &&
campaign.exportResults.campaignExportUrl.startsWith("http") ? (
{campaign.exportResults.campaignExportUrl && (
(campaign.exportResults.campaignExportUrl.startsWith("http")) ? (
<div>
Most recent export:
<a href={campaign.exportResults.campaignExportUrl} download>
Expand All @@ -360,15 +360,16 @@ class AdminCampaignStats extends React.Component {
Messages Export CSV
</a>
</div>
) : (
<div>
Local export was successful, saved on the server at:
<br />
{campaign.exportResults.campaignExportUrl}
<br />
{campaign.exportResults.campaignMessagesExportUrl}
</div>
)}
) : (campaign.exportResults.campaignExportUrl.startsWith("file://") && (
<div>
Local export was successful, saved on the server at:
<br />
{campaign.exportResults.campaignExportUrl}
<br />
{campaign.exportResults.campaignMessagesExportUrl}
</div>
)
))}
</div>
)}
{campaign.joinToken && campaign.useDynamicAssignment && (
Expand Down Expand Up @@ -424,21 +425,21 @@ class AdminCampaignStats extends React.Component {
message={
<span>
Export started -
{this.props.organizationData &&
this.props.organizationData.emailEnabled &&
" we'll e-mail you when it's done. "}
{campaign.cacheable && (
{(this.props.organizationData &&
this.props.organizationData.organization.emailEnabled) ?
" we'll e-mail you when it's done. " :
(campaign.cacheable && (
<span>
<Link
onClick={() => {
this.props.data.refetch();
}}
>
Reload the page
{" Reload the page"} {/*Hacky way to add a space at the beginning */}
</Link>{" "}
to see a download link when its ready.
</span>
)}
))}
</span>
}
autoHideDuration={campaign.cacheable ? null : 5000}
Expand Down
16 changes: 14 additions & 2 deletions src/containers/AdminIncomingMessageList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export class AdminIncomingMessageList extends Component {
props.location.query,
props.organization.organization.tags
);
// Make sure campaignIds is an array of numbers
filters.campaignsFilter = {
campaignIds: filters.campaignsFilter.campaignIds?.map(id => Number(id))
};

this.state = {
page: 0,
pageSize: 10,
Expand Down Expand Up @@ -207,10 +212,17 @@ export class AdminIncomingMessageList extends Component {
};

handleReassignRequested = async newTexterUserId => {
const updatedCampaignIdsContactIds = this.state.campaignIdsContactIds.map(
campaign => {
campaign.campaignContactId = Number(campaign.campaignContactId);
campaign.messageIds = campaign.messageIds.map(id => Number(id));
return campaign;
}
);
await this.props.mutations.reassignCampaignContacts(
this.props.params.organizationId,
this.state.campaignIdsContactIds,
newTexterUserId
updatedCampaignIdsContactIds,
newTexterUserId.toString()
);
this.setState({
utc: Date.now().toString(),
Expand Down
4 changes: 2 additions & 2 deletions src/containers/PeopleList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ export class PeopleList extends Component {
};

renderChangePasswordButton = (value, tableMeta) => {
const texterId = tableMeta.rowData[0];
const texterId = Number(tableMeta.rowData[0]);
const { currentUser } = this.props;
return (
<Button
disabled={currentUser.id === texterId}
disabled={currentUser.id == texterId}
onClick={() => {
this.resetPassword(texterId);
}}
Expand Down
2 changes: 1 addition & 1 deletion src/server/models/cacheable_queries/campaign.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ const campaignCache = {
await r.redis
.MULTI()
.SET(exportCacheKey, JSON.stringify(data))
.EXPIRE(exportCacheKey, 43200)
.EXPIRE(exportCacheKey, 86400)
.exec();
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/server/models/cacheable_queries/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const dbLoadUserRoles = async userId => {
await r.redis
.MULTI()
.DEL(key)
.HSET(key, ...mappedHighestRoles)
.HSET(key, mappedHighestRoles)
.exec();
} else {
await r.redis.DEL(key);
Expand Down
129 changes: 98 additions & 31 deletions src/workers/jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ import { rawIngestMethod } from "../extensions/contact-loaders";

import { Lambda } from "@aws-sdk/client-lambda";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { GetObjectCommand, S3 } from "@aws-sdk/client-s3";
import {
CreateBucketCommand,
HeadBucketCommand,
GetObjectCommand,
waitUntilBucketExists,
S3Client,
PutObjectCommand
} from "@aws-sdk/client-s3";
import { SQS } from "@aws-sdk/client-sqs";
import Papa from "papaparse";
import moment from "moment";
Expand Down Expand Up @@ -861,46 +868,106 @@ export async function exportCampaign(job) {
(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)
) {
try {
const s3bucket = new S3({
// The transformation for params is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for params.
params: { Bucket: process.env.AWS_S3_BUCKET_NAME }
const client = new S3Client({
region: process.env.AWS_REGION
});
const bucketName = process.env.AWS_S3_BUCKET_NAME;

try {
// Check if the S3 bucket already exists
const verifyBucketCommand = new HeadBucketCommand({
Bucket: bucketName
});
await client.send(verifyBucketCommand);

console.log(`S3 bucket "${bucketName}" already exists.`);
} catch (error) {
if (error.name === "NotFound") {
console.log(
`S3 bucket "${bucketName}" not found. Creating a new bucket.`
);

try {
// Create the S3 bucket
const createBucketCommand = new CreateBucketCommand({
Bucket: bucketName
});
await client.send(createBucketCommand);

console.log(`S3 bucket "${bucketName}" created successfully.`);
} catch (createError) {
console.error(
`Error creating bucket "${bucketName}":`,
createError
);
}
} else {
console.error("Error checking bucket existence:", error);
}
}

// verifies that the bucket exists before moving forward
// if for some reason this fails, Spoke defensively deletes the job
await waitUntilBucketExists(
{ client, maxWaitTime: 15 },
{ Bucket: bucketName }
);

const campaignTitle = campaign.title
.replace(/ /g, "_")
.replace(/\//g, "_");
const key = `${campaignTitle}-${moment().format(
"YYYY-MM-DD-HH-mm-ss"
)}.csv`;
const messageKey = `${key}-messages.csv`;
let params = { Key: key, Body: campaignCsv };
await s3bucket.putObject(params);
params = { Key: key, Expires: 86400 };
const campaignExportUrl = await await getSignedUrl(s3bucket, new GetObjectCommand(params), {
expiresIn: "/* add value from 'Expires' from v2 call if present, else remove */"
});
params = { Key: messageKey, Body: messageCsv };
await s3bucket.putObject(params);
params = { Key: messageKey, Expires: 86400 };
const campaignMessagesExportUrl = await await getSignedUrl(s3bucket, new GetObjectCommand(params), {
expiresIn: "/* add value from 'Expires' from v2 call if present, else remove */"
});
let params = { Key: key,
Body: campaignCsv,
Bucket: bucketName };
await client.send(new PutObjectCommand(params));
params = { Key: key,
Expires: 86400,
Bucket: bucketName };
const campaignExportUrl = await getSignedUrl(client, new GetObjectCommand(params));
params = { Key: messageKey,
Body: messageCsv,
Bucket: bucketName };
await client.send(new PutObjectCommand(params));
params = { Key: messageKey,
Expires: 86400,
Bucket: bucketName };
const campaignMessagesExportUrl = await getSignedUrl(client, new GetObjectCommand(params));
exportResults.campaignExportUrl = campaignExportUrl;
exportResults.campaignMessagesExportUrl = campaignMessagesExportUrl;

await sendEmail({
to: user.email,
subject: `Export ready for ${campaign.title}`,
text: `Your Spoke exports are ready! These URLs will be valid for 24 hours.
Campaign export: ${campaignExportUrl}
Message export: ${campaignMessagesExportUrl}`
}).catch(err => {
log.error(err);
log.info(`Campaign Export URL - ${campaignExportUrl}`);
log.info(`Campaign Messages Export URL - ${campaignMessagesExportUrl}`);
});
log.info(`Successfully exported ${id}`);
// extreme check on email set-up
if ((
process.env.EMAIL_FROM &&
process.env.EMAIL_HOST &&
process.env.EMAIL_HOST_PASSWORD &&
process.env.EMAIL_HOST_PORT &&
process.env.EMAIL_HOST_USER) ||
(
process.env.MAILGUN_DOMAIN &&
process.env.MAILGUN_SMTP_LOGIN &&
process.env.MAILGUN_SMTP_PASSWORD &&
process.env.MAILGUN_SMTP_PORT &&
process.env.MAILGUN_SMTP_SERVER &&
process.env.MAILGUN_PUBLIC_KEY
)
) {
await sendEmail({
to: user.email,
subject: `Export ready for ${campaign.title}`,
text: `Your Spoke exports are ready! These URLs will be valid for 24 hours.
Campaign export: ${campaignExportUrl}
Message export: ${campaignMessagesExportUrl}`
}).catch(err => {
log.error(err);
log.info(`Campaign Export URL - ${campaignExportUrl}`);
log.info(`Campaign Messages Export URL - ${campaignMessagesExportUrl}`);
});
log.info(`Successfully exported ${id}`);
}
} catch (err) {
log.error(err);
exportResults.error = err.message;
Expand All @@ -927,7 +994,7 @@ export async function exportCampaign(job) {
log.debug(campaignCsv);
log.debug(messageCsv);
}
if (exportResults.campaignExportUrl) {
if (exportResults.campaignExportUrl || exportResults.error) {
exportResults.createdAt = String(new Date());
await cacheableData.campaign.saveExportData(campaign.id, exportResults);
}
Expand Down

0 comments on commit adb6064

Please sign in to comment.