Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/join-block/join.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* Plugin Name: Common Knowledge Join Flow
* Description: Common Knowledge join flow plugin.
* Version: 1.4.37
* Version: 1.4.39
* Author: Common Knowledge <hello@commonknowledge.coop>
* Text Domain: common-knowledge-join-flow
* License: GPLv2 or later
Expand Down
4 changes: 3 additions & 1 deletion packages/join-block/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Tags: membership, subscription, join
Contributors: commonknowledgecoop
Requires at least: 5.4
Tested up to: 7.0
Stable tag: 1.4.37
Stable tag: 1.4.39
Requires PHP: 8.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Expand Down Expand Up @@ -107,6 +107,8 @@ Need help? Contact us at [hello@commonknowledge.coop](mailto:hello@commonknowled

== Changelog ==

= 1.4.39 =
* Add Zetkin people listing and Zetkin and Mailchimp tag helpers that report status instead of throwing, for bulk maintenance jobs
= 1.4.37 =
* Add "cancelled" tag option, distinguishing this from lapsed membership
= 1.4.36 =
Expand Down
121 changes: 84 additions & 37 deletions packages/join-block/src/Services/MailchimpService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

class MailchimpService
{
// Outcomes of a tag write, returned by tryAddTag and tryRemoveTag.
public const TAG_OK = 'ok';
public const TAG_NOT_FOUND = 'not_found';
public const TAG_NOT_CONFIGURED = 'not_configured';
public const TAG_ERROR = 'error';

public static function buildMergeFields(array $data): array
{
if ($data['isUpdateFlow']) {
Expand Down Expand Up @@ -146,7 +152,6 @@ public static function signup($data)
// For new members, we need to remove tags via updateListMemberTags (can't do it in addListMember)
if ($memberExists || !empty($removeTags)) {
try {
$subscriberHash = md5(strtolower($email));
$tagUpdates = [];

// If member exists, add tags that weren't added during creation
Expand All @@ -164,11 +169,7 @@ public static function signup($data)
}

if (!empty($tagUpdates)) {
$mailchimp->lists->updateListMemberTags(
$mailchimp_audience_id,
$subscriberHash,
["tags" => $tagUpdates]
);
self::updateMemberTags($email, $tagUpdates, $mailchimp);
$joinBlockLog->info("Updated tags for $email in Mailchimp");
}
} catch (\GuzzleHttp\Exception\ClientException $e) {
Expand Down Expand Up @@ -235,11 +236,11 @@ public static function updateMember($email, $mergeFields, $previousEmail = null)
* @param string $email
* @return bool
*/
public static function memberExists($email)
public static function memberExists($email, $client = null)
{
global $joinBlockLog;

$mailchimp = self::getClient();
$mailchimp = $client ?? self::getClient();
$mailchimp_audience_id = Settings::get("MAILCHIMP_AUDIENCE_ID");
$subscriberHash = md5(strtolower($email));

Expand All @@ -255,57 +256,103 @@ public static function memberExists($email)
}
}

public static function addTag($email, $tag)
public static function isConfigured()
{
global $joinBlockLog;
return !empty(Settings::get("MAILCHIMP_API_KEY"))
&& !empty(Settings::get("MAILCHIMP_AUDIENCE_ID"));
}

if (!self::memberExists($email)) {
$joinBlockLog->warning("Skipping Mailchimp addTag('$tag') for $email: member does not exist");
// The one place a Mailchimp tag write is built. Does not catch: callers
// pick their own error policy.
private static function updateMemberTags($email, array $tagUpdates, $client = null)
{
if (empty($tagUpdates)) {
return;
}

$mailchimp = self::getClient();
$client = $client ?? self::getClient();
$mailchimp_audience_id = Settings::get("MAILCHIMP_AUDIENCE_ID");

$subscriberHash = md5(strtolower($email));

$client->lists->updateListMemberTags(
$mailchimp_audience_id,
$subscriberHash,
["tags" => $tagUpdates]
);
}

// Reporting counterparts to addTag and removeTag: return a TAG_* status
// rather than throwing, so a bulk run can carry on and account for it.
public static function tryAddTag($email, $tag, $client = null)
{
return self::trySetTag($email, $tag, 'active', $client);
}

public static function tryRemoveTag($email, $tag, $client = null)
{
return self::trySetTag($email, $tag, 'inactive', $client);
}

// Mailchimp has no separate remove call; a tag is switched between active
// and inactive. A member missing from the audience comes back as a 404,
// which is reportable rather than a failure.
private static function trySetTag($email, $tag, $status, $client = null)
{
global $joinBlockLog;

if (!self::isConfigured()) {
return self::TAG_NOT_CONFIGURED;
}

try {
$mailchimp->lists->updateListMemberTags(
$mailchimp_audience_id,
$subscriberHash,
["tags" => [["name" => $tag, "status" => "active"]]]
);
$joinBlockLog->info("Added tag '$tag' to $email in Mailchimp");
self::updateMemberTags($email, [["name" => $tag, "status" => $status]], $client);
return self::TAG_OK;
} catch (\GuzzleHttp\Exception\ClientException $e) {
$joinBlockLog->error("Failed to add tag '$tag' to $email in Mailchimp: " . $e->getMessage());
throw $e;
$response = $e->getResponse();
$body = $response ? $response->getBody()->getContents() : $e->getMessage();

if (($response && $response->getStatusCode() === 404) || str_contains($body, "Resource Not Found")) {
return self::TAG_NOT_FOUND;
}

$joinBlockLog->error(
"Mailchimp rejected setting tag '$tag' to $status for $email: " . $body
);
return self::TAG_ERROR;
} catch (\Throwable $e) {
$joinBlockLog->error(
"Could not reach Mailchimp to set tag '$tag' to $status for $email: " . $e->getMessage()
);
return self::TAG_ERROR;
}
}

public static function removeTag($email, $tag)
// Throwing counterpart to trySetTag, for callers that want an exception.
private static function setTagOrThrow($email, $tag, $status, $client = null)
{
global $joinBlockLog;

if (!self::memberExists($email)) {
$joinBlockLog->warning("Skipping Mailchimp removeTag('$tag') for $email: member does not exist");
if (!self::memberExists($email, $client)) {
$joinBlockLog->warning("Skipping Mailchimp tag update for $email: member does not exist");
return;
}

$mailchimp = self::getClient();
$mailchimp_audience_id = Settings::get("MAILCHIMP_AUDIENCE_ID");

$subscriberHash = md5(strtolower($email));

try {
$mailchimp->lists->updateListMemberTags(
$mailchimp_audience_id,
$subscriberHash,
["tags" => [["name" => $tag, "status" => "inactive"]]]
);
$joinBlockLog->info("Removed tag '$tag' from $email in Mailchimp");
self::updateMemberTags($email, [["name" => $tag, "status" => $status]], $client);
$joinBlockLog->info("Set Mailchimp tag '$tag' to $status for $email");
} catch (\GuzzleHttp\Exception\ClientException $e) {
$joinBlockLog->error("Failed to remove tag '$tag' from $email in Mailchimp: " . $e->getMessage());
$joinBlockLog->error("Failed to set Mailchimp tag '$tag' to $status for $email: " . $e->getMessage());
throw $e;
}
}

public static function addTag($email, $tag, $client = null)
{
self::setTagOrThrow($email, $tag, 'active', $client);
}

public static function removeTag($email, $tag, $client = null)
{
self::setTagOrThrow($email, $tag, 'inactive', $client);
}
}
Loading
Loading