Преглед изворни кода

Merge pull request #18021 from overleaf/rh-mailchimp-api

[web] Replace node-mailchimp with own MailChimpClient

GitOrigin-RevId: 10207620c48f30ad29f4f0e7ea5193c11d256902
roo hutton пре 2 година
родитељ
комит
06cac44d84

+ 0 - 131
package-lock.json

@@ -27908,78 +27908,6 @@
       "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
       "dev": true
     },
-    "node_modules/mailchimp-api-v3": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.15.0.tgz",
-      "integrity": "sha512-9TxCFG+VRpl14HOHgABHYmC5GvpCY7LYqyTefOXd4GtI07oXCiJ7W5fEvk3SJKBctlbjhKbzjB5qOZMQpacEUQ==",
-      "dependencies": {
-        "bluebird": "^3.4.0",
-        "lodash": "^4.17.14",
-        "request": "^2.88.0",
-        "tar": "^4.0.2"
-      }
-    },
-    "node_modules/mailchimp-api-v3/node_modules/fs-minipass": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
-      "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
-      "dependencies": {
-        "minipass": "^2.6.0"
-      }
-    },
-    "node_modules/mailchimp-api-v3/node_modules/minipass": {
-      "version": "2.9.0",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
-      "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
-      "dependencies": {
-        "safe-buffer": "^5.1.2",
-        "yallist": "^3.0.0"
-      }
-    },
-    "node_modules/mailchimp-api-v3/node_modules/minizlib": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
-      "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
-      "dependencies": {
-        "minipass": "^2.9.0"
-      }
-    },
-    "node_modules/mailchimp-api-v3/node_modules/safe-buffer": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ]
-    },
-    "node_modules/mailchimp-api-v3/node_modules/tar": {
-      "version": "4.4.19",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
-      "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
-      "dependencies": {
-        "chownr": "^1.1.4",
-        "fs-minipass": "^1.2.7",
-        "minipass": "^2.9.0",
-        "minizlib": "^1.3.3",
-        "mkdirp": "^0.5.5",
-        "safe-buffer": "^5.2.1",
-        "yallist": "^3.1.1"
-      },
-      "engines": {
-        "node": ">=4.5"
-      }
-    },
     "node_modules/make-dir": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -43585,7 +43513,6 @@
         "jsonwebtoken": "^9.0.0",
         "lodash": "^4.17.19",
         "lru-cache": "^7.10.1",
-        "mailchimp-api-v3": "^1.12.0",
         "marked": "^4.1.0",
         "method-override": "^2.3.3",
         "minimatch": "^7.4.2",
@@ -52263,7 +52190,6 @@
         "less-loader": "^11.1.3",
         "lodash": "^4.17.19",
         "lru-cache": "^7.10.1",
-        "mailchimp-api-v3": "^1.12.0",
         "marked": "^4.1.0",
         "match-sorter": "^6.2.0",
         "mathjax": "^3.2.2",
@@ -69281,63 +69207,6 @@
         }
       }
     },
-    "mailchimp-api-v3": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.15.0.tgz",
-      "integrity": "sha512-9TxCFG+VRpl14HOHgABHYmC5GvpCY7LYqyTefOXd4GtI07oXCiJ7W5fEvk3SJKBctlbjhKbzjB5qOZMQpacEUQ==",
-      "requires": {
-        "bluebird": "^3.4.0",
-        "lodash": "^4.17.14",
-        "request": "^2.88.0",
-        "tar": "^4.0.2"
-      },
-      "dependencies": {
-        "fs-minipass": {
-          "version": "1.2.7",
-          "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
-          "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
-          "requires": {
-            "minipass": "^2.6.0"
-          }
-        },
-        "minipass": {
-          "version": "2.9.0",
-          "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
-          "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
-          "requires": {
-            "safe-buffer": "^5.1.2",
-            "yallist": "^3.0.0"
-          }
-        },
-        "minizlib": {
-          "version": "1.3.3",
-          "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
-          "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
-          "requires": {
-            "minipass": "^2.9.0"
-          }
-        },
-        "safe-buffer": {
-          "version": "5.2.1",
-          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-          "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
-        },
-        "tar": {
-          "version": "4.4.19",
-          "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
-          "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
-          "requires": {
-            "chownr": "^1.1.4",
-            "fs-minipass": "^1.2.7",
-            "minipass": "^2.9.0",
-            "minizlib": "^1.3.3",
-            "mkdirp": "^0.5.5",
-            "safe-buffer": "^5.2.1",
-            "yallist": "^3.1.1"
-          }
-        }
-      }
-    },
     "make-dir": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

+ 60 - 0
services/web/app/src/Features/Newsletter/MailChimpClient.js

