@open-wa/integration-s3 5.0.0-alpha.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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +21 -0
- package/LICENSE.md +67 -0
- package/README.md +62 -0
- package/dist/index.cjs +141 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +52 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +116 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +40 -0
- package/src/config.ts +20 -0
- package/src/index.ts +3 -0
- package/src/plugin.ts +46 -0
- package/src/uploader.ts +137 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +5 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
> @open-wa/integration-s3@5.0.0-alpha.0 build /Users/Mohammed/projects/tools/wa/integrations/s3
|
|
3
|
+
> tsdown src/index.ts --format esm --format cjs
|
|
4
|
+
|
|
5
|
+
[34mℹ[39m [34mtsdown v0.21.7[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.12[39m
|
|
6
|
+
[34mℹ[39m config file: [4m/Users/Mohammed/projects/tools/wa/integrations/s3/tsdown.config.ts[24m
|
|
7
|
+
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
8
|
+
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
+
[34mℹ[39m Build start
|
|
10
|
+
[34mℹ[39m Cleaning 7 files
|
|
11
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.cjs[22m [2m4.90 kB[22m [2m│ gzip: 1.74 kB[22m
|
|
12
|
+
[34mℹ[39m [33m[CJS][39m 1 files, total: 4.90 kB
|
|
13
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [2m0.78 kB[22m [2m│ gzip: 0.35 kB[22m
|
|
14
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [2m1.46 kB[22m [2m│ gzip: 0.66 kB[22m
|
|
15
|
+
[34mℹ[39m [33m[CJS][39m 2 files, total: 2.24 kB
|
|
16
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m3.73 kB[22m [2m│ gzip: 1.31 kB[22m
|
|
17
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m8.02 kB[22m [2m│ gzip: 2.53 kB[22m
|
|
18
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m0.78 kB[22m [2m│ gzip: 0.35 kB[22m
|
|
19
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m1.46 kB[22m [2m│ gzip: 0.66 kB[22m
|
|
20
|
+
[34mℹ[39m [34m[ESM][39m 4 files, total: 13.99 kB
|
|
21
|
+
[32m✔[39m Build complete in [32m4936ms[39m
|
|
22
|
+
[32m✔[39m Build complete in [32m4939ms[39m
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @open-wa/integration-s3
|
|
2
|
+
|
|
3
|
+
## 5.0.0-alpha.3
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Dry run minor bump
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies []:
|
|
12
|
+
- @open-wa/core@6.0.0-alpha.2
|
|
13
|
+
- @open-wa/logger@6.0.0-alpha.2
|
|
14
|
+
|
|
15
|
+
## 5.0.0-alpha.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies []:
|
|
20
|
+
- @open-wa/core@5.0.1-alpha.2
|
|
21
|
+
- @open-wa/logger@5.0.1-alpha.2
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
### Copyright (c) 2020 Mohammed Shah
|
|
2
|
+
|
|
3
|
+
# Hippocratic + Do Not Harm (H-DNH) Version 1.1
|
|
4
|
+
|
|
5
|
+
## Preamble
|
|
6
|
+
|
|
7
|
+
Most software today is developed with little to no thought of how it will be used, or the consequences for our society and planet.
|
|
8
|
+
|
|
9
|
+
As software developers, we engineer the infrastructure of the 21st century. We recognise that our infrastructure has great power to shape the world and the lives of those we share it with, and we choose to consciously take responsibility for the social and environmental impacts of what we build.
|
|
10
|
+
|
|
11
|
+
We envisage a world free from injustice, inequality, and the reckless destruction of lives and our planet. We reject slavery in all its forms, whether by force, indebtedness, or by algorithms that hack human vulnerabilities. We seek a world where humankind is at peace with our neighbours, nature, and ourselves. We want our work to enrich the physical, mental and spiritual wellbeing of all society.
|
|
12
|
+
|
|
13
|
+
We build software to further this vision of a just world, or at the very least, to not put that vision further from reach.
|
|
14
|
+
|
|
15
|
+
## Terms
|
|
16
|
+
|
|
17
|
+
Licensor hereby grants permission by this license ("License"), free of charge, to any person or entity (the "Licensee") obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
1. All redistribution of source code or binary form, including any modifications must be under these terms. You must inform recipients that the code is governed by these conditions, and how they can obtain a copy of this license. You may not attempt to alter the conditions of who may/may not use this software.
|
|
20
|
+
|
|
21
|
+
2. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
22
|
+
|
|
23
|
+
3. This software must not be used by any organisation, website, product or service that:
|
|
24
|
+
|
|
25
|
+
a) lobbies for, promotes, or derives a majority of income from actions that support or contribute to:
|
|
26
|
+
* sex trafficking
|
|
27
|
+
* human trafficking
|
|
28
|
+
* slavery
|
|
29
|
+
* indentured servitude
|
|
30
|
+
* gambling
|
|
31
|
+
* tobacco
|
|
32
|
+
* adversely addictive behaviours
|
|
33
|
+
* nuclear energy
|
|
34
|
+
* warfare
|
|
35
|
+
* weapons manufacturing
|
|
36
|
+
* war crimes
|
|
37
|
+
* settler colonialism
|
|
38
|
+
* apartheid
|
|
39
|
+
* vaccine misinformation
|
|
40
|
+
* climate misinformation
|
|
41
|
+
* child endangerment
|
|
42
|
+
* violence (except when required to protect public safety)
|
|
43
|
+
* burning of forests
|
|
44
|
+
* deforestation
|
|
45
|
+
* hate speech or discrimination based on age, gender, gender identity, race, sexuality, religion, nationality
|
|
46
|
+
|
|
47
|
+
b) lobbies against, or derives a majority of income from actions that discourage or frustrate:
|
|
48
|
+
* peace
|
|
49
|
+
* access to the rights set out in the Universal Declaration of Human Rights and the Convention on the Rights of the Child
|
|
50
|
+
* peaceful assembly and association (including worker associations)
|
|
51
|
+
* a safe environment or action to curtail the use of fossil fuels or prevent climate change
|
|
52
|
+
* democratic processes
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
4. Compliance with Human Rights Laws and Human Rights Principles:
|
|
56
|
+
|
|
57
|
+
a. **Human Rights Laws**. The Software shall not be used by any person or entity for any systems, activities, or other uses that violate any applicable laws, regulations, or rules that protect human, civil, labor, privacy, political, environmental, security, economic, due process, or similar rights (the "Human Rights Laws"). Where the Human Rights Laws of more than one jurisdiction are applicable to the use of the Software, the Human Rights Laws that are most protective of the individuals or groups harmed shall apply.
|
|
58
|
+
|
|
59
|
+
b. **Human Rights Principles**. Licensee is advised to consult the articles of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/) and the United Nations Global Compact (https://www.unglobalcompact.org/ what-is-gc/mission/principles) that define recognized principles of international human rights (the "Human Rights Principles"). It is Licensor's express intent that all use of the Software be consistent with Human Rights Principles. If Licensor receives notification or otherwise learns of an alleged violation of any Human Rights Principles relating to Licensee's use of the Software, Licensor may in its discretion and without obligation (i) (a) notify Licensee of such allegation and (b) allow Licensee 90 days from notification under (i)(a) to investigate and respond to Licensor regarding the allegation and (ii) (a) after the earlier of 90 days from notification under (i)(a), or Licensee's response under (i)(b), notify Licensee of License termination and (b) allow Licensee an additional 90 days from notification under (ii)(a) to cease use of the Software.
|
|
60
|
+
|
|
61
|
+
c. **Indemnity**. Licensee shall hold harmless and indemnify Licensor against all losses, damages, liabilities, deficiencies, claims, actions, judgments, settlements, interest, awards, penalties, fines, costs, or expenses of whatever kind, including Licensor's reasonable attorneys' fees, arising out of or relating to Licensee's non-compliance with this License or use of the Software in violation of Human Rights Laws or Human Rights Principles.Â
|
|
62
|
+
|
|
63
|
+
5. Enforceability: If any portion or provision of this License is determined to be invalid, illegal, or unenforceable by a court of competent jurisdiction, then such invalidity, illegality, or unenforceability shall not affect any other term or provision of this License or invalidate or render unenforceable such term or provision in any other jurisdiction. Upon a determination that any term or provision is invalid, illegal, or unenforceable, to the extent permitted by applicable law, the court may modify this License to affect the original intent of the parties as closely as possible. The section headings are for convenience only and are not intended to affect the construction or interpretation of this License. Any rule of construction to the effect that ambiguities are to be resolved against the drafting party shall not apply in interpreting this License. The language in this License shall be interpreted as to its fair meaning and not strictly for or against any party.
|
|
64
|
+
|
|
65
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
66
|
+
|
|
67
|
+
This license is an extension of the Hippocratic License - an Ethical Source license (https://ethicalsource.dev) and the Do Not Harm License - (https://github.com/raisely/NoHarm)
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @open-wa/integration-s3
|
|
2
|
+
|
|
3
|
+
S3/Cloud storage integration plugin for open-wa
|
|
4
|
+
|
|
5
|
+
Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs).
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
`@open-wa/integration-s3` handles WhatsApp `message.received` events that contain decryptable media, uploads the media to an S3-compatible cloud storage provider through `pico-s3`, and attaches the resulting `cloudUrl` to the message object.
|
|
10
|
+
|
|
11
|
+
Use this integration when downstream message handlers should receive a cloud URL for media instead of only the original WhatsApp media reference.
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
The configuration type is `S3Config` in `src/config.ts`.
|
|
16
|
+
|
|
17
|
+
| Field | Required | Source-visible behavior |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `provider` | Yes | Cloud provider identifier. Source type allows `aws`, `gcp`, `do`, `wasabi`, or `backblaze`. |
|
|
20
|
+
| `accessKeyId` | Yes | Passed to `pico-s3` upload and URL generation options. |
|
|
21
|
+
| `secretAccessKey` | Yes | Passed to `pico-s3` upload and URL generation options. |
|
|
22
|
+
| `bucket` | Yes | Target bucket passed to `pico-s3`. |
|
|
23
|
+
| `region` | No | Optional region passed to `pico-s3`. |
|
|
24
|
+
| `public` | No | Optional public flag passed to `pico-s3`. |
|
|
25
|
+
| `directory` | No | Optional directory strategy or literal directory string. |
|
|
26
|
+
| `ignoreHostAccount` | No | When true, messages with `fromMe` are not uploaded. |
|
|
27
|
+
| `headers` | No | Optional headers passed to upload options. |
|
|
28
|
+
|
|
29
|
+
`directory` supports the exported `DirectoryStrategy` enum values `DATE`, `CHAT`, `DATE_CHAT`, and `CHAT_DATE`. A custom string is used directly as the upload directory.
|
|
30
|
+
|
|
31
|
+
## Runtime behavior
|
|
32
|
+
|
|
33
|
+
- On `message.received`, the plugin checks for `deprecatedMms3Url`, `mimetype`, and a client with `decryptMedia`.
|
|
34
|
+
- Matching media messages are passed to `S3Uploader.uploadMedia`.
|
|
35
|
+
- `uploadMedia` skips messages without media URL or MIME type, and skips host-account messages when `ignoreHostAccount` is true.
|
|
36
|
+
- File extensions are derived from the MIME type with the `mime` package, falling back to `bin`.
|
|
37
|
+
- File names use `message.mId` when present, otherwise `Date.now()`.
|
|
38
|
+
- Duplicate file names are tracked in memory and return the existing cloud URL instead of uploading again.
|
|
39
|
+
- Uploads run through a `PQueue` with concurrency 2, interval 1000 ms, and interval cap 2.
|
|
40
|
+
- After a successful queue upload, `getCloudUrl` is used to compute the URL and the plugin assigns it to `message.cloudUrl`.
|
|
41
|
+
- On `dispose`, the plugin waits for the upload queue to become idle and logs that the queue drained.
|
|
42
|
+
|
|
43
|
+
## Exports
|
|
44
|
+
|
|
45
|
+
- `s3Plugin` from `src/plugin.ts`.
|
|
46
|
+
- `S3Uploader` from `src/uploader.ts`.
|
|
47
|
+
- `S3Config`, `CloudProvider`, and `DirectoryStrategy` from `src/config.ts`.
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
- `pnpm --filter @open-wa/integration-s3 build`
|
|
52
|
+
- `pnpm --filter @open-wa/integration-s3 dev`
|
|
53
|
+
- `pnpm --filter @open-wa/integration-s3 lint`
|
|
54
|
+
- `pnpm --filter @open-wa/integration-s3 clean`
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
See the [docs site](https://docs.openwa.dev).
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[H-DNH V1.0](https://github.com/open-wa/wa-automate-nodejs/blob/main/LICENSE.md) - Hippocratic + Do Not Harm
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let p_queue = require("p-queue");
|
|
25
|
+
p_queue = __toESM(p_queue);
|
|
26
|
+
let pico_s3 = require("pico-s3");
|
|
27
|
+
let mime = require("mime");
|
|
28
|
+
mime = __toESM(mime);
|
|
29
|
+
//#region src/config.ts
|
|
30
|
+
let DirectoryStrategy = /* @__PURE__ */ function(DirectoryStrategy) {
|
|
31
|
+
DirectoryStrategy["DATE"] = "DATE";
|
|
32
|
+
DirectoryStrategy["CHAT"] = "CHAT";
|
|
33
|
+
DirectoryStrategy["DATE_CHAT"] = "DATE_CHAT";
|
|
34
|
+
DirectoryStrategy["CHAT_DATE"] = "CHAT_DATE";
|
|
35
|
+
return DirectoryStrategy;
|
|
36
|
+
}({});
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/uploader.ts
|
|
39
|
+
var S3Uploader = class {
|
|
40
|
+
constructor(config, logger) {
|
|
41
|
+
this.processedFiles = /* @__PURE__ */ new Set();
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.logger = logger;
|
|
44
|
+
this.uploadQueue = new p_queue.default({
|
|
45
|
+
concurrency: 2,
|
|
46
|
+
interval: 1e3,
|
|
47
|
+
intervalCap: 2
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async uploadMedia(message, client) {
|
|
51
|
+
if (!message.deprecatedMms3Url || !message.mimetype) return null;
|
|
52
|
+
if (message.fromMe && this.config.ignoreHostAccount) return null;
|
|
53
|
+
const extension = mime.default.getExtension(message.mimetype) ?? "bin";
|
|
54
|
+
const filename = `${message.mId ?? Date.now()}.${extension}`;
|
|
55
|
+
if (this.processedFiles.has(filename)) return this.getCloudUrlForFile(filename);
|
|
56
|
+
try {
|
|
57
|
+
const mediaData = await client.decryptMedia(message);
|
|
58
|
+
const opts = this.buildUploadOptions(filename, mediaData, message.from);
|
|
59
|
+
this.processedFiles.add(filename);
|
|
60
|
+
await this.uploadQueue.add(() => (0, pico_s3.upload)(opts).catch(() => void 0));
|
|
61
|
+
return this.getCloudUrlForFile(filename);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.logger.error(`Upload error: ${error.message}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
getCloudUrlForFile(filename) {
|
|
68
|
+
return (0, pico_s3.getCloudUrl)({
|
|
69
|
+
file: "",
|
|
70
|
+
filename,
|
|
71
|
+
provider: this.config.provider,
|
|
72
|
+
accessKeyId: this.config.accessKeyId,
|
|
73
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
74
|
+
bucket: this.config.bucket,
|
|
75
|
+
region: this.config.region,
|
|
76
|
+
public: this.config.public
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
buildUploadOptions(filename, mediaData, from) {
|
|
80
|
+
const opts = {
|
|
81
|
+
file: mediaData,
|
|
82
|
+
filename,
|
|
83
|
+
provider: this.config.provider,
|
|
84
|
+
accessKeyId: this.config.accessKeyId,
|
|
85
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
86
|
+
bucket: this.config.bucket,
|
|
87
|
+
region: this.config.region,
|
|
88
|
+
public: this.config.public,
|
|
89
|
+
headers: this.config.headers
|
|
90
|
+
};
|
|
91
|
+
const directory = this.resolveDirectory(from);
|
|
92
|
+
if (directory) opts.directory = directory;
|
|
93
|
+
return opts;
|
|
94
|
+
}
|
|
95
|
+
resolveDirectory(from) {
|
|
96
|
+
const dirStrat = this.config.directory;
|
|
97
|
+
if (!dirStrat) return void 0;
|
|
98
|
+
const cleanFrom = from.replace("@c.us", "").replace("@g.us", "");
|
|
99
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
100
|
+
switch (dirStrat) {
|
|
101
|
+
case DirectoryStrategy.DATE: return dateStr;
|
|
102
|
+
case DirectoryStrategy.CHAT: return cleanFrom;
|
|
103
|
+
case DirectoryStrategy.DATE_CHAT: return `${dateStr}/${cleanFrom}`;
|
|
104
|
+
case DirectoryStrategy.CHAT_DATE: return `${cleanFrom}/${dateStr}`;
|
|
105
|
+
default: return typeof dirStrat === "string" ? dirStrat : void 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async waitForQueue() {
|
|
109
|
+
await this.uploadQueue.onIdle();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/plugin.ts
|
|
114
|
+
function s3Plugin(config) {
|
|
115
|
+
return async (input) => {
|
|
116
|
+
const { logger, client } = input;
|
|
117
|
+
const uploader = new S3Uploader(config, logger);
|
|
118
|
+
const getWAClient = () => {
|
|
119
|
+
if (client && typeof client === "object" && "decryptMedia" in client) return client;
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
"message.received": async ({ message }) => {
|
|
124
|
+
const msg = message;
|
|
125
|
+
const waClient = getWAClient();
|
|
126
|
+
if (msg.deprecatedMms3Url && msg.mimetype && waClient) {
|
|
127
|
+
const cloudUrl = await uploader.uploadMedia(msg, waClient);
|
|
128
|
+
if (cloudUrl) msg.cloudUrl = cloudUrl;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
dispose: async () => {
|
|
132
|
+
await uploader.waitForQueue();
|
|
133
|
+
logger.info("S3 uploader queue drained");
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
exports.DirectoryStrategy = DirectoryStrategy;
|
|
140
|
+
exports.S3Uploader = S3Uploader;
|
|
141
|
+
exports.s3Plugin = s3Plugin;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin } from "@open-wa/core";
|
|
2
|
+
import { Logger } from "@open-wa/logger";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
type CloudProvider = 'aws' | 'gcp' | 'do' | 'wasabi' | 'backblaze';
|
|
6
|
+
declare enum DirectoryStrategy {
|
|
7
|
+
DATE = "DATE",
|
|
8
|
+
CHAT = "CHAT",
|
|
9
|
+
DATE_CHAT = "DATE_CHAT",
|
|
10
|
+
CHAT_DATE = "CHAT_DATE"
|
|
11
|
+
}
|
|
12
|
+
interface S3Config {
|
|
13
|
+
provider: CloudProvider;
|
|
14
|
+
accessKeyId: string;
|
|
15
|
+
secretAccessKey: string;
|
|
16
|
+
bucket: string;
|
|
17
|
+
region?: string;
|
|
18
|
+
public?: boolean;
|
|
19
|
+
directory?: DirectoryStrategy | string;
|
|
20
|
+
ignoreHostAccount?: boolean;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/plugin.d.ts
|
|
25
|
+
declare function s3Plugin(config: S3Config): Plugin;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/uploader.d.ts
|
|
28
|
+
interface MediaMessage {
|
|
29
|
+
mId?: string;
|
|
30
|
+
mimetype: string;
|
|
31
|
+
from: string;
|
|
32
|
+
fromMe?: boolean;
|
|
33
|
+
deprecatedMms3Url?: string;
|
|
34
|
+
}
|
|
35
|
+
interface WAClient {
|
|
36
|
+
decryptMedia: (message: unknown) => Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
declare class S3Uploader {
|
|
39
|
+
private readonly config;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
private readonly uploadQueue;
|
|
42
|
+
private readonly processedFiles;
|
|
43
|
+
constructor(config: S3Config, logger: Logger);
|
|
44
|
+
uploadMedia(message: MediaMessage, client: WAClient): Promise<string | null>;
|
|
45
|
+
getCloudUrlForFile(filename: string): string;
|
|
46
|
+
private buildUploadOptions;
|
|
47
|
+
private resolveDirectory;
|
|
48
|
+
waitForQueue(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { type CloudProvider, DirectoryStrategy, type S3Config, S3Uploader, s3Plugin };
|
|
52
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/config.ts","../src/plugin.ts","../src/uploader.ts"],"mappings":";;;;KAAY,aAAA;AAAA,aAEA,iBAAA;EACV,IAAA;EACA,IAAA;EACA,SAAA;EACA,SAAA;AAAA;AAAA,UAGe,QAAA;EACf,QAAA,EAAU,aAAA;EACV,WAAA;EACA,eAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,SAAA,GAAY,iBAAA;EACZ,iBAAA;EACA,OAAA,GAAU,MAAA;AAAA;;;iBCJI,QAAA,CAAS,MAAA,EAAQ,QAAA,GAAW,MAAA;;;UCRlC,YAAA;EACR,GAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA,iBAAA;AAAA;AAAA,UAGQ,QAAA;EACR,YAAA,GAAe,OAAA,cAAqB,OAAA;AAAA;AAAA,cAgBzB,UAAA;EAAA,iBACM,MAAA;EAAA,iBACA,MAAA;EAAA,iBACA,WAAA;EAAA,iBACA,cAAA;cAEL,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,MAAA;EAUhC,WAAA,CAAY,OAAA,EAAS,YAAA,EAAc,MAAA,EAAQ,QAAA,GAAW,OAAA;EA8B5D,kBAAA,CAAmB,QAAA;EAAA,QAcX,kBAAA;EAAA,QAqBA,gBAAA;EAqBF,YAAA,CAAA,GAAgB,OAAA;AAAA"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin } from "@open-wa/core";
|
|
2
|
+
import { Logger } from "@open-wa/logger";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
type CloudProvider = 'aws' | 'gcp' | 'do' | 'wasabi' | 'backblaze';
|
|
6
|
+
declare enum DirectoryStrategy {
|
|
7
|
+
DATE = "DATE",
|
|
8
|
+
CHAT = "CHAT",
|
|
9
|
+
DATE_CHAT = "DATE_CHAT",
|
|
10
|
+
CHAT_DATE = "CHAT_DATE"
|
|
11
|
+
}
|
|
12
|
+
interface S3Config {
|
|
13
|
+
provider: CloudProvider;
|
|
14
|
+
accessKeyId: string;
|
|
15
|
+
secretAccessKey: string;
|
|
16
|
+
bucket: string;
|
|
17
|
+
region?: string;
|
|
18
|
+
public?: boolean;
|
|
19
|
+
directory?: DirectoryStrategy | string;
|
|
20
|
+
ignoreHostAccount?: boolean;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/plugin.d.ts
|
|
25
|
+
declare function s3Plugin(config: S3Config): Plugin;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/uploader.d.ts
|
|
28
|
+
interface MediaMessage {
|
|
29
|
+
mId?: string;
|
|
30
|
+
mimetype: string;
|
|
31
|
+
from: string;
|
|
32
|
+
fromMe?: boolean;
|
|
33
|
+
deprecatedMms3Url?: string;
|
|
34
|
+
}
|
|
35
|
+
interface WAClient {
|
|
36
|
+
decryptMedia: (message: unknown) => Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
declare class S3Uploader {
|
|
39
|
+
private readonly config;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
private readonly uploadQueue;
|
|
42
|
+
private readonly processedFiles;
|
|
43
|
+
constructor(config: S3Config, logger: Logger);
|
|
44
|
+
uploadMedia(message: MediaMessage, client: WAClient): Promise<string | null>;
|
|
45
|
+
getCloudUrlForFile(filename: string): string;
|
|
46
|
+
private buildUploadOptions;
|
|
47
|
+
private resolveDirectory;
|
|
48
|
+
waitForQueue(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { type CloudProvider, DirectoryStrategy, type S3Config, S3Uploader, s3Plugin };
|
|
52
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/plugin.ts","../src/uploader.ts"],"mappings":";;;;KAAY,aAAA;AAAA,aAEA,iBAAA;EACV,IAAA;EACA,IAAA;EACA,SAAA;EACA,SAAA;AAAA;AAAA,UAGe,QAAA;EACf,QAAA,EAAU,aAAA;EACV,WAAA;EACA,eAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,SAAA,GAAY,iBAAA;EACZ,iBAAA;EACA,OAAA,GAAU,MAAA;AAAA;;;iBCJI,QAAA,CAAS,MAAA,EAAQ,QAAA,GAAW,MAAA;;;UCRlC,YAAA;EACR,GAAA;EACA,QAAA;EACA,IAAA;EACA,MAAA;EACA,iBAAA;AAAA;AAAA,UAGQ,QAAA;EACR,YAAA,GAAe,OAAA,cAAqB,OAAA;AAAA;AAAA,cAgBzB,UAAA;EAAA,iBACM,MAAA;EAAA,iBACA,MAAA;EAAA,iBACA,WAAA;EAAA,iBACA,cAAA;cAEL,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,MAAA;EAUhC,WAAA,CAAY,OAAA,EAAS,YAAA,EAAc,MAAA,EAAQ,QAAA,GAAW,OAAA;EA8B5D,kBAAA,CAAmB,QAAA;EAAA,QAcX,kBAAA;EAAA,QAqBA,gBAAA;EAqBF,YAAA,CAAA,GAAgB,OAAA;AAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import PQueue from "p-queue";
|
|
2
|
+
import { getCloudUrl, upload } from "pico-s3";
|
|
3
|
+
import mime from "mime";
|
|
4
|
+
//#region src/config.ts
|
|
5
|
+
let DirectoryStrategy = /* @__PURE__ */ function(DirectoryStrategy) {
|
|
6
|
+
DirectoryStrategy["DATE"] = "DATE";
|
|
7
|
+
DirectoryStrategy["CHAT"] = "CHAT";
|
|
8
|
+
DirectoryStrategy["DATE_CHAT"] = "DATE_CHAT";
|
|
9
|
+
DirectoryStrategy["CHAT_DATE"] = "CHAT_DATE";
|
|
10
|
+
return DirectoryStrategy;
|
|
11
|
+
}({});
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/uploader.ts
|
|
14
|
+
var S3Uploader = class {
|
|
15
|
+
constructor(config, logger) {
|
|
16
|
+
this.processedFiles = /* @__PURE__ */ new Set();
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
this.uploadQueue = new PQueue({
|
|
20
|
+
concurrency: 2,
|
|
21
|
+
interval: 1e3,
|
|
22
|
+
intervalCap: 2
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async uploadMedia(message, client) {
|
|
26
|
+
if (!message.deprecatedMms3Url || !message.mimetype) return null;
|
|
27
|
+
if (message.fromMe && this.config.ignoreHostAccount) return null;
|
|
28
|
+
const extension = mime.getExtension(message.mimetype) ?? "bin";
|
|
29
|
+
const filename = `${message.mId ?? Date.now()}.${extension}`;
|
|
30
|
+
if (this.processedFiles.has(filename)) return this.getCloudUrlForFile(filename);
|
|
31
|
+
try {
|
|
32
|
+
const mediaData = await client.decryptMedia(message);
|
|
33
|
+
const opts = this.buildUploadOptions(filename, mediaData, message.from);
|
|
34
|
+
this.processedFiles.add(filename);
|
|
35
|
+
await this.uploadQueue.add(() => upload(opts).catch(() => void 0));
|
|
36
|
+
return this.getCloudUrlForFile(filename);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
this.logger.error(`Upload error: ${error.message}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
getCloudUrlForFile(filename) {
|
|
43
|
+
return getCloudUrl({
|
|
44
|
+
file: "",
|
|
45
|
+
filename,
|
|
46
|
+
provider: this.config.provider,
|
|
47
|
+
accessKeyId: this.config.accessKeyId,
|
|
48
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
49
|
+
bucket: this.config.bucket,
|
|
50
|
+
region: this.config.region,
|
|
51
|
+
public: this.config.public
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
buildUploadOptions(filename, mediaData, from) {
|
|
55
|
+
const opts = {
|
|
56
|
+
file: mediaData,
|
|
57
|
+
filename,
|
|
58
|
+
provider: this.config.provider,
|
|
59
|
+
accessKeyId: this.config.accessKeyId,
|
|
60
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
61
|
+
bucket: this.config.bucket,
|
|
62
|
+
region: this.config.region,
|
|
63
|
+
public: this.config.public,
|
|
64
|
+
headers: this.config.headers
|
|
65
|
+
};
|
|
66
|
+
const directory = this.resolveDirectory(from);
|
|
67
|
+
if (directory) opts.directory = directory;
|
|
68
|
+
return opts;
|
|
69
|
+
}
|
|
70
|
+
resolveDirectory(from) {
|
|
71
|
+
const dirStrat = this.config.directory;
|
|
72
|
+
if (!dirStrat) return void 0;
|
|
73
|
+
const cleanFrom = from.replace("@c.us", "").replace("@g.us", "");
|
|
74
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
75
|
+
switch (dirStrat) {
|
|
76
|
+
case DirectoryStrategy.DATE: return dateStr;
|
|
77
|
+
case DirectoryStrategy.CHAT: return cleanFrom;
|
|
78
|
+
case DirectoryStrategy.DATE_CHAT: return `${dateStr}/${cleanFrom}`;
|
|
79
|
+
case DirectoryStrategy.CHAT_DATE: return `${cleanFrom}/${dateStr}`;
|
|
80
|
+
default: return typeof dirStrat === "string" ? dirStrat : void 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async waitForQueue() {
|
|
84
|
+
await this.uploadQueue.onIdle();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/plugin.ts
|
|
89
|
+
function s3Plugin(config) {
|
|
90
|
+
return async (input) => {
|
|
91
|
+
const { logger, client } = input;
|
|
92
|
+
const uploader = new S3Uploader(config, logger);
|
|
93
|
+
const getWAClient = () => {
|
|
94
|
+
if (client && typeof client === "object" && "decryptMedia" in client) return client;
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
"message.received": async ({ message }) => {
|
|
99
|
+
const msg = message;
|
|
100
|
+
const waClient = getWAClient();
|
|
101
|
+
if (msg.deprecatedMms3Url && msg.mimetype && waClient) {
|
|
102
|
+
const cloudUrl = await uploader.uploadMedia(msg, waClient);
|
|
103
|
+
if (cloudUrl) msg.cloudUrl = cloudUrl;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
dispose: async () => {
|
|
107
|
+
await uploader.waitForQueue();
|
|
108
|
+
logger.info("S3 uploader queue drained");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { DirectoryStrategy, S3Uploader, s3Plugin };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/config.ts","../src/uploader.ts","../src/plugin.ts"],"sourcesContent":["export type CloudProvider = 'aws' | 'gcp' | 'do' | 'wasabi' | 'backblaze';\n\nexport enum DirectoryStrategy {\n DATE = 'DATE',\n CHAT = 'CHAT',\n DATE_CHAT = 'DATE_CHAT',\n CHAT_DATE = 'CHAT_DATE',\n}\n\nexport interface S3Config {\n provider: CloudProvider;\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n region?: string;\n public?: boolean;\n directory?: DirectoryStrategy | string;\n ignoreHostAccount?: boolean;\n headers?: Record<string, string>;\n}\n","import PQueue from 'p-queue';\nimport { upload, getCloudUrl } from 'pico-s3';\nimport mime from 'mime';\nimport type { Logger } from '@open-wa/logger';\nimport { type S3Config, DirectoryStrategy } from './config.js';\n\ninterface MediaMessage {\n mId?: string;\n mimetype: string;\n from: string;\n fromMe?: boolean;\n deprecatedMms3Url?: string;\n}\n\ninterface WAClient {\n decryptMedia: (message: unknown) => Promise<string>;\n}\n\ninterface S3UploadOpts {\n file: string;\n filename: string;\n provider: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n region?: string;\n public?: boolean;\n headers?: Record<string, string>;\n directory?: string;\n}\n\nexport class S3Uploader {\n private readonly config: S3Config;\n private readonly logger: Logger;\n private readonly uploadQueue: PQueue;\n private readonly processedFiles: Set<string> = new Set();\n\n constructor(config: S3Config, logger: Logger) {\n this.config = config;\n this.logger = logger;\n this.uploadQueue = new PQueue({\n concurrency: 2,\n interval: 1000,\n intervalCap: 2,\n });\n }\n\n async uploadMedia(message: MediaMessage, client: WAClient): Promise<string | null> {\n if (!message.deprecatedMms3Url || !message.mimetype) {\n return null;\n }\n\n if (message.fromMe && this.config.ignoreHostAccount) {\n return null;\n }\n\n const extension = mime.getExtension(message.mimetype) ?? 'bin';\n const filename = `${message.mId ?? Date.now()}.${extension}`;\n\n if (this.processedFiles.has(filename)) {\n return this.getCloudUrlForFile(filename);\n }\n\n try {\n const mediaData = await client.decryptMedia(message);\n const opts = this.buildUploadOptions(filename, mediaData, message.from);\n\n this.processedFiles.add(filename);\n await this.uploadQueue.add(() => upload(opts as never).catch(() => undefined));\n\n return this.getCloudUrlForFile(filename);\n } catch (error) {\n this.logger.error(`Upload error: ${(error as Error).message}`);\n return null;\n }\n }\n\n getCloudUrlForFile(filename: string): string {\n const opts: S3UploadOpts = {\n file: '',\n filename,\n provider: this.config.provider,\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey,\n bucket: this.config.bucket,\n region: this.config.region,\n public: this.config.public,\n };\n return getCloudUrl(opts as never);\n }\n\n private buildUploadOptions(filename: string, mediaData: string, from: string): S3UploadOpts {\n const opts: S3UploadOpts = {\n file: mediaData,\n filename,\n provider: this.config.provider,\n accessKeyId: this.config.accessKeyId,\n secretAccessKey: this.config.secretAccessKey,\n bucket: this.config.bucket,\n region: this.config.region,\n public: this.config.public,\n headers: this.config.headers,\n };\n\n const directory = this.resolveDirectory(from);\n if (directory) {\n opts.directory = directory;\n }\n\n return opts;\n }\n\n private resolveDirectory(from: string): string | undefined {\n const dirStrat = this.config.directory;\n if (!dirStrat) return undefined;\n\n const cleanFrom = from.replace('@c.us', '').replace('@g.us', '');\n const dateStr = new Date().toISOString().slice(0, 10);\n\n switch (dirStrat) {\n case DirectoryStrategy.DATE:\n return dateStr;\n case DirectoryStrategy.CHAT:\n return cleanFrom;\n case DirectoryStrategy.DATE_CHAT:\n return `${dateStr}/${cleanFrom}`;\n case DirectoryStrategy.CHAT_DATE:\n return `${cleanFrom}/${dateStr}`;\n default:\n return typeof dirStrat === 'string' ? dirStrat : undefined;\n }\n }\n\n async waitForQueue(): Promise<void> {\n await this.uploadQueue.onIdle();\n }\n}\n","import type { Plugin, PluginInput, Hooks } from '@open-wa/core';\nimport type { S3Config } from './config.js';\nimport { S3Uploader } from './uploader.js';\n\ninterface MediaMessage {\n deprecatedMms3Url?: string;\n mimetype?: string;\n cloudUrl?: string;\n}\n\ninterface WAClient {\n decryptMedia: (message: unknown) => Promise<string>;\n}\n\nexport function s3Plugin(config: S3Config): Plugin {\n return async (input: PluginInput): Promise<Hooks> => {\n const { logger, client } = input;\n const uploader = new S3Uploader(config, logger);\n\n const getWAClient = (): WAClient | null => {\n if (client && typeof client === 'object' && 'decryptMedia' in client) {\n return client as WAClient;\n }\n return null;\n };\n\n return {\n 'message.received': async ({ message }) => {\n const msg = message as MediaMessage;\n const waClient = getWAClient();\n\n if (msg.deprecatedMms3Url && msg.mimetype && waClient) {\n const cloudUrl = await uploader.uploadMedia(msg as never, waClient);\n if (cloudUrl) {\n msg.cloudUrl = cloudUrl;\n }\n }\n },\n\n dispose: async () => {\n await uploader.waitForQueue();\n logger.info('S3 uploader queue drained');\n },\n };\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-wa/integration-s3",
|
|
3
|
+
"version": "5.0.0-alpha.0",
|
|
4
|
+
"description": "S3/Cloud storage integration plugin for open-wa",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"types": "./dist/index.d.mts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"mime": "^4.0.0",
|
|
17
|
+
"p-queue": "^8.0.0",
|
|
18
|
+
"pico-s3": "^1.0.0",
|
|
19
|
+
"@open-wa/core": "5.0.0-alpha.0",
|
|
20
|
+
"@open-wa/logger": "5.0.0-alpha.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.9.1",
|
|
24
|
+
"typescript": "^6.0.3"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/open-wa/wa-automate-nodejs.git"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown src/index.ts --format esm --format cjs",
|
|
36
|
+
"dev": "tsdown src/index.ts --format esm --format cjs --watch",
|
|
37
|
+
"lint": "oxlint src",
|
|
38
|
+
"clean": "rimraf dist"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CloudProvider = 'aws' | 'gcp' | 'do' | 'wasabi' | 'backblaze';
|
|
2
|
+
|
|
3
|
+
export enum DirectoryStrategy {
|
|
4
|
+
DATE = 'DATE',
|
|
5
|
+
CHAT = 'CHAT',
|
|
6
|
+
DATE_CHAT = 'DATE_CHAT',
|
|
7
|
+
CHAT_DATE = 'CHAT_DATE',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface S3Config {
|
|
11
|
+
provider: CloudProvider;
|
|
12
|
+
accessKeyId: string;
|
|
13
|
+
secretAccessKey: string;
|
|
14
|
+
bucket: string;
|
|
15
|
+
region?: string;
|
|
16
|
+
public?: boolean;
|
|
17
|
+
directory?: DirectoryStrategy | string;
|
|
18
|
+
ignoreHostAccount?: boolean;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}
|
package/src/index.ts
ADDED
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Plugin, PluginInput, Hooks } from '@open-wa/core';
|
|
2
|
+
import type { S3Config } from './config.js';
|
|
3
|
+
import { S3Uploader } from './uploader.js';
|
|
4
|
+
|
|
5
|
+
interface MediaMessage {
|
|
6
|
+
deprecatedMms3Url?: string;
|
|
7
|
+
mimetype?: string;
|
|
8
|
+
cloudUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface WAClient {
|
|
12
|
+
decryptMedia: (message: unknown) => Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function s3Plugin(config: S3Config): Plugin {
|
|
16
|
+
return async (input: PluginInput): Promise<Hooks> => {
|
|
17
|
+
const { logger, client } = input;
|
|
18
|
+
const uploader = new S3Uploader(config, logger);
|
|
19
|
+
|
|
20
|
+
const getWAClient = (): WAClient | null => {
|
|
21
|
+
if (client && typeof client === 'object' && 'decryptMedia' in client) {
|
|
22
|
+
return client as WAClient;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
'message.received': async ({ message }) => {
|
|
29
|
+
const msg = message as MediaMessage;
|
|
30
|
+
const waClient = getWAClient();
|
|
31
|
+
|
|
32
|
+
if (msg.deprecatedMms3Url && msg.mimetype && waClient) {
|
|
33
|
+
const cloudUrl = await uploader.uploadMedia(msg as never, waClient);
|
|
34
|
+
if (cloudUrl) {
|
|
35
|
+
msg.cloudUrl = cloudUrl;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
dispose: async () => {
|
|
41
|
+
await uploader.waitForQueue();
|
|
42
|
+
logger.info('S3 uploader queue drained');
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/uploader.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import PQueue from 'p-queue';
|
|
2
|
+
import { upload, getCloudUrl } from 'pico-s3';
|
|
3
|
+
import mime from 'mime';
|
|
4
|
+
import type { Logger } from '@open-wa/logger';
|
|
5
|
+
import { type S3Config, DirectoryStrategy } from './config.js';
|
|
6
|
+
|
|
7
|
+
interface MediaMessage {
|
|
8
|
+
mId?: string;
|
|
9
|
+
mimetype: string;
|
|
10
|
+
from: string;
|
|
11
|
+
fromMe?: boolean;
|
|
12
|
+
deprecatedMms3Url?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface WAClient {
|
|
16
|
+
decryptMedia: (message: unknown) => Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface S3UploadOpts {
|
|
20
|
+
file: string;
|
|
21
|
+
filename: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
accessKeyId: string;
|
|
24
|
+
secretAccessKey: string;
|
|
25
|
+
bucket: string;
|
|
26
|
+
region?: string;
|
|
27
|
+
public?: boolean;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
directory?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class S3Uploader {
|
|
33
|
+
private readonly config: S3Config;
|
|
34
|
+
private readonly logger: Logger;
|
|
35
|
+
private readonly uploadQueue: PQueue;
|
|
36
|
+
private readonly processedFiles: Set<string> = new Set();
|
|
37
|
+
|
|
38
|
+
constructor(config: S3Config, logger: Logger) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.logger = logger;
|
|
41
|
+
this.uploadQueue = new PQueue({
|
|
42
|
+
concurrency: 2,
|
|
43
|
+
interval: 1000,
|
|
44
|
+
intervalCap: 2,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async uploadMedia(message: MediaMessage, client: WAClient): Promise<string | null> {
|
|
49
|
+
if (!message.deprecatedMms3Url || !message.mimetype) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (message.fromMe && this.config.ignoreHostAccount) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const extension = mime.getExtension(message.mimetype) ?? 'bin';
|
|
58
|
+
const filename = `${message.mId ?? Date.now()}.${extension}`;
|
|
59
|
+
|
|
60
|
+
if (this.processedFiles.has(filename)) {
|
|
61
|
+
return this.getCloudUrlForFile(filename);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const mediaData = await client.decryptMedia(message);
|
|
66
|
+
const opts = this.buildUploadOptions(filename, mediaData, message.from);
|
|
67
|
+
|
|
68
|
+
this.processedFiles.add(filename);
|
|
69
|
+
await this.uploadQueue.add(() => upload(opts as never).catch(() => undefined));
|
|
70
|
+
|
|
71
|
+
return this.getCloudUrlForFile(filename);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
this.logger.error(`Upload error: ${(error as Error).message}`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getCloudUrlForFile(filename: string): string {
|
|
79
|
+
const opts: S3UploadOpts = {
|
|
80
|
+
file: '',
|
|
81
|
+
filename,
|
|
82
|
+
provider: this.config.provider,
|
|
83
|
+
accessKeyId: this.config.accessKeyId,
|
|
84
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
85
|
+
bucket: this.config.bucket,
|
|
86
|
+
region: this.config.region,
|
|
87
|
+
public: this.config.public,
|
|
88
|
+
};
|
|
89
|
+
return getCloudUrl(opts as never);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private buildUploadOptions(filename: string, mediaData: string, from: string): S3UploadOpts {
|
|
93
|
+
const opts: S3UploadOpts = {
|
|
94
|
+
file: mediaData,
|
|
95
|
+
filename,
|
|
96
|
+
provider: this.config.provider,
|
|
97
|
+
accessKeyId: this.config.accessKeyId,
|
|
98
|
+
secretAccessKey: this.config.secretAccessKey,
|
|
99
|
+
bucket: this.config.bucket,
|
|
100
|
+
region: this.config.region,
|
|
101
|
+
public: this.config.public,
|
|
102
|
+
headers: this.config.headers,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const directory = this.resolveDirectory(from);
|
|
106
|
+
if (directory) {
|
|
107
|
+
opts.directory = directory;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return opts;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private resolveDirectory(from: string): string | undefined {
|
|
114
|
+
const dirStrat = this.config.directory;
|
|
115
|
+
if (!dirStrat) return undefined;
|
|
116
|
+
|
|
117
|
+
const cleanFrom = from.replace('@c.us', '').replace('@g.us', '');
|
|
118
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
119
|
+
|
|
120
|
+
switch (dirStrat) {
|
|
121
|
+
case DirectoryStrategy.DATE:
|
|
122
|
+
return dateStr;
|
|
123
|
+
case DirectoryStrategy.CHAT:
|
|
124
|
+
return cleanFrom;
|
|
125
|
+
case DirectoryStrategy.DATE_CHAT:
|
|
126
|
+
return `${dateStr}/${cleanFrom}`;
|
|
127
|
+
case DirectoryStrategy.CHAT_DATE:
|
|
128
|
+
return `${cleanFrom}/${dateStr}`;
|
|
129
|
+
default:
|
|
130
|
+
return typeof dirStrat === 'string' ? dirStrat : undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async waitForQueue(): Promise<void> {
|
|
135
|
+
await this.uploadQueue.onIdle();
|
|
136
|
+
}
|
|
137
|
+
}
|
package/tsconfig.json
ADDED