Created
December 6, 2020 05:46
-
-
Save kentcdodds/c1a777a566e6a8b8d5e5f5fe7f9d56fd to your computer and use it in GitHub Desktop.
Sync ConvertKit with TestingJavaScript.com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs/promises') | |
const fetch = require('make-fetch-happen').defaults({ | |
cacheManager: './node_modules/.cache/make-fetch-happen', | |
}) | |
const csv = require('csvtojson') | |
const {CONVERT_KIT_API_KEY, CONVERT_KIT_API_SECRET} = process.env | |
const levels = ['standard-testing', 'basic-testing', 'pro-testing'] | |
const appendToFinished = ({ | |
email, | |
level, | |
id = 'No Subscriber', | |
needs = [], | |
remove = [], | |
}) => { | |
return fs.appendFile( | |
'./finished.csv', | |
[email, level, id, needs.join(';'), remove.join(';')].join(',') + '\n', | |
) | |
} | |
async function go() { | |
const data = await csv().fromFile('./data.csv') | |
const finishedEmails = (await csv().fromFile('./finished.csv')).map( | |
f => f.email, | |
) | |
console.log(`Processing ${data.length} emails`) | |
for (const item of data) { | |
const {email, level} = item | |
if (finishedEmails.includes(email)) continue | |
const percent = ((data.indexOf(item) / data.length) * 100).toFixed(2) | |
console.log(`${percent}%`) | |
const subscriber = await getConvertKitSubscriber(email) | |
if (!subscriber) { | |
// console.log('no subscriber:', item) | |
appendToFinished(item) | |
continue | |
} | |
const subscriberTags = await getTags(subscriber) | |
const purchasedTestingId = tagIds['purchased-testing'] | |
const needs = [ | |
subscriberTags.includes(purchasedTestingId) ? null : purchasedTestingId, | |
].filter(Boolean) | |
const remove = [] | |
for (const levelName of levels) { | |
const tagId = tagIds[levelName] | |
const hasTag = subscriberTags.includes(tagId) | |
if (!tagId) throw new Error(`No tagId for ${levelName}`) | |
if (level === levelName && !hasTag) needs.push(tagId) | |
else if (level !== levelName && hasTag) remove.push(tagId) | |
} | |
const promises = [ | |
needs.length ? addTag(email, needs) : null, | |
...remove.map(r => removeTag(subscriber, r)), | |
].filter(Boolean) | |
if (promises.length) { | |
const result = await Promise.all(promises) | |
const meta = { | |
id: subscriber.id, | |
email, | |
level, | |
needs: needs.map(n => tagNames[n]), | |
remove: remove.map(r => tagNames[r]), | |
} | |
// console.log('done updating:', meta) | |
appendToFinished(meta) | |
} else { | |
const meta = {email, level, id: subscriber.id} | |
// console.log('no update needed:', meta) | |
appendToFinished(meta) | |
} | |
} | |
console.log('All done...') | |
} | |
async function getConvertKitSubscriber(email) { | |
const url = new URL('https://api.convertkit.com/v3/subscribers') | |
url.searchParams.set('api_secret', CONVERT_KIT_API_SECRET) | |
url.searchParams.set('email_address', email) | |
const data = await fetch(url.toString()).then(r => r.json()) | |
const {subscribers: [subscriber] = []} = data | |
return subscriber?.state === 'active' ? subscriber : null | |
} | |
async function getTags(subscriber) { | |
return fetch( | |
`https://api.convertkit.com/v3/subscribers/${subscriber.id}/tags?api_key=${CONVERT_KIT_API_KEY}`, | |
) | |
.then(r => r.json()) | |
.then(d => d.tags.map(t => String(t.id))) | |
} | |
const tagIds = { | |
purchased: '746921', | |
'purchased-testing': '746923', | |
'standard-testing': '746934', | |
'basic-testing': '746932', | |
'pro-testing': '746922', | |
} | |
const tagNames = { | |
[tagIds.purchased]: 'purchased', | |
[tagIds['purchased-testing']]: 'purchased-testing', | |
[tagIds['standard-testing']]: 'standard-testing', | |
[tagIds['basic-testing']]: 'basic-testing', | |
[tagIds['pro-testing']]: 'pro-testing', | |
} | |
async function addTag(email, tags) { | |
return fetch( | |
`https://api.convertkit.com/v3/tags/${tagIds.purchased}/subscribe`, | |
{ | |
method: 'POST', | |
headers: {'content-type': 'application/json'}, | |
body: JSON.stringify({ | |
api_key: CONVERT_KIT_API_KEY, | |
api_secret: CONVERT_KIT_API_SECRET, | |
email, | |
tags, | |
}), | |
}, | |
).then(r => r.json()) | |
} | |
async function removeTag(subscriber, tagId) { | |
return fetch( | |
`https://api.convertkit.com/v3/subscribers/${subscriber.id}/tags/${tagId}?api_secret=${CONVERT_KIT_API_SECRET}`, | |
{method: 'DELETE'}, | |
).then(r => r.json()) | |
} | |
function makeGo() { | |
go().catch(error => { | |
if (error.stack?.includes?.('Unexpected token R in JSON at position 0')) { | |
console.log('rate limited... wait a minute then continue.') | |
setTimeout(() => { | |
makeGo() | |
}, 60 * 1000) | |
} | |
console.error(error.stack) | |
}) | |
} | |
makeGo() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment