@open-wa/integration-s3 5.0.0-alpha.7 → 5.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/LICENSE.md +2 -2
- package/README.md +6 -6
- package/dist/index.cjs +55 -27
- package/dist/index.d.cts +12 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +12 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +51 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -8
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -69
- package/src/config.ts +0 -20
- package/src/index.ts +0 -3
- package/src/plugin.ts +0 -46
- package/src/uploader.ts +0 -137
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -5
package/LICENSE.md
CHANGED
|
@@ -55,9 +55,9 @@ Licensor hereby grants permission by this license ("License"), free of charge,
|
|
|
55
55
|
4. Compliance with Human Rights Laws and Human Rights Principles:
|
|
56
56
|
|
|
57
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
|
-
|
|
58
|
+
|
|
59
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
|
-
|
|
60
|
+
|
|
61
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
62
|
|
|
63
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.
|
package/README.md
CHANGED
|
@@ -6,15 +6,15 @@ Part of the [@open-wa v5 monorepo](https://github.com/open-wa/wa-automate-nodejs
|
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
9
|
-
`@open-wa/integration-s3` handles
|
|
9
|
+
`@open-wa/integration-s3` handles `message.received` events that contain decryptable media. It uploads media through `pico-s3` to S3-compatible storage. It adds the resulting `cloudUrl` to the message object.
|
|
10
10
|
|
|
11
|
-
Use this integration
|
|
11
|
+
Use this integration so downstream message handlers receive a cloud URL instead of only the original WhatsApp media reference.
|
|
12
12
|
|
|
13
13
|
## Configuration
|
|
14
14
|
|
|
15
15
|
The configuration type is `S3Config` in `src/config.ts`.
|
|
16
16
|
|
|
17
|
-
| Field |
|
|
17
|
+
| Field | Necessary | Source-visible behavior |
|
|
18
18
|
| --- | --- | --- |
|
|
19
19
|
| `provider` | Yes | Cloud provider identifier. Source type allows `aws`, `gcp`, `do`, `wasabi`, or `backblaze`. |
|
|
20
20
|
| `accessKeyId` | Yes | Passed to `pico-s3` upload and URL generation options. |
|
|
@@ -36,7 +36,7 @@ The configuration type is `S3Config` in `src/config.ts`.
|
|
|
36
36
|
- File extensions are derived from the MIME type with the `mime` package, falling back to `bin`.
|
|
37
37
|
- File names use `message.mId` when present, otherwise `Date.now()`.
|
|
38
38
|
- Duplicate file names are tracked in memory and return the existing cloud URL instead of uploading again.
|
|
39
|
-
- Uploads run through a
|
|
39
|
+
- Uploads run through a bounded Effect queue with concurrency 2 and a two-per-second rate limit.
|
|
40
40
|
- After a successful queue upload, `getCloudUrl` is used to compute the URL and the plugin assigns it to `message.cloudUrl`.
|
|
41
41
|
- On `dispose`, the plugin waits for the upload queue to become idle and logs that the queue drained.
|
|
42
42
|
|
|
@@ -55,8 +55,8 @@ The configuration type is `S3Config` in `src/config.ts`.
|
|
|
55
55
|
|
|
56
56
|
## Documentation
|
|
57
57
|
|
|
58
|
-
See the [docs site](https://
|
|
58
|
+
See the [docs site](https://openwa.dev).
|
|
59
59
|
|
|
60
60
|
## License
|
|
61
61
|
|
|
62
|
-
[H-DNH
|
|
62
|
+
[H-DNH 1.1](https://github.com/open-wa/wa-automate-nodejs/blob/master/LICENSE.md) - Hippocratic + Do Not Harm
|
package/dist/index.cjs
CHANGED
|
@@ -16,16 +16,16 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
20
|
value: mod,
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
-
let
|
|
25
|
-
p_queue = __toESM(p_queue);
|
|
24
|
+
let effect = require("effect");
|
|
26
25
|
let pico_s3 = require("pico-s3");
|
|
27
26
|
let mime = require("mime");
|
|
28
|
-
mime = __toESM(mime);
|
|
27
|
+
mime = __toESM(mime, 1);
|
|
28
|
+
let _open_wa_runtime_core = require("@open-wa/runtime-core");
|
|
29
29
|
//#region src/config.ts
|
|
30
30
|
let DirectoryStrategy = /* @__PURE__ */ function(DirectoryStrategy) {
|
|
31
31
|
DirectoryStrategy["DATE"] = "DATE";
|
|
@@ -41,10 +41,15 @@ var S3Uploader = class {
|
|
|
41
41
|
this.processedFiles = /* @__PURE__ */ new Set();
|
|
42
42
|
this.config = config;
|
|
43
43
|
this.logger = logger;
|
|
44
|
-
this.uploadQueue =
|
|
44
|
+
this.uploadQueue = _open_wa_runtime_core.ScopedTaskQueue.make({
|
|
45
|
+
name: "s3.upload",
|
|
45
46
|
concurrency: 2,
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
capacity: 64,
|
|
48
|
+
overload: "backpressure",
|
|
49
|
+
rate: {
|
|
50
|
+
intervalMs: 1e3,
|
|
51
|
+
limit: 2
|
|
52
|
+
}
|
|
48
53
|
});
|
|
49
54
|
}
|
|
50
55
|
async uploadMedia(message, client) {
|
|
@@ -56,8 +61,8 @@ var S3Uploader = class {
|
|
|
56
61
|
try {
|
|
57
62
|
const mediaData = await client.decryptMedia(message);
|
|
58
63
|
const opts = this.buildUploadOptions(filename, mediaData, message.from);
|
|
64
|
+
await (await this.uploadQueue).submit(() => (0, pico_s3.upload)(opts), filename);
|
|
59
65
|
this.processedFiles.add(filename);
|
|
60
|
-
await this.uploadQueue.add(() => (0, pico_s3.upload)(opts).catch(() => void 0));
|
|
61
66
|
return this.getCloudUrlForFile(filename);
|
|
62
67
|
} catch (error) {
|
|
63
68
|
this.logger.error(`Upload error: ${error.message}`);
|
|
@@ -65,7 +70,7 @@ var S3Uploader = class {
|
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
getCloudUrlForFile(filename) {
|
|
68
|
-
|
|
73
|
+
const opts = {
|
|
69
74
|
file: "",
|
|
70
75
|
filename,
|
|
71
76
|
provider: this.config.provider,
|
|
@@ -74,7 +79,8 @@ var S3Uploader = class {
|
|
|
74
79
|
bucket: this.config.bucket,
|
|
75
80
|
region: this.config.region,
|
|
76
81
|
public: this.config.public
|
|
77
|
-
}
|
|
82
|
+
};
|
|
83
|
+
return (0, pico_s3.getCloudUrl)(opts);
|
|
78
84
|
}
|
|
79
85
|
buildUploadOptions(filename, mediaData, from) {
|
|
80
86
|
const opts = {
|
|
@@ -98,44 +104,66 @@ var S3Uploader = class {
|
|
|
98
104
|
const cleanFrom = from.replace("@c.us", "").replace("@g.us", "");
|
|
99
105
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
100
106
|
switch (dirStrat) {
|
|
101
|
-
case
|
|
102
|
-
case
|
|
103
|
-
case
|
|
104
|
-
case
|
|
107
|
+
case "DATE": return dateStr;
|
|
108
|
+
case "CHAT": return cleanFrom;
|
|
109
|
+
case "DATE_CHAT": return `${dateStr}/${cleanFrom}`;
|
|
110
|
+
case "CHAT_DATE": return `${cleanFrom}/${dateStr}`;
|
|
105
111
|
default: return typeof dirStrat === "string" ? dirStrat : void 0;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
async waitForQueue() {
|
|
109
|
-
await this.uploadQueue.
|
|
115
|
+
await (await this.uploadQueue).waitForIdle();
|
|
116
|
+
}
|
|
117
|
+
async close() {
|
|
118
|
+
await (await this.uploadQueue).close();
|
|
110
119
|
}
|
|
111
120
|
};
|
|
112
121
|
//#endregion
|
|
122
|
+
//#region src/service.ts
|
|
123
|
+
const S3UploaderService = effect.Context.Service("@open-wa/integration-s3/S3Uploader");
|
|
124
|
+
const s3UploaderTestLayer = (uploader) => effect.Layer.succeed(S3UploaderService, uploader);
|
|
125
|
+
const s3UploaderLayer = (config, logger) => effect.Layer.effect(S3UploaderService, effect.Effect.acquireRelease(effect.Effect.sync(() => new S3Uploader(config, logger)), (uploader) => effect.Effect.promise(async () => {
|
|
126
|
+
await uploader.waitForQueue();
|
|
127
|
+
await uploader.close();
|
|
128
|
+
})));
|
|
129
|
+
//#endregion
|
|
113
130
|
//#region src/plugin.ts
|
|
114
131
|
function s3Plugin(config) {
|
|
115
|
-
return async (input) => {
|
|
132
|
+
return Object.assign(async (input) => {
|
|
116
133
|
const { logger, client } = input;
|
|
117
|
-
const
|
|
134
|
+
const runtime = effect.ManagedRuntime.make(s3UploaderLayer(config, logger));
|
|
135
|
+
const uploader = await runtime.runPromise(S3UploaderService);
|
|
118
136
|
const getWAClient = () => {
|
|
119
137
|
if (client && typeof client === "object" && "decryptMedia" in client) return client;
|
|
120
138
|
return null;
|
|
121
139
|
};
|
|
122
140
|
return {
|
|
123
|
-
"message.received":
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
"message.received": ({ message }) => runtime.runPromise(effect.Effect.tryPromise({
|
|
142
|
+
try: async () => {
|
|
143
|
+
const msg = message;
|
|
144
|
+
const waClient = getWAClient();
|
|
145
|
+
if (msg.deprecatedMms3Url && msg.mimetype && waClient) {
|
|
146
|
+
const cloudUrl = await uploader.uploadMedia(msg, waClient);
|
|
147
|
+
if (cloudUrl) msg.cloudUrl = cloudUrl;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
catch: (cause) => cause
|
|
151
|
+
})),
|
|
131
152
|
dispose: async () => {
|
|
132
|
-
await
|
|
153
|
+
await runtime.dispose();
|
|
133
154
|
logger.info("S3 uploader queue drained");
|
|
134
155
|
}
|
|
135
156
|
};
|
|
136
|
-
}
|
|
157
|
+
}, { meta: {
|
|
158
|
+
name: "s3",
|
|
159
|
+
version: "5.0.0-alpha.7",
|
|
160
|
+
description: "Upload WhatsApp media to S3-compatible object storage"
|
|
161
|
+
} });
|
|
137
162
|
}
|
|
138
163
|
//#endregion
|
|
139
164
|
exports.DirectoryStrategy = DirectoryStrategy;
|
|
140
165
|
exports.S3Uploader = S3Uploader;
|
|
166
|
+
exports.S3UploaderService = S3UploaderService;
|
|
141
167
|
exports.s3Plugin = s3Plugin;
|
|
168
|
+
exports.s3UploaderLayer = s3UploaderLayer;
|
|
169
|
+
exports.s3UploaderTestLayer = s3UploaderTestLayer;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Plugin } from "@open-wa/core";
|
|
2
2
|
import { Logger } from "@open-wa/logger";
|
|
3
|
-
|
|
3
|
+
import { Context, Layer } from "effect";
|
|
4
4
|
//#region src/config.d.ts
|
|
5
5
|
type CloudProvider = 'aws' | 'gcp' | 'do' | 'wasabi' | 'backblaze';
|
|
6
|
-
declare enum DirectoryStrategy {
|
|
6
|
+
export declare enum DirectoryStrategy {
|
|
7
7
|
DATE = "DATE",
|
|
8
8
|
CHAT = "CHAT",
|
|
9
9
|
DATE_CHAT = "DATE_CHAT",
|
|
@@ -22,7 +22,7 @@ interface S3Config {
|
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/plugin.d.ts
|
|
25
|
-
declare function s3Plugin(config: S3Config): Plugin;
|
|
25
|
+
export declare function s3Plugin(config: S3Config): Plugin;
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/uploader.d.ts
|
|
28
28
|
interface MediaMessage {
|
|
@@ -35,18 +35,24 @@ interface MediaMessage {
|
|
|
35
35
|
interface WAClient {
|
|
36
36
|
decryptMedia: (message: unknown) => Promise<string>;
|
|
37
37
|
}
|
|
38
|
-
declare class S3Uploader {
|
|
38
|
+
export declare class S3Uploader {
|
|
39
39
|
private readonly config;
|
|
40
40
|
private readonly logger;
|
|
41
41
|
private readonly uploadQueue;
|
|
42
42
|
private readonly processedFiles;
|
|
43
|
-
constructor(config: S3Config, logger: Logger);
|
|
43
|
+
constructor(config: S3Config, logger: Pick<Logger, 'error'>);
|
|
44
44
|
uploadMedia(message: MediaMessage, client: WAClient): Promise<string | null>;
|
|
45
45
|
getCloudUrlForFile(filename: string): string;
|
|
46
46
|
private buildUploadOptions;
|
|
47
47
|
private resolveDirectory;
|
|
48
48
|
waitForQueue(): Promise<void>;
|
|
49
|
+
close(): Promise<void>;
|
|
49
50
|
}
|
|
50
51
|
//#endregion
|
|
51
|
-
|
|
52
|
+
//#region src/service.d.ts
|
|
53
|
+
export declare const S3UploaderService: Context.Service<S3Uploader, S3Uploader>;
|
|
54
|
+
export declare const s3UploaderTestLayer: (uploader: S3Uploader) => Layer.Layer<S3Uploader, never, never>;
|
|
55
|
+
export declare const s3UploaderLayer: (config: S3Config, logger: Pick<Logger, 'error'>) => Layer.Layer<S3Uploader, never, never>;
|
|
56
|
+
//#endregion
|
|
57
|
+
export type { CloudProvider, S3Config };
|
|
52
58
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/config.ts","../src/plugin.ts","../src/uploader.ts"],"mappings":";;;;KAAY
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/config.ts","../src/plugin.ts","../src/uploader.ts","../src/service.ts"],"mappings":";;;;KAAY;oBAEA;EACV;EACA;EACA;EACA;;UAGe;EACf,UAAU;EACV;EACA;EACA;EACA;EACA;EACA,YAAY;EACZ;EACA,UAAU;;;;wBCHI,SAAS,QAAQ,WAAW;;;UCTlC;EACR;EACA;EACA;EACA;EACA;;UAGQ;EACR,eAAe,qBAAqB;;qBAgBzB;mBACM;mBACA;mBACA;mBACA;EAEjB,YAAY,QAAQ,UAAU,QAAQ,KAAK;EAYrC,YAAY,SAAS,cAAc,QAAQ,WAAW;EAiC5D,mBAAmB;UAcX;UAqBA;EAqBF,gBAAgB;EAIhB,SAAS;;;;qBCzIJ,mBAAiB,QAAA,QAAA,YAAA;qBAGjB,sBAAmB,UAAc,eAAU,MAAA,MAAA;qBAG3C,kBAAe,QAClB,UAAQ,QACR,KAAK,qBAAgB,MAAA,MAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
1
2
|
import { Plugin } from "@open-wa/core";
|
|
2
3
|
import { Logger } from "@open-wa/logger";
|
|
3
|
-
|
|
4
4
|
//#region src/config.d.ts
|
|
5
5
|
type CloudProvider = 'aws' | 'gcp' | 'do' | 'wasabi' | 'backblaze';
|
|
6
|
-
declare enum DirectoryStrategy {
|
|
6
|
+
export declare enum DirectoryStrategy {
|
|
7
7
|
DATE = "DATE",
|
|
8
8
|
CHAT = "CHAT",
|
|
9
9
|
DATE_CHAT = "DATE_CHAT",
|
|
@@ -22,7 +22,7 @@ interface S3Config {
|
|
|
22
22
|
}
|
|
23
23
|
//#endregion
|
|
24
24
|
//#region src/plugin.d.ts
|
|
25
|
-
declare function s3Plugin(config: S3Config): Plugin;
|
|
25
|
+
export declare function s3Plugin(config: S3Config): Plugin;
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/uploader.d.ts
|
|
28
28
|
interface MediaMessage {
|
|
@@ -35,18 +35,24 @@ interface MediaMessage {
|
|
|
35
35
|
interface WAClient {
|
|
36
36
|
decryptMedia: (message: unknown) => Promise<string>;
|
|
37
37
|
}
|
|
38
|
-
declare class S3Uploader {
|
|
38
|
+
export declare class S3Uploader {
|
|
39
39
|
private readonly config;
|
|
40
40
|
private readonly logger;
|
|
41
41
|
private readonly uploadQueue;
|
|
42
42
|
private readonly processedFiles;
|
|
43
|
-
constructor(config: S3Config, logger: Logger);
|
|
43
|
+
constructor(config: S3Config, logger: Pick<Logger, 'error'>);
|
|
44
44
|
uploadMedia(message: MediaMessage, client: WAClient): Promise<string | null>;
|
|
45
45
|
getCloudUrlForFile(filename: string): string;
|
|
46
46
|
private buildUploadOptions;
|
|
47
47
|
private resolveDirectory;
|
|
48
48
|
waitForQueue(): Promise<void>;
|
|
49
|
+
close(): Promise<void>;
|
|
49
50
|
}
|
|
50
51
|
//#endregion
|
|
51
|
-
|
|
52
|
+
//#region src/service.d.ts
|
|
53
|
+
export declare const S3UploaderService: Context.Service<S3Uploader, S3Uploader>;
|
|
54
|
+
export declare const s3UploaderTestLayer: (uploader: S3Uploader) => Layer.Layer<S3Uploader, never, never>;
|
|
55
|
+
export declare const s3UploaderLayer: (config: S3Config, logger: Pick<Logger, 'error'>) => Layer.Layer<S3Uploader, never, never>;
|
|
56
|
+
//#endregion
|
|
57
|
+
export type { CloudProvider, S3Config };
|
|
52
58
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/plugin.ts","../src/uploader.ts"],"mappings":";;;;KAAY
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/plugin.ts","../src/uploader.ts","../src/service.ts"],"mappings":";;;;KAAY;oBAEA;EACV;EACA;EACA;EACA;;UAGe;EACf,UAAU;EACV;EACA;EACA;EACA;EACA;EACA,YAAY;EACZ;EACA,UAAU;;;;wBCHI,SAAS,QAAQ,WAAW;;;UCTlC;EACR;EACA;EACA;EACA;EACA;;UAGQ;EACR,eAAe,qBAAqB;;qBAgBzB;mBACM;mBACA;mBACA;mBACA;EAEjB,YAAY,QAAQ,UAAU,QAAQ,KAAK;EAYrC,YAAY,SAAS,cAAc,QAAQ,WAAW;EAiC5D,mBAAmB;UAcX;UAqBA;EAqBF,gBAAgB;EAIhB,SAAS;;;;qBCzIJ,mBAAiB,QAAA,QAAA,YAAA;qBAGjB,sBAAmB,UAAc,eAAU,MAAA,MAAA;qBAG3C,kBAAe,QAClB,UAAQ,QACR,KAAK,qBAAgB,MAAA,MAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Context, Effect, Layer, ManagedRuntime } from "effect";
|
|
2
2
|
import { getCloudUrl, upload } from "pico-s3";
|
|
3
3
|
import mime from "mime";
|
|
4
|
+
import { ScopedTaskQueue } from "@open-wa/runtime-core";
|
|
4
5
|
//#region src/config.ts
|
|
5
6
|
let DirectoryStrategy = /* @__PURE__ */ function(DirectoryStrategy) {
|
|
6
7
|
DirectoryStrategy["DATE"] = "DATE";
|
|
@@ -16,10 +17,15 @@ var S3Uploader = class {
|
|
|
16
17
|
this.processedFiles = /* @__PURE__ */ new Set();
|
|
17
18
|
this.config = config;
|
|
18
19
|
this.logger = logger;
|
|
19
|
-
this.uploadQueue =
|
|
20
|
+
this.uploadQueue = ScopedTaskQueue.make({
|
|
21
|
+
name: "s3.upload",
|
|
20
22
|
concurrency: 2,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
capacity: 64,
|
|
24
|
+
overload: "backpressure",
|
|
25
|
+
rate: {
|
|
26
|
+
intervalMs: 1e3,
|
|
27
|
+
limit: 2
|
|
28
|
+
}
|
|
23
29
|
});
|
|
24
30
|
}
|
|
25
31
|
async uploadMedia(message, client) {
|
|
@@ -31,8 +37,8 @@ var S3Uploader = class {
|
|
|
31
37
|
try {
|
|
32
38
|
const mediaData = await client.decryptMedia(message);
|
|
33
39
|
const opts = this.buildUploadOptions(filename, mediaData, message.from);
|
|
40
|
+
await (await this.uploadQueue).submit(() => upload(opts), filename);
|
|
34
41
|
this.processedFiles.add(filename);
|
|
35
|
-
await this.uploadQueue.add(() => upload(opts).catch(() => void 0));
|
|
36
42
|
return this.getCloudUrlForFile(filename);
|
|
37
43
|
} catch (error) {
|
|
38
44
|
this.logger.error(`Upload error: ${error.message}`);
|
|
@@ -40,7 +46,7 @@ var S3Uploader = class {
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
getCloudUrlForFile(filename) {
|
|
43
|
-
|
|
49
|
+
const opts = {
|
|
44
50
|
file: "",
|
|
45
51
|
filename,
|
|
46
52
|
provider: this.config.provider,
|
|
@@ -49,7 +55,8 @@ var S3Uploader = class {
|
|
|
49
55
|
bucket: this.config.bucket,
|
|
50
56
|
region: this.config.region,
|
|
51
57
|
public: this.config.public
|
|
52
|
-
}
|
|
58
|
+
};
|
|
59
|
+
return getCloudUrl(opts);
|
|
53
60
|
}
|
|
54
61
|
buildUploadOptions(filename, mediaData, from) {
|
|
55
62
|
const opts = {
|
|
@@ -73,44 +80,63 @@ var S3Uploader = class {
|
|
|
73
80
|
const cleanFrom = from.replace("@c.us", "").replace("@g.us", "");
|
|
74
81
|
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
75
82
|
switch (dirStrat) {
|
|
76
|
-
case
|
|
77
|
-
case
|
|
78
|
-
case
|
|
79
|
-
case
|
|
83
|
+
case "DATE": return dateStr;
|
|
84
|
+
case "CHAT": return cleanFrom;
|
|
85
|
+
case "DATE_CHAT": return `${dateStr}/${cleanFrom}`;
|
|
86
|
+
case "CHAT_DATE": return `${cleanFrom}/${dateStr}`;
|
|
80
87
|
default: return typeof dirStrat === "string" ? dirStrat : void 0;
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
async waitForQueue() {
|
|
84
|
-
await this.uploadQueue.
|
|
91
|
+
await (await this.uploadQueue).waitForIdle();
|
|
92
|
+
}
|
|
93
|
+
async close() {
|
|
94
|
+
await (await this.uploadQueue).close();
|
|
85
95
|
}
|
|
86
96
|
};
|
|
87
97
|
//#endregion
|
|
98
|
+
//#region src/service.ts
|
|
99
|
+
const S3UploaderService = Context.Service("@open-wa/integration-s3/S3Uploader");
|
|
100
|
+
const s3UploaderTestLayer = (uploader) => Layer.succeed(S3UploaderService, uploader);
|
|
101
|
+
const s3UploaderLayer = (config, logger) => Layer.effect(S3UploaderService, Effect.acquireRelease(Effect.sync(() => new S3Uploader(config, logger)), (uploader) => Effect.promise(async () => {
|
|
102
|
+
await uploader.waitForQueue();
|
|
103
|
+
await uploader.close();
|
|
104
|
+
})));
|
|
105
|
+
//#endregion
|
|
88
106
|
//#region src/plugin.ts
|
|
89
107
|
function s3Plugin(config) {
|
|
90
|
-
return async (input) => {
|
|
108
|
+
return Object.assign(async (input) => {
|
|
91
109
|
const { logger, client } = input;
|
|
92
|
-
const
|
|
110
|
+
const runtime = ManagedRuntime.make(s3UploaderLayer(config, logger));
|
|
111
|
+
const uploader = await runtime.runPromise(S3UploaderService);
|
|
93
112
|
const getWAClient = () => {
|
|
94
113
|
if (client && typeof client === "object" && "decryptMedia" in client) return client;
|
|
95
114
|
return null;
|
|
96
115
|
};
|
|
97
116
|
return {
|
|
98
|
-
"message.received":
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
"message.received": ({ message }) => runtime.runPromise(Effect.tryPromise({
|
|
118
|
+
try: async () => {
|
|
119
|
+
const msg = message;
|
|
120
|
+
const waClient = getWAClient();
|
|
121
|
+
if (msg.deprecatedMms3Url && msg.mimetype && waClient) {
|
|
122
|
+
const cloudUrl = await uploader.uploadMedia(msg, waClient);
|
|
123
|
+
if (cloudUrl) msg.cloudUrl = cloudUrl;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
catch: (cause) => cause
|
|
127
|
+
})),
|
|
106
128
|
dispose: async () => {
|
|
107
|
-
await
|
|
129
|
+
await runtime.dispose();
|
|
108
130
|
logger.info("S3 uploader queue drained");
|
|
109
131
|
}
|
|
110
132
|
};
|
|
111
|
-
}
|
|
133
|
+
}, { meta: {
|
|
134
|
+
name: "s3",
|
|
135
|
+
version: "5.0.0-alpha.7",
|
|
136
|
+
description: "Upload WhatsApp media to S3-compatible object storage"
|
|
137
|
+
} });
|
|
112
138
|
}
|
|
113
139
|
//#endregion
|
|
114
|
-
export { DirectoryStrategy, S3Uploader, s3Plugin };
|
|
140
|
+
export { DirectoryStrategy, S3Uploader, S3UploaderService, s3Plugin, s3UploaderLayer, s3UploaderTestLayer };
|
|
115
141
|
|
|
116
142
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/config.ts","../src/uploader.ts","../src/service.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 { upload, getCloudUrl } from 'pico-s3';\nimport mime from 'mime';\nimport type { Logger } from '@open-wa/logger';\nimport { ScopedTaskQueue } from '@open-wa/runtime-core';\nimport { type S3Config, DirectoryStrategy } from './config';\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: Pick<Logger, 'error'>;\n private readonly uploadQueue: Promise<ScopedTaskQueue>;\n private readonly processedFiles: Set<string> = new Set();\n\n constructor(config: S3Config, logger: Pick<Logger, 'error'>) {\n this.config = config;\n this.logger = logger;\n this.uploadQueue = ScopedTaskQueue.make({\n name: 's3.upload',\n concurrency: 2,\n capacity: 64,\n overload: 'backpressure',\n rate: { intervalMs: 1000, limit: 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 await (await this.uploadQueue).submit(\n () => upload(opts as never),\n filename,\n );\n this.processedFiles.add(filename);\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 (await this.uploadQueue).waitForIdle();\n }\n\n async close(): Promise<void> {\n await (await this.uploadQueue).close();\n }\n}\n","import type { Logger } from '@open-wa/logger';\nimport { Context, Effect, Layer } from 'effect';\nimport type { S3Config } from './config';\nimport { S3Uploader } from './uploader';\n\nexport const S3UploaderService =\n Context.Service<S3Uploader>('@open-wa/integration-s3/S3Uploader');\n\nexport const s3UploaderTestLayer = (uploader: S3Uploader) =>\n Layer.succeed(S3UploaderService, uploader);\n\nexport const s3UploaderLayer = (\n config: S3Config,\n logger: Pick<Logger, 'error'>,\n) => Layer.effect(\n S3UploaderService,\n Effect.acquireRelease(\n Effect.sync(() => new S3Uploader(config, logger)),\n (uploader) => Effect.promise(async () => {\n await uploader.waitForQueue();\n await uploader.close();\n }),\n ),\n);\n","import type { Plugin, PluginInput, Hooks } from '@open-wa/core';\nimport type { S3Config } from './config';\nimport { Effect, ManagedRuntime } from 'effect';\nimport { S3UploaderService, s3UploaderLayer } from './service';\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 Object.assign(async (input: PluginInput): Promise<Hooks> => {\n const { logger, client } = input;\n const runtime = ManagedRuntime.make(s3UploaderLayer(config, logger));\n const uploader = await runtime.runPromise(S3UploaderService);\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': ({ message }) => runtime.runPromise(Effect.tryPromise({\n try: async () => {\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 catch: (cause) => cause,\n })),\n\n dispose: async () => {\n await runtime.dispose();\n logger.info('S3 uploader queue drained');\n },\n };\n }, {\n meta: {\n name: 's3',\n version: '5.0.0-alpha.7',\n description: 'Upload WhatsApp media to S3-compatible object storage',\n },\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-wa/integration-s3",
|
|
3
|
-
"version": "5.0.0
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "S3/Cloud storage integration plugin for open-wa",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
16
|
+
"@open-wa/core": "5.0.0",
|
|
17
|
+
"@open-wa/logger": "5.0.0",
|
|
18
|
+
"@open-wa/runtime-core": "5.0.0",
|
|
19
|
+
"effect": "4.0.0-beta.100",
|
|
20
|
+
"mime": "^4.1.0",
|
|
21
|
+
"pico-s3": "^2.11.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
24
|
-
"typescript": "^
|
|
24
|
+
"@types/node": "^26.6.3",
|
|
25
|
+
"typescript": "^7.0.2",
|
|
26
|
+
"vitest": "5.0.2"
|
|
25
27
|
},
|
|
26
28
|
"publishConfig": {
|
|
27
29
|
"access": "public"
|
|
@@ -31,9 +33,15 @@
|
|
|
31
33
|
"type": "git",
|
|
32
34
|
"url": "https://github.com/open-wa/wa-automate-nodejs.git"
|
|
33
35
|
},
|
|
36
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
34
40
|
"scripts": {
|
|
35
41
|
"build": "tsdown src/index.ts --format esm --format cjs",
|
|
36
42
|
"dev": "tsdown src/index.ts --format esm --format cjs --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
37
45
|
"lint": "oxlint src",
|
|
38
46
|
"clean": "rimraf dist"
|
|
39
47
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @open-wa/integration-s3@5.0.0-alpha.7 build /Users/Mohammed/projects/tools/wa/integrations/s3
|
|
3
|
-
> tsdown src/index.ts --format esm --format cjs
|
|
4
|
-
|
|
5
|
-
ℹ tsdown v0.21.7 powered by rolldown v1.0.0-rc.12
|
|
6
|
-
ℹ config file: /Users/Mohammed/projects/tools/wa/integrations/s3/tsdown.config.ts
|
|
7
|
-
ℹ entry: src/index.ts
|
|
8
|
-
ℹ tsconfig: tsconfig.json
|
|
9
|
-
ℹ Build start
|
|
10
|
-
ℹ Cleaning 7 files
|
|
11
|
-
ℹ [CJS] dist/index.cjs 4.90 kB │ gzip: 1.74 kB
|
|
12
|
-
ℹ [CJS] 1 files, total: 4.90 kB
|
|
13
|
-
ℹ [ESM] dist/index.mjs 3.73 kB │ gzip: 1.31 kB
|
|
14
|
-
ℹ [ESM] dist/index.mjs.map 8.02 kB │ gzip: 2.53 kB
|
|
15
|
-
ℹ [ESM] dist/index.d.mts.map 0.78 kB │ gzip: 0.35 kB
|
|
16
|
-
ℹ [ESM] dist/index.d.mts 1.46 kB │ gzip: 0.66 kB
|
|
17
|
-
ℹ [ESM] 4 files, total: 13.99 kB
|
|
18
|
-
✔ Build complete in 1285ms
|
|
19
|
-
ℹ [CJS] dist/index.d.cts.map 0.78 kB │ gzip: 0.35 kB
|
|
20
|
-
ℹ [CJS] dist/index.d.cts 1.46 kB │ gzip: 0.66 kB
|
|
21
|
-
ℹ [CJS] 2 files, total: 2.24 kB
|
|
22
|
-
✔ Build complete in 1292ms
|
package/CHANGELOG.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# @open-wa/integration-s3
|
|
2
|
-
|
|
3
|
-
## 5.0.0-alpha.7
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies []:
|
|
8
|
-
- @open-wa/core@5.0.0-alpha.7
|
|
9
|
-
- @open-wa/logger@5.0.0-alpha.7
|
|
10
|
-
|
|
11
|
-
## 5.0.0-alpha.6
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- Updated dependencies []:
|
|
16
|
-
- @open-wa/core@5.0.0-alpha.6
|
|
17
|
-
- @open-wa/logger@5.0.0-alpha.6
|
|
18
|
-
|
|
19
|
-
## 5.0.0-alpha.5
|
|
20
|
-
|
|
21
|
-
### Patch Changes
|
|
22
|
-
|
|
23
|
-
- Updated dependencies []:
|
|
24
|
-
- @open-wa/core@5.0.0-alpha.5
|
|
25
|
-
- @open-wa/logger@5.0.0-alpha.5
|
|
26
|
-
|
|
27
|
-
## 5.0.0-alpha.4
|
|
28
|
-
|
|
29
|
-
### Patch Changes
|
|
30
|
-
|
|
31
|
-
- Updated dependencies []:
|
|
32
|
-
- @open-wa/core@5.0.0-alpha.4
|
|
33
|
-
- @open-wa/logger@5.0.0-alpha.4
|
|
34
|
-
|
|
35
|
-
## 5.0.0-alpha.3
|
|
36
|
-
|
|
37
|
-
### Patch Changes
|
|
38
|
-
|
|
39
|
-
- Updated dependencies []:
|
|
40
|
-
- @open-wa/core@5.0.0-alpha.3
|
|
41
|
-
- @open-wa/logger@5.0.0-alpha.3
|
|
42
|
-
|
|
43
|
-
## 5.0.0-alpha.2
|
|
44
|
-
|
|
45
|
-
### Patch Changes
|
|
46
|
-
|
|
47
|
-
- Updated dependencies []:
|
|
48
|
-
- @open-wa/core@5.0.0-alpha.2
|
|
49
|
-
- @open-wa/logger@5.0.0-alpha.2
|
|
50
|
-
|
|
51
|
-
## 5.0.0-alpha.3
|
|
52
|
-
|
|
53
|
-
### Minor Changes
|
|
54
|
-
|
|
55
|
-
- Dry run minor bump
|
|
56
|
-
|
|
57
|
-
### Patch Changes
|
|
58
|
-
|
|
59
|
-
- Updated dependencies []:
|
|
60
|
-
- @open-wa/core@6.0.0-alpha.2
|
|
61
|
-
- @open-wa/logger@6.0.0-alpha.2
|
|
62
|
-
|
|
63
|
-
## 5.0.0-alpha.2
|
|
64
|
-
|
|
65
|
-
### Patch Changes
|
|
66
|
-
|
|
67
|
-
- Updated dependencies []:
|
|
68
|
-
- @open-wa/core@5.0.1-alpha.2
|
|
69
|
-
- @open-wa/logger@5.0.1-alpha.2
|
package/src/config.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
DELETED
package/src/plugin.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
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
DELETED