1
0
mirror of https://github.com/beestat/app.git synced 2025-05-23 18:04:14 -04:00

Fixed #266 - Migrate from MailChimp to MailGun

This commit is contained in:
Jon Ziebell 2020-06-21 22:10:38 -04:00
parent f909470b5d
commit d0ce15606b
17 changed files with 356 additions and 152 deletions

View File

@ -111,19 +111,26 @@ final class setting {
'beestat_root_uri' => '',
/**
* Your Mailchimp API Key; provided to you when you create a Mailchimp
* developer account.
* Your Mailgun API Key.
*
* Example: hcU74TJgGS5k7vuw3NSzkRMSWNPkv8Af-us18
* Example: 4b34e48e768fa45c4a6ac65dd4cf1da9-7e28d3c3-61713777
*/
'mailchimp_api_key' => '',
'mailgun_api_key' => '',
/**
* ID of the mailing list to send emails to.
* API base URL including the sending domain. Make sure to include the
* trailing slash.
*
* Example: uw3NSzkRMS
* Example: https://api.mailgun.net/v3/
*/
'mailchimp_list_id' => '',
'mailgun_base_url' => '',
/**
* The specific newsletter to subscribe users to.
*
* Example: newsletter@app.beestat.io
*/
'mailgun_newsletter' => '',
/**
* Auth ID for Smarty Streets address verification.

View File

@ -60,13 +60,9 @@ class ecobee extends external_api {
);
$identifiers = [];
$email_addresses = [];
foreach($response['thermostatList'] as $thermostat) {
$runtime = $thermostat['runtime'];
$identifiers[] = $thermostat['identifier'];
$notification_settings = $thermostat['notificationSettings'];
$email_addresses = array_merge($email_addresses, $notification_settings['emailAddresses']);
}
// Look to see if any of the returned thermostats exist. This does not use
@ -119,22 +115,6 @@ class ecobee extends external_api {
else {
$this->api('user', 'create_anonymous_user');
$this->api('ecobee_token', 'create', ['attributes' => $ecobee_token]);
if(count($email_addresses) > 0) {
try {
$this->api(
'mailchimp',
'subscribe',
[
'email_address' => $email_addresses[0]
]
);
} catch(Exception $e) {
// Ignore failed subscribe exceptions since it's not critical to the
// success of this. Everything is logged regardless.
}
}
}
// Redirect to the proper location.

View File

@ -99,6 +99,7 @@ class ecobee_thermostat extends cora\crud {
// Loop over the returned thermostats and create/update them as necessary.
$thermostat_ids_to_keep = [];
$email_addresses = [];
foreach($response['thermostatList'] as $api_thermostat) {
$ecobee_thermostat = $this->get(
[
@ -169,6 +170,12 @@ class ecobee_thermostat extends cora\crud {
]
);
foreach($api_thermostat['notificationSettings']['emailAddresses'] as $email_address) {
if(preg_match('/.+@.+\..+/', $email_address) === 1) {
$email_addresses[] = trim(strtolower($email_address));
}
}
// Grab a bunch of attributes from the ecobee_thermostat and attach them
// to the thermostat.
$attributes = [];
@ -260,6 +267,19 @@ class ecobee_thermostat extends cora\crud {
);
}
// Update the email_address on the user.
if(count($email_addresses) > 0) {
$email_address_counts = array_count_values($email_addresses);
arsort($email_address_counts);
$email_address = array_keys(array_slice($email_address_counts, 0, 1, true))[0];
$this->api('user', 'update', [
'attributes' => [
'user_id' => $this->session->get_user_id(),
'email_address' => $email_address
]
]);
}
// Inactivate any ecobee_thermostats that were no longer returned.
$thermostats = $this->api('thermostat', 'read');
$ecobee_thermostat_ids_to_return = [];

View File

@ -1,7 +1,7 @@
<?php
/**
* All external APIs (ecobee, SmartyStreets, Patreon, MailChimp) extend this
* All external APIs (ecobee, SmartyStreets, Patreon, Mailgun) extend this
* class. This provides a generic cURL function with a couple basic arguments,
* and also logging.
*
@ -88,7 +88,10 @@ class external_api extends cora\api {
$this->curl_info = curl_getinfo($curl_handle);
if($curl_response === false || curl_errno($curl_handle) !== 0) {
if(
$curl_response === false ||
curl_errno($curl_handle) !== 0
) {
// Error logging
if($this::$log_mysql === 'all' || $this::$log_mysql === 'error') {
$this->log_mysql($curl_response, true);
@ -103,7 +106,6 @@ class external_api extends cora\api {
'curl_error' => curl_error($curl_handle)
]
);
}
// General (success) logging

View File

@ -1,80 +0,0 @@
<?php
/**
* High level functionality for interacting with the Mailchimp API.
*
* @author Jon Ziebell
*/
class mailchimp extends external_api {
protected static $log_mysql = 'all';
protected static $cache = false;
protected static $cache_for = null;
/**
* Send an API call off to MailChimp
*
* @param string $method HTTP Method.
* @param string $endpoint API Endpoint.
* @param array $data API request data.
*
* @throws Exception If MailChimp did not return valid JSON.
*
* @return array The MailChimp response.
*/
private function mailchimp_api($method, $endpoint, $data) {
$curl_response = $this->curl([
'url' => 'https://us18.api.mailchimp.com/3.0/' . $endpoint,
'post_fields' => json_encode($data, JSON_FORCE_OBJECT),
'method' => $method,
'header' => [
'Authorization: Basic ' . base64_encode(':' . $this->setting->get('mailchimp_api_key')),
'Content-Type: application/x-www-form-urlencoded'
]
]);
$response = json_decode($curl_response, true);
if ($response === null) {
throw new Exception('Invalid JSON');
}
return $response;
}
/**
* Subscribe an email address to the mailing list. This will only mark you
* as "pending" so you have to click a link in the email to actually
* subscribe.
*
* @param string $email_address The email address to subscribe.
*
* @throws Exception If subscribing to the mailing list fails for some
* reason. For example, if already subscribed.
*
* @return array The MailChimp response.
*/
public function subscribe($email_address) {
$method = 'POST';
$endpoint =
'lists/' .
$this->setting->get('mailchimp_list_id') .
'/members/'
;
$data = [
'email_address' => $email_address,
'status' => 'pending'
];
$response = $this->mailchimp_api($method, $endpoint, $data);
if(isset($response['id']) === false) {
throw new Exception('Could not subscribe to mailing list.');
}
return $response;
}
}

123
api/mailgun.php Normal file
View File

@ -0,0 +1,123 @@
<?php
/**
* High level functionality for interacting with the Mailgun API.
*
* @author Jon Ziebell
*/
class mailgun extends external_api {
protected static $log_mysql = 'all';
protected static $cache = false;
protected static $cache_for = null;
public static $exposed = [
'private' => [],
'public' => [
'subscribe',
'unsubscribe'
]
];
/**
* Send an API call off to mailgun
*
* @param string $method HTTP Method.
* @param string $endpoint API Endpoint.
* @param array $data API request data.
*
* @throws Exception If mailgun did not return valid JSON.
*
* @return array The mailgun response.
*/
private function mailgun_api($method, $endpoint, $data) {
$curl_response = $this->curl([
'url' => $this->setting->get('mailgun_base_url') . $endpoint,
'post_fields' => $data,
'method' => $method,
'header' => [
'Authorization: Basic ' . base64_encode('api:' . $this->setting->get('mailgun_api_key')),
'Content-Type: multipart/form-data'
]
]);
$response = json_decode($curl_response, true);
if ($response === null) {
throw new cora\exception('Invalid JSON', 10600);
}
return $response;
}
/**
* Subscribe to the mailing list.
*
* @param string $email_address The email address to subscribe.
*
* @throws exception If the subscribe failed.
*
* @return array Subscriber info.
*/
public function subscribe($email_address) {
$method = 'POST';
$endpoint = 'lists/' . $this->setting->get('mailgun_newsletter') . '/members';
$data = [
'address' => $email_address,
'subscribed' => 'yes',
'upsert' => 'yes'
];
$response = $this->mailgun_api($method, $endpoint, $data);
if (
isset($response['member']) &&
isset($response['member']['address']) &&
isset($response['member']['subscribed']) &&
$response['member']['address'] === $email_address &&
$response['member']['subscribed'] === true
) {
return $response['member'];
} else {
throw new cora\exception('Failed to subscribe.', 10601);
}
}
/**
* Unsubscribe from the mailing list.
*
* @param string $email_address The email address to unsubscribe.
*
* @throws exception If the unsubscribe failed.
*
* @return array Subscriber info.
*/
public function unsubscribe($email_address) {
$method = 'POST';
$endpoint = 'lists/' . $this->setting->get('mailgun_newsletter') . '/members';
$data = [
'address' => $email_address,
'subscribed' => 'no',
'upsert' => 'yes'
];
$response = $this->mailgun_api($method, $endpoint, $data);
if (
isset($response['member']) &&
isset($response['member']['address']) &&
isset($response['member']['subscribed']) &&
$response['member']['address'] === $email_address &&
$response['member']['subscribed'] === false
) {
return $response['member'];
} else {
throw new cora\exception('Failed to unsubscribe.', 10602);
}
}
}

View File

@ -1,8 +1,8 @@
<?php
/**
* Cache for these external API calls.
*
* @author Jon Ziebell
*/
class mailchimp_api_cache extends external_api_cache {}
<?php
/**
* Cache for these external API calls.
*
* @author Jon Ziebell
*/
class mailgun_api_cache extends external_api_cache {}

View File

@ -1,8 +1,8 @@
<?php
/**
* Log for these external API calls.
*
* @author Jon Ziebell
*/
class mailchimp_api_log extends external_api_log {}
<?php
/**
* Log for these external API calls.
*
* @author Jon Ziebell
*/
class mailgun_api_log extends external_api_log {}

View File

@ -345,6 +345,7 @@ a.inverted:active {
.icon.code_tags:before { content: "\F174"; }
.icon.dots_vertical:before { content: "\F1D9"; }
.icon.download:before { content: "\F1DA"; }
.icon.email:before { content: "\F1EE"; }
.icon.exit_to_app:before { content: "\F206"; }
.icon.eye:before { content: "\F208"; }
.icon.eye_off:before { content: "\F209"; }

View File

@ -164,21 +164,32 @@ beestat.api.prototype.load_ = function(response_text) {
return;
}
// Special handling for these error codes.
const error_codes = {
'log_out': [
1505, // Session is expired.
10000, // Could not get first token.
10001, // Could not refresh ecobee token; no token found.
10002, // Could not refresh ecobee token; ecobee returned no token.
10500, // Ecobee access was revoked by user.
10501 // No ecobee access for this user.
],
'ignore': [
10601 // Failed to subscribe.
]
};
// Error handling
if (
response.data &&
(
response.data.error_code === 1505 || // Session is expired.
response.data.error_code === 10000 || // Could not get first token.
response.data.error_code === 10001 || // Could not refresh ecobee token; no token found.
response.data.error_code === 10002 || // Could not refresh ecobee token; ecobee returned no token.
response.data.error_code === 10500 || // Ecobee access was revoked by user.
response.data.error_code === 10501 // No ecobee access for this user.
)
error_codes.log_out.includes(response.data.error_code) === true
) {
window.location.href = '/';
return;
} else if (response.success !== true) {
} else if (
response.success !== true &&
error_codes.ignore.includes(response.data.error_code) === false
) {
beestat.error(
'API call failed: ' + response.data.error_message,
JSON.stringify(response, null, 2)

View File

@ -34,7 +34,9 @@ beestat.setting = function(key, opt_value, opt_callback) {
'comparison_region': 'global',
'comparison_property_type': 'similar',
'temperature_unit': '°F'
'temperature_unit': '°F',
'first_run': true
};
if (user.settings === null) {

View File

@ -21,38 +21,39 @@ beestat.component.card.footer.prototype.decorate_contents_ = function(parent) {
footer_links.appendChild(
$.createElement('a')
.setAttribute('href', 'https://doc.beestat.io/')
.innerHTML('Help')
.innerText('Help')
);
footer_links.appendChild($.createElement('span').innerHTML(' • '));
footer_links.appendChild($.createElement('span').innerText(' • '));
footer_links.appendChild(
$.createElement('a')
.setAttribute('href', 'https://community.beestat.io/')
.setAttribute('target', '_blank')
.innerHTML('Feedback')
.innerText('Feedback')
);
footer_links.appendChild($.createElement('span').innerHTML(' • '));
footer_links.appendChild($.createElement('span').innerText(' • '));
footer_links.appendChild(
$.createElement('a')
.setAttribute('href', 'https://beestat.io/privacy/')
.setAttribute('target', '_blank')
.innerHTML('Privacy')
.innerText('Privacy')
);
footer_links.appendChild($.createElement('span').innerHTML(' • '));
footer_links.appendChild($.createElement('span').innerText(' • '));
footer_links.appendChild(
$.createElement('a')
.setAttribute('href', 'http://eepurl.com/dum59r')
.setAttribute('target', '_blank')
.innerHTML('Mailing List')
.innerText('Newsletter')
.addEventListener('click', function() {
(new beestat.component.modal.newsletter()).render();
})
);
footer_links.appendChild($.createElement('span').innerHTML(' • '));
footer_links.appendChild($.createElement('span').innerText(' • '));
footer_links.appendChild(
$.createElement('a')
.setAttribute('href', 'mailto:contact@beestat.io')
.innerHTML('Contact')
.innerText('Contact')
);
};

View File

@ -27,8 +27,7 @@ beestat.component.card.runtime_thermostat_detail = function(thermostat_id) {
[
'setting.runtime_thermostat_detail_range_type',
'setting.runtime_thermostat_detail_range_dynamic',
'cache.runtime_thermostat',
'cache.thermostat'
'cache.runtime_thermostat'
],
change_function
);

View File

@ -63,7 +63,7 @@ beestat.component.input.text.prototype.decorate_ = function(parent) {
parent.appendChild(icon_container);
this.input_.style({
'padding-left': '24px'
'padding-left': '28px'
});
(new beestat.component.icon(this.icon_).set_size(16)

View File

@ -0,0 +1,131 @@
/**
* Newsletter
*/
beestat.component.modal.newsletter = function() {
beestat.component.modal.apply(this, arguments);
};
beestat.extend(beestat.component.modal.newsletter, beestat.component.modal);
/**
* Decorate
*
* @param {rocket.Elements} parent
*/
beestat.component.modal.newsletter.prototype.decorate_contents_ = function(parent) {
parent.appendChild($.createElement('p').innerHTML('Interested in following beestat development? Subscribe to the newsletter for an updates. Emails are sparse; only a few every year.'));
this.email_address_ = new beestat.component.input.text()
.set_style({
'width': '100%',
'max-width': '300px',
'border-bottom': '2px solid ' + beestat.style.color.lightblue.base
});
if (this.subscribed_ === true) {
this.email_address_.set_icon('check');
this.email_address_.disable();
this.email_address_.set_value(this.state_.email_address_);
} else if (this.subscribed_ === false) {
this.email_address_.set_icon('email');
this.email_address_.set_value(this.state_.email_address_);
} else {
this.email_address_.set_icon('email');
this.email_address_.set_value(beestat.user.get().email_address);
}
this.email_address_.render(parent);
if (this.subscribed_ === true) {
parent.appendChild($.createElement('p').innerHTML('You are now subscribed to the beestat newsletter!'));
} else if (this.subscribed_ === false) {
parent.appendChild(
$.createElement('p')
.innerHTML(this.error_)
.style('color', beestat.style.color.red.base)
);
}
};
/**
* Get title.
*
* @return {string} Tht title.
*/
beestat.component.modal.newsletter.prototype.get_title_ = function() {
return 'Newsletter';
};
/**
* Get the buttons that go on the bottom of this modal.
*
* @return {[beestat.component.button]} The buttons.
*/
beestat.component.modal.newsletter.prototype.get_buttons_ = function() {
var self = this;
let buttons = [];
if (this.subscribed_ === true) {
const ok = new beestat.component.button()
.set_background_color(beestat.style.color.green.base)
.set_background_hover_color(beestat.style.color.green.light)
.set_text_color('#fff')
.set_text('Close')
.addEventListener('click', function() {
self.dispose();
});
buttons.push(ok);
} else {
const no_thanks = new beestat.component.button()
.set_background_color('#fff')
.set_text_color(beestat.style.color.gray.base)
.set_text_hover_color(beestat.style.color.red.base)
.set_text('No Thanks')
.addEventListener('click', function() {
self.dispose();
});
buttons.push(no_thanks);
const subscribe = new beestat.component.button()
.set_background_color(beestat.style.color.green.base)
.set_background_hover_color(beestat.style.color.green.light)
.set_text_color('#fff')
.set_text('Subscribe')
.addEventListener('click', function() {
self.state_.email_address_ = self.email_address_.get_value();
if (self.state_.email_address_.match(/.+@.+\..+/) === null) {
self.subscribed_ = false;
self.error_ = 'Invalid email address.';
self.rerender();
return;
}
this
.set_background_color(beestat.style.color.gray.base)
.set_background_hover_color()
.removeEventListener('click');
no_thanks.removeEventListener('click');
new beestat.api()
.add_call(
'mailgun',
'subscribe',
{'email_address': self.state_.email_address_}
)
.set_callback(function(response) {
if (response.subscribed === true) {
self.subscribed_ = true;
} else {
self.subscribed_ = false;
self.error_ = 'Subscribe failed; please try again later.';
}
self.rerender();
})
.send();
});
buttons.push(subscribe);
}
return buttons;
};

View File

@ -97,6 +97,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
echo '<script src="/js/component/modal/thermostat_info.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/weather.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/patreon_status.js"></script>' . PHP_EOL;
echo '<script src="/js/component/modal/newsletter.js"></script>' . PHP_EOL;
echo '<script src="/js/component/input.js"></script>' . PHP_EOL;
echo '<script src="/js/component/input/text.js"></script>' . PHP_EOL;
echo '<script src="/js/component/button.js"></script>' . PHP_EOL;

View File

@ -225,7 +225,14 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
}
}
if (
/*
* Show the first run modal or the announcements modal if there are unread
* important announcements.
*/
if (beestat.setting('first_run') === true) {
beestat.setting('first_run', false);
(new beestat.component.modal.newsletter()).render();
} else if (
last_read_announcement_id === undefined ||
(
most_recent_important_announcement_id !== undefined &&
@ -234,7 +241,6 @@ beestat.layer.load.prototype.decorate_ = function(parent) {
) {
(new beestat.component.modal.announcements()).render();
}
});
api.send();