@meltwater/conversations-api-services 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.drone.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ kind: pipeline
3
+ type: docker
4
+ name: Publish TechDocs
5
+ trigger:
6
+ branch:
7
+ - main
8
+ event:
9
+ - push
10
+
11
+ steps:
12
+ - name: Publish TechDocs
13
+ image: meltwater/drone-techdocs:1
@@ -0,0 +1,42 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+
7
+ permissions:
8
+ contents: read # for checkout
9
+
10
+ jobs:
11
+ release:
12
+ name: Release
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write # to be able to publish a GitHub release
16
+ issues: write # to be able to comment on released issues
17
+ pull-requests: write # to be able to comment on released pull requests
18
+ id-token: write # to enable use of OIDC for npm provenance
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v3
22
+ with:
23
+ fetch-depth: 0
24
+ persist-credential: false
25
+ token: ${{ secrets.GH_TOKEN }}
26
+ - name: Setup Node.js
27
+ uses: actions/setup-node@v3
28
+ with:
29
+ node-version: "lts/*"
30
+ - name: NPM Auth Setup
31
+ run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
32
+ - name: NPM whoami
33
+ run: npm whoami
34
+ - name: Install dependencies
35
+ run: npm clean-install
36
+ - name: Verify signatures
37
+ run: npm audit signatures
38
+ - name: Release
39
+ env:
40
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
41
+ GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
42
+ run: npx semantic-release
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 20.11.0
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # conversations-api-services
2
+
3
+ Repository to contain all conversations api services shared across our services
4
+
5
+ ---
6
+
7
+ This section should answer:
8
+
9
+ > What is this?
10
+
11
+ Adding visuals to this project description will make your project more memorable and the README easier to understand.
12
+
13
+ Great examples:
14
+
15
+ - [meltwater/contact-source-service](https://github.com/meltwater/contact-source-service) - _It is the backend behind the Influencers application used by Meltwater customers. It provides sources/contacts management and search capabilities._
16
+ - [release-it/release-it](https://github.com/release-it/release-it) - _‌Generic CLI tool to automate versioning and package publishing related tasks_
17
+
18
+ ## Product / Business Purpose
19
+
20
+ An explanation of the business purpose of this repo, e.g. the Meltwater Product that this ties into. This section should answer:
21
+
22
+ > Why does this exist?
23
+
24
+ Great examples:
25
+
26
+ - [meltwater/chat-message-integration](https://github.com/meltwater/chat-message-integration) _The chat-message-integration project is the API that is used to configure and handle messages for Slack and Teams integrations in the MI App._
27
+ - [meltwater/explore-compare-service](https://github.com/meltwater/explore-compare-service) - _Compares in Explorer allow users to run several saved queries at the same time and do a comparison._
28
+
29
+ ## Maintainers
30
+
31
+ Meltwater's current ownership model assumes that every piece of software running in Production is owned by a team. This section should answer:
32
+
33
+ > Who owns this?
34
+ > Who to contact for help?
35
+
36
+ Example:
37
+
38
+ > Maintained by TEAM_NAME (TEAM_EMAIL_ADDRESS@meltwater.com).
39
+ > Contact us via #TEAM_SLACK_CHANNEL in Slack.
40
+
41
+ ## Contributing
42
+
43
+ Make it easier for people to understand how they can contribute to your project. Remember that these people could be from your own team, or another team.
44
+
45
+ Example:
46
+
47
+ > We welcome and appreciate any and all contributions, even if it is a typo fix to this README.
48
+ > When working on a larger contribution, please open an issue first, so that we can discuss the approach.
49
+ > Once you have your contribution ready, please open a Pull Requests containing your changes and contact us via #TEAM_SLACK_CHANNEL in Slack.
50
+
51
+ ## How do I use it? (Installation)
52
+
53
+ Instructions targeted at the **user** of your project, rather than at a co-developer in your team. This section should answer:
54
+
55
+ > How to use this?
56
+
57
+ If this is an internal tool that is just used within your team, then you can share this here as well.
58
+
59
+ ## Development Guide
60
+
61
+ These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. This section should answer:
62
+
63
+ > How to get started with developing on this project?
64
+
65
+ This is essential for onboarding new members on your team but also for allowing people from outside of your team to contribute.
66
+
67
+ This Development Guide may contain different sub-sections, depending on what stack/ecosystem your project is using. Below some examples of sub-sections that apply to most projects.
68
+
69
+ ### Prerequisites
70
+
71
+ What things do you need to install the software and how to install them? Including links here is helpful.
72
+
73
+ ### Running it locally
74
+
75
+ A step-by-step series of examples that tell you how to get a development env running.
76
+
77
+ End with an example of getting some data out of the system or using it for a little demo.
78
+
79
+ ### Running the tests
80
+
81
+ Explain how to run the automated tests for this system.
82
+
83
+ ### Deployment
84
+
85
+ Add additional notes about how to deploy this on a live system.
86
+ You may also want to link to the different environments where this is deployed (e.g. Staging, Production).
87
+
88
+ ### Monitoring & Troubleshooting
89
+
90
+ How can you inspect the system running in remote environments (Staging, Production)?
91
+ This should include logs, dashboards, etc.
@@ -0,0 +1,12 @@
1
+ apiVersion: backstage.io/v1alpha1
2
+ kind: Component
3
+ metadata:
4
+ name: "conversations-api-services"
5
+ description: "Repository to contain all conversations api services shared across our services"
6
+ annotations:
7
+ github.com/project-slug: meltwater/conversations-api-services
8
+ backstage.io/techdocs-ref: dir:.
9
+ spec:
10
+ type: "module"
11
+ lifecycle: experimental
12
+ owner: "group:default/phoenix"
package/docs/index.md ADDED
@@ -0,0 +1,28 @@
1
+ ## conversations-api-services
2
+
3
+ Repository to contain all conversations api services shared across our services
4
+
5
+ ## Getting started
6
+
7
+ Start write your documentation by adding more markdown (.md) files to this folder (/docs) or replace the content in this file.
8
+
9
+ ## Table of Contents
10
+
11
+ The Table of Contents on the right is generated automatically based on the hierarchy
12
+ of headings. Only use one H1 (`#` in Markdown) per file.
13
+
14
+ ## Site navigation
15
+
16
+ For new pages to appear in the left hand navigation you need edit the `mkdocs.yml`
17
+ file in root of your repo. The navigation can also link out to other sites.
18
+
19
+ Alternatively, if there is no `nav` section in `mkdocs.yml`, a navigation section
20
+ will be created for you. However, you will not be able to use alternate titles for
21
+ pages, or include links to other sites.
22
+
23
+ Note that MkDocs uses `mkdocs.yml`, not `mkdocs.yaml`, although both appear to work.
24
+ See also <https://www.mkdocs.org/user-guide/configuration/>.
25
+
26
+ ## Support
27
+
28
+ That's it. If you need support, reach out in [#docs-like-code](https://discord.com/channels/687207715902193673/714754240933003266) on Discord.
package/mkdocs.yml ADDED
@@ -0,0 +1,8 @@
1
+ site_name: "conversations-api-services"
2
+ site_description: "Repository to contain all conversations api services shared across our services"
3
+
4
+ nav:
5
+ - Introduction: index.md
6
+
7
+ plugins:
8
+ - techdocs-core
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@meltwater/conversations-api-services",
3
+ "version": "1.0.0",
4
+ "description": "Repository to contain all conversations api services shared across our services",
5
+ "type": "module",
6
+ "main": "src/data-access/index.js",
7
+ "directories": {
8
+ "doc": "docs"
9
+ },
10
+ "prettier": "@meltwater/engage-prettier-config",
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/meltwater/conversations-api-services.git"
17
+ },
18
+ "keywords": [],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "bugs": {
22
+ "url": "https://github.com/meltwater/conversations-api-services/issues"
23
+ },
24
+ "homepage": "https://github.com/meltwater/conversations-api-services#readme",
25
+ "dependencies": {
26
+ "@hapi/joi": "^17.1.1",
27
+ "@meltwater/date-range": "^3.6.0",
28
+ "@meltwater/engage-conversations-schemas": "^2.3.10",
29
+ "@meltwater/xrunes": "^1.1.1",
30
+ "@meltwater/xrunes-core": "^3.0.0",
31
+ "apollo-server-hapi": "^3.13.0",
32
+ "aws-sdk": "^2.1562.0",
33
+ "dataloader": "^2.2.2",
34
+ "html-entities": "^2.4.0",
35
+ "jsonwebtoken": "^9.0.2",
36
+ "lodash": "^4.17.21",
37
+ "moment": "^2.30.1",
38
+ "prom-client": "^15.1.0",
39
+ "superagent": "^8.1.2",
40
+ "winston": "^3.11.0"
41
+ },
42
+ "devDependencies": {
43
+ "@meltwater/engage-prettier-config": "^1.0.0"
44
+ }
45
+ }
@@ -0,0 +1,37 @@
1
+ import superagent from 'superagent';
2
+ import configuration from '../../lib/configuration.js';
3
+ import logger from '../../lib/logger.js';
4
+
5
+ export class InstagramVideoClient {
6
+ /**
7
+ * @private
8
+ * @param {string} url
9
+ * @returns {Promise<object>}
10
+ */
11
+ _getDownloadLink = async (url) => {
12
+ const apiUrl = configuration.get('UGC_MEDIA_API_URL');
13
+ const apiToken = configuration.get('UGC_MEDIA_API_TOKEN');
14
+ try {
15
+ const response = await superagent
16
+ .get(`${apiUrl}/instagram/video/`)
17
+ .set('x-api-token', apiToken)
18
+ .query({ url });
19
+
20
+ return response.body?.data;
21
+ } catch (error) {
22
+ logger.error(`Could not retrieve video link from ${url}`, {
23
+ error,
24
+ });
25
+ throw new Error('Could not retrieve video link');
26
+ }
27
+ };
28
+
29
+ /**
30
+ *
31
+ * @param {string} url
32
+ * @returns {Promise<object>}
33
+ */
34
+ getVideoLink = async (url) => {
35
+ return await this._getDownloadLink(url);
36
+ };
37
+ }
@@ -0,0 +1,39 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class WarpZoneApiClient {
6
+ constructor() {
7
+ this.warpzoneURL = configuration.get('WARPZONE_API_URL');
8
+ this.warpzoneAPI = configuration.get('WARPZONE_API_KEY');
9
+ }
10
+
11
+ async sendPost(postData = undefined) {
12
+ let response = {};
13
+ try {
14
+ response = await superagent
15
+ .post(this.warpzoneURL + 'event')
16
+ .set('Accept', 'application/json')
17
+ .set('Content-Type', 'application/json')
18
+ .set('X-API-Key', this.warpzoneAPI)
19
+ .send(postData);
20
+ } catch (err) {
21
+ if (
22
+ err &&
23
+ err.response &&
24
+ err.response.body &&
25
+ err.response.body.error
26
+ ) {
27
+ logger.error(
28
+ `Failed to call warp zone api: ${err.response.body.error.message}`
29
+ );
30
+ } else {
31
+ logger.error(`Failed to call warp zone api`, err);
32
+ }
33
+
34
+ throw err;
35
+ }
36
+
37
+ return response;
38
+ }
39
+ }
@@ -0,0 +1,41 @@
1
+ import AWS from 'aws-sdk';
2
+ import assert from 'assert';
3
+
4
+ class AWS_S3 {
5
+ constructor() {}
6
+
7
+ async getClient() {
8
+ const {
9
+ Credentials: { AccessKeyId, SecretAccessKey, SessionToken } = {},
10
+ } = await assetManagerTvmRepository.get(companyId);
11
+
12
+ if (!AccessKeyId || !SecretAccessKey || !SessionToken) {
13
+ // probably a bit extreme, what would you prefer to do
14
+ throw 'No Credentials available';
15
+ }
16
+
17
+ return new AWS.S3({
18
+ accessKeyId: AccessKeyId,
19
+ secretAccessKey: SecretAccessKey,
20
+ sessionToken: SessionToken,
21
+ useAccelerateEndpoint: true,
22
+ });
23
+ }
24
+
25
+ GetS3ObjectQuery(bucket, filename) {
26
+ assert(bucket, 'S3 bucket is required');
27
+ assert(filename, 'filePath is required');
28
+ assert(typeof filename === 'string', 'filePath should be a string.');
29
+
30
+ return {
31
+ config: {
32
+ Bucket: bucket,
33
+ Key: filename.replace(`/${bucket}/`, ''),
34
+ GrantRead:
35
+ 'uri=http://acs.amazonaws.com/groups/global/AllUsers',
36
+ },
37
+ };
38
+ }
39
+ }
40
+
41
+ export const awsS3Client = new AWS_S3();
@@ -0,0 +1,32 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+
4
+ const { ASSET_MANAGER_TVM_API_KEY, ASSET_MANAGER_TVM_URL } = process.env;
5
+
6
+ class AssetManagerTVMRepository {
7
+ apiKey;
8
+ constructor() {}
9
+
10
+ async get(companyId) {
11
+ let response;
12
+ try {
13
+ response = await superagent
14
+ .get(`https://${ASSET_MANAGER_TVM_URL}/tvm`)
15
+ .query({
16
+ companyId,
17
+ })
18
+ .timeout(5000)
19
+ .set({
20
+ 'x-api-key': ASSET_MANAGER_TVM_API_KEY,
21
+ })
22
+ .then((result) => result.body);
23
+ } catch (error) {
24
+ logger.error(error);
25
+ throw error;
26
+ }
27
+
28
+ return response;
29
+ }
30
+ }
31
+
32
+ export const assetManagerTvmRepository = new AssetManagerTVMRepository();
@@ -0,0 +1,35 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class CompanyApiClient {
6
+ constructor({ services }) {
7
+ this.companiesApiUrl = configuration.get('COMPANIES_API_URL');
8
+
9
+ this.authService = services.auth;
10
+ }
11
+
12
+ async getById(companyId, jwt, retries = 0) {
13
+ let company;
14
+ try {
15
+ let resignedToken = await this.authService.getResignedToken(jwt);
16
+ company = await superagent
17
+ .get(`${this.companiesApiUrl}/${companyId}`)
18
+ .set('Authorization', resignedToken)
19
+ .set('x-client-name', configuration.get('CLIENT_NAME_HEADER'))
20
+ .then((result) => result.body);
21
+ } catch (error) {
22
+ logger.error(
23
+ `Failed requesting Companies Api for companyId ${companyId} retry ${retries}`,
24
+ error
25
+ );
26
+
27
+ if (retries <= 3) {
28
+ return this.getById(companyId, jwt, ++retries);
29
+ }
30
+ return;
31
+ }
32
+
33
+ return company;
34
+ }
35
+ }
@@ -0,0 +1,137 @@
1
+ import superagent from 'superagent'; // @todo remove superagent
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class CredentialsApiClient {
6
+ constructor() {
7
+ this.credentialsApiUrl = configuration.get('CREDENTIALS_API_URL');
8
+ this.credentialsApiApplicationId = configuration.get(
9
+ 'CREDENTIALS_API_APPLICATION_ID'
10
+ );
11
+ }
12
+
13
+ async getCredential(
14
+ { companyId, userId, accountId, productArea },
15
+ userPermissionsEnabled
16
+ ) {
17
+ const credentials = await this.getCredentialsByCompany(
18
+ { companyId, userId, productArea },
19
+ userPermissionsEnabled
20
+ );
21
+
22
+ if (!credentials) {
23
+ throw 'No credentials returned';
24
+ }
25
+
26
+ const credential = credentials.find(
27
+ (credential) => credential.social_account_id == accountId
28
+ );
29
+
30
+ if (!credential) {
31
+ throw `${sourceId} credential was not found.`;
32
+ }
33
+
34
+ return credential;
35
+ }
36
+
37
+ async getCredentialsByCompany(
38
+ { companyId, userId, productArea },
39
+ userPermissionsEnabled
40
+ ) {
41
+ let credentials;
42
+ try {
43
+ if (productArea) {
44
+ logger.info(
45
+ `Get Credentials for company: ${companyId} and user: ${userId}`
46
+ );
47
+ credentials = await superagent
48
+ .get(
49
+ `${this.credentialsApiUrl}/api/v4.0/CompanyCredentials/${this.credentialsApiApplicationId}/${companyId}/${userId}`
50
+ )
51
+ .query({
52
+ productArea,
53
+ })
54
+ .then((result) => result.body.data);
55
+ } else if (userPermissionsEnabled) {
56
+ credentials = await superagent
57
+ .get(
58
+ `${this.credentialsApiUrl}/api/v4.0/CompanyCredentials/${this.credentialsApiApplicationId}/${companyId}/${userId}`
59
+ )
60
+ .then((result) => result.body.data);
61
+ } else {
62
+ credentials = await superagent
63
+ .get(
64
+ `${this.credentialsApiUrl}/api/v3.0/CompanyCredentials/${this.credentialsApiApplicationId}/${companyId}/${userId}`
65
+ )
66
+ .then((result) => result.body.data);
67
+ }
68
+ } catch (error) {
69
+ logger.error(
70
+ `Failed requesting Credentials API for companyId ${companyId} ${userId}`,
71
+ error
72
+ );
73
+ return [];
74
+ }
75
+
76
+ logger.info(
77
+ `Finished requesting Credentials API for companyId ${companyId}`
78
+ );
79
+
80
+ return credentials;
81
+ }
82
+
83
+ // todo: will this need to ever use productArea?
84
+ async getToken(credentialId, companyId) {
85
+ let token;
86
+ try {
87
+ token = await superagent
88
+ .get(
89
+ `${this.credentialsApiUrl}/api/Token/${credentialId}/${companyId}`
90
+ )
91
+ .then((result) => {
92
+ return result.body;
93
+ });
94
+ } catch (error) {
95
+ logger.error(
96
+ `Failed requesting Credentials API for credentialId ${credentialId} and companyId ${companyId}`,
97
+ error
98
+ );
99
+ return;
100
+ }
101
+
102
+ logger.info(
103
+ `Finished requesting Credentials API for credentialId ${credentialId} and companyId ${companyId}`
104
+ );
105
+
106
+ return token;
107
+ }
108
+
109
+ async getCredentialIdByAccountId(accountId, channel) {
110
+ let credentialId;
111
+ try {
112
+ credentialId = await superagent
113
+ .get(
114
+ `${this.credentialsApiUrl}/api/CompanyCredentials/GetCredential/${channel}/${accountId}`
115
+ )
116
+ .then((result) => {
117
+ return (
118
+ result.body &&
119
+ result.body.data &&
120
+ result.body.data.credentialId
121
+ );
122
+ });
123
+ } catch (error) {
124
+ logger.error(
125
+ `Failed requesting Credentials API for credentialId ${accountId} and channel ${channel}`,
126
+ error
127
+ );
128
+ return;
129
+ }
130
+
131
+ logger.info(
132
+ `Finished requesting Credentials API for credentialId ${accountId} and channel ${channel}`
133
+ );
134
+
135
+ return credentialId;
136
+ }
137
+ }
@@ -0,0 +1,41 @@
1
+ import superagent from 'superagent';
2
+ import logger from '../../lib/logger.js';
3
+ import configuration from '../../lib/configuration.js';
4
+
5
+ export class EntitlementsApiClient {
6
+ constructor({ services }) {
7
+ this.entitlementsApiUrl = configuration.get('ENTITLEMENTS_API_URL');
8
+
9
+ this.authService = services.auth;
10
+ }
11
+
12
+ async getById(entitlementId, companyId, jwt, retries = 0) {
13
+ let entitlement;
14
+ try {
15
+ let resignedToken = await this.authService.getResignedToken(jwt);
16
+ entitlement = await superagent
17
+ .get(
18
+ `${this.entitlementsApiUrl}/company/${companyId}/${entitlementId}`
19
+ )
20
+ .set('Authorization', resignedToken)
21
+ .set('x-client-name', configuration.get('CLIENT_NAME_HEADER'))
22
+ .then((result) => result.body);
23
+ } catch (error) {
24
+ logger.error(
25
+ `Failed requesting entitlement ${entitlementId} for companyId ${companyId} retry ${retries}`,
26
+ error
27
+ );
28
+
29
+ if (retries <= 3) {
30
+ return this.getById(entitlementId, companyId, jwt, ++retries);
31
+ }
32
+ return;
33
+ }
34
+
35
+ logger.info(
36
+ `Finished requesting entitlement ${entitlementId} for companyId ${companyId}`
37
+ );
38
+
39
+ return entitlement;
40
+ }
41
+ }