@@ -0,0 +1,60 @@
+const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
+
+class MailChimpClient {
+  constructor(apiKey) {
+    this.apiKey = apiKey
+    this.dc = apiKey.split('-')[1]
+    this.baseUrl = `https://${this.dc}.api.mailchimp.com/3.0/`
+    this.fetchOptions = {
+      method: 'GET',
+      basicAuth: {
+        user: 'any',
+        password: this.apiKey,
+      },
+    }
+  }
+
+  async request(path, options) {
+    try {
+      const requestUrl = `${this.baseUrl}${path}`
+      if (options.method === 'GET') {
+        return await fetchJson(requestUrl, options)
+      }
+      await fetchNothing(requestUrl, options)
+    } catch (err) {
+      // if there's a json body in the response, expose it in the error (for compatibility with node-mailchimp)
+      const errorBody = err.body ? JSON.parse(err.body) : {}
+      const errWithBody = Object.assign(err, errorBody)
+      throw errWithBody
+    }
+  }
+
+  async get(path) {
+    return await this.request(path, this.fetchOptions)
+  }
+
+  async put(path, body) {
+    const options = Object.assign({}, this.fetchOptions)
+    options.method = 'PUT'
+    options.json = body
+
+    return await this.request(path, options)
+  }
+
+  async delete(path) {
+    const options = Object.assign({}, this.fetchOptions)
+    options.method = 'DELETE'
+
+    return await this.request(path, options)
+  }
+
+  async patch(path, body) {
+    const options = Object.assign({}, this.fetchOptions)
+    options.method = 'PATCH'
+    options.json = body
+
+    return await this.request(path, options)
+  }
+}
+
+module.exports = MailChimpClient

+ 4 - 4
services/web/app/src/Features/Newsletter/MailChimpProvider.js

@@ -1,9 +1,9 @@
 const logger = require('@overleaf/logger')
 const Settings = require('@overleaf/settings')
 const crypto = require('crypto')
-const Mailchimp = require('mailchimp-api-v3')
 const OError = require('@overleaf/o-error')
 const { callbackify } = require('util')
+const MailChimpClient = require('./MailChimpClient')
 
 function mailchimpIsConfigured() {
   return Settings.mailchimp != null && Settings.mailchimp.api_key != null
@@ -38,7 +38,7 @@ class NonFatalEmailUpdateError extends OError {
 }
 
 function makeMailchimpProvider(listName, listId) {
-  const mailchimp = new Mailchimp(Settings.mailchimp.api_key)
+  const mailchimp = new MailChimpClient(Settings.mailchimp.api_key)
   const MAILCHIMP_LIST_ID = listId
 
   return {
@@ -54,7 +54,7 @@ function makeMailchimpProvider(listName, listId) {
       const result = await mailchimp.get(path)
       return result?.status === 'subscribed'
     } catch (err) {
-      if (err.status === 404) {
+      if (err?.response?.status === 404) {
         return false
       }
       throw OError.tag(err, 'error getting newsletter subscriptions status', {
@@ -101,7 +101,7 @@ function makeMailchimpProvider(listName, listId) {
         'finished unsubscribing user from newsletter'
       )
     } catch (err) {
-      if (err.status === 404 || err.status === 405) {
+      if ([404, 405].includes(err?.response?.status)) {
         // silently ignore users who were never subscribed (404) or previously deleted (405)
         return
       }

+ 0 - 1
services/web/package.json

@@ -123,7 +123,6 @@
     "jsonwebtoken": "^9.0.0",
     "lodash": "^4.17.19",
     "lru-cache": "^7.10.1",
-    "mailchimp-api-v3": "^1.12.0",
     "marked": "^4.1.0",
     "method-override": "^2.3.3",
     "minimatch": "^7.4.2",

+ 14 - 4
services/web/test/unit/src/Newsletter/NewsletterManagerTests.js

@@ -1,5 +1,6 @@
 const { expect } = require('chai')
 const sinon = require('sinon')
+const { RequestFailedError } = require('@overleaf/fetch-utils')
 const SandboxedModule = require('sandboxed-module')
 
 const MODULE_PATH = '../../../../app/src/Features/Newsletter/NewsletterManager'
@@ -28,11 +29,15 @@ describe('NewsletterManager', function () {
 
     this.NewsletterManager = SandboxedModule.require(MODULE_PATH, {
       requires: {
-        'mailchimp-api-v3': this.Mailchimp,
+        './MailChimpClient': this.Mailchimp,
         '@overleaf/settings': this.Settings,
       },
+      globals: { AbortController },
     }).promises
 
+    this.NewsletterManager.get = sinon.stub()
+    this.NewsletterManager.delete = sinon.stub()
+
     this.user = {
       _id: 'user_id',
       email: 'overleaf.duck@example.com',
@@ -59,9 +64,14 @@ describe('NewsletterManager', function () {
     })
 
     it('returns false on 404', async function () {
-      const err = new Error()
-      err.status = 404
-      this.mailchimp.get.rejects(err)
+      this.mailchimp.get.rejects(
+        new RequestFailedError(
+          'http://some-url',
+          {},
+          { status: 404 },
+          'Not found'
+        )
+      )
       const subscribed = await this.NewsletterManager.subscribed(this.user)
       expect(subscribed).to.be.false
     })