swan-api 1.0.0 → 1.1.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/Message.js +2 -2
- package/README.md +47 -2
- package/Socket.js +54 -27
- package/Sticker.js +125 -0
- package/index.js +1 -0
- package/package.json +14 -6
package/Message.js
CHANGED
|
@@ -31,7 +31,7 @@ export default class Message {
|
|
|
31
31
|
const ctx = m.extendedTextMessage?.contextInfo;
|
|
32
32
|
if (ctx?.quotedMessage){
|
|
33
33
|
this.quoted = new Message(socket, {
|
|
34
|
-
key: {remoteJid: this.fromId, fromMe: ctx.participant === socket.
|
|
34
|
+
key: {remoteJid: this.fromId, fromMe: ctx.participant === socket.userId},
|
|
35
35
|
message: ctx.quotedMessage
|
|
36
36
|
});
|
|
37
37
|
}
|
|
@@ -52,7 +52,7 @@ export default class Message {
|
|
|
52
52
|
|
|
53
53
|
// returns the sender's name and phone number
|
|
54
54
|
async getContact() {
|
|
55
|
-
const jid = this.fromMe ? this.socket.
|
|
55
|
+
const jid = this.fromMe ? this.socket.userId : this.author;
|
|
56
56
|
const number = jid.split('@')[0];
|
|
57
57
|
return {name: this.fromName, number}
|
|
58
58
|
}
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ socket.on("newMessage", async (msg) => {
|
|
|
53
53
|
if (msg.fromMe)
|
|
54
54
|
return;
|
|
55
55
|
|
|
56
|
-
console.log(`Message from ${msg.
|
|
56
|
+
console.log(`New Message from ${msg.fromName}`);
|
|
57
57
|
|
|
58
58
|
await msg.reply("Processing your message...");
|
|
59
59
|
|
|
@@ -168,13 +168,58 @@ const media = await MessageMedia.fromUrl("https://example.com/image.png");
|
|
|
168
168
|
|
|
169
169
|
---
|
|
170
170
|
|
|
171
|
+
### Sticker
|
|
172
|
+
Converts an image or video buffer into a WhatsApp-ready sticker (WebP, 512x512, with embedded pack/author metadata).
|
|
173
|
+
|
|
174
|
+
You normally don't need to call `Sticker` directly — just use `msg.reply(media, {asSticker: true})`, which handles quality adjustment automatically to stay under WhatsApp's size limit. `Sticker` is exported in case you want to generate the WebP buffer yourself.
|
|
175
|
+
|
|
176
|
+
#### Options
|
|
177
|
+
| Option | Type | Default | Description |
|
|
178
|
+
| ------ | ---- | ------- | ----------- |
|
|
179
|
+
| `stickerAuthor` | `string` | `""` | Sticker pack publisher name. |
|
|
180
|
+
| `stickerPack` | `string` | `""` | Sticker pack name. |
|
|
181
|
+
| `stickerType` | `string` | `"default"` | Resize/crop mode. See table below. |
|
|
182
|
+
| `stickerQuality` | `number` | `80` | WebP encoding quality (0-100). |
|
|
183
|
+
|
|
184
|
+
#### Sticker types
|
|
185
|
+
| Type | Behavior |
|
|
186
|
+
| ---- | -------- |
|
|
187
|
+
| `default` | Stretches the image to fill 512x512, ignoring original proportions. |
|
|
188
|
+
| `full` | Keeps original proportions, padding the rest with transparency. |
|
|
189
|
+
| `crop` | Keeps original proportions, cropping the excess to fill 512x512. |
|
|
190
|
+
| `circle` | Same as `crop`, masked into a circle. |
|
|
191
|
+
| `rounded` | Same as `crop`, masked with rounded corners. |
|
|
192
|
+
|
|
193
|
+
Videos are automatically converted to animated stickers.
|
|
194
|
+
|
|
195
|
+
#### Usage
|
|
196
|
+
```javascript
|
|
197
|
+
import { Socket, Sticker } from "swan-api";
|
|
198
|
+
const socket = new Socket();
|
|
199
|
+
|
|
200
|
+
socket.on("newMessage", async (msg) => {
|
|
201
|
+
const media = await msg.downloadMedia();
|
|
202
|
+
if (!media)
|
|
203
|
+
return;
|
|
204
|
+
|
|
205
|
+
// send directly as a sticker
|
|
206
|
+
await msg.reply(media, {
|
|
207
|
+
asSticker: true,
|
|
208
|
+
stickerPack: "Bot Pack",
|
|
209
|
+
stickerAuthor: "Bot"
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
---
|
|
214
|
+
|
|
171
215
|
## Exports
|
|
172
216
|
|
|
173
217
|
```javascript
|
|
174
218
|
import {
|
|
175
219
|
Socket,
|
|
176
220
|
Message,
|
|
177
|
-
MessageMedia
|
|
221
|
+
MessageMedia,
|
|
222
|
+
Sticker
|
|
178
223
|
} from "swan-api";
|
|
179
224
|
```
|
|
180
225
|
|
package/Socket.js
CHANGED
|
@@ -3,20 +3,45 @@ import makeWASocket, { useMultiFileAuthState, fetchLatestBaileysVersion, Disconn
|
|
|
3
3
|
import P from 'pino'
|
|
4
4
|
import qrcode from 'qrcode-terminal'
|
|
5
5
|
import { EventEmitter } from "node:events";
|
|
6
|
-
import { Sticker } from 'wa-sticker-formatter'
|
|
7
6
|
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import Sticker from './Sticker.js'
|
|
8
11
|
import Message from './Message.js';
|
|
9
12
|
import MessageMedia from './MessageMedia.js';
|
|
10
13
|
|
|
11
14
|
//Wrapper Class For Baileys Socket
|
|
12
15
|
export default class Socket extends EventEmitter {
|
|
13
|
-
|
|
14
|
-
constructor() {
|
|
16
|
+
constructor(){
|
|
15
17
|
super();
|
|
16
|
-
this.
|
|
18
|
+
this.muteSessionErrors()
|
|
19
|
+
this.connect()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// hide internal session errors that does not affect the use
|
|
23
|
+
muteSessionErrors(){
|
|
24
|
+
const oldLog = console.log;
|
|
25
|
+
console.log = (...args) => {
|
|
26
|
+
if (String(args[0]).includes("Closing session"))
|
|
27
|
+
return;
|
|
28
|
+
|
|
29
|
+
oldLog(...args);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const oldError = console.error;
|
|
33
|
+
console.error = (...args) => {
|
|
34
|
+
if (
|
|
35
|
+
String(args[0]).includes("Failed to decrypt") ||
|
|
36
|
+
String(args[0]).includes("Session error")
|
|
37
|
+
)
|
|
38
|
+
return;
|
|
39
|
+
|
|
40
|
+
oldError(...args);
|
|
41
|
+
};
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
//
|
|
44
|
+
// creates a Socket instance and tries to authenticate
|
|
20
45
|
async connect() {
|
|
21
46
|
await this.authenticate();
|
|
22
47
|
this.bindEvents();
|
|
@@ -26,16 +51,21 @@ export default class Socket extends EventEmitter {
|
|
|
26
51
|
async authenticate(){
|
|
27
52
|
const { state, saveCreds } = await useMultiFileAuthState('auth')
|
|
28
53
|
const { version } = await fetchLatestBaileysVersion()
|
|
54
|
+
const logger = P({level: 'silent'})
|
|
29
55
|
|
|
30
|
-
this.socket = makeWASocket({version, auth: state, logger
|
|
31
|
-
this.user = this.socket.user;
|
|
56
|
+
this.socket = makeWASocket({version, auth: state, logger});
|
|
32
57
|
this.socket.ev.on('creds.update', saveCreds)
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
//bind events from baileys to default treatment functions
|
|
36
61
|
bindEvents(){
|
|
62
|
+
let socketReady = false
|
|
63
|
+
|
|
37
64
|
//each new message emits a 'newMessage' event and a 'Message' object
|
|
38
65
|
this.socket.ev.on('messages.upsert', ({messages}) => {
|
|
66
|
+
if (!socketReady)
|
|
67
|
+
return;
|
|
68
|
+
|
|
39
69
|
for (const msg of messages){
|
|
40
70
|
if (!msg.message)
|
|
41
71
|
continue;
|
|
@@ -45,7 +75,7 @@ export default class Socket extends EventEmitter {
|
|
|
45
75
|
|
|
46
76
|
//emits 'ready' when opens connection
|
|
47
77
|
//on connnection lost, tries to reconnect
|
|
48
|
-
this.socket.ev.on('connection.update', (update) => {
|
|
78
|
+
this.socket.ev.on('connection.update', async (update) => {
|
|
49
79
|
const { connection, lastDisconnect, qr } = update;
|
|
50
80
|
if (qr)
|
|
51
81
|
qrcode.generate(qr, {small: true})
|
|
@@ -54,9 +84,12 @@ export default class Socket extends EventEmitter {
|
|
|
54
84
|
const shouldReconnect = lastDisconnect?.error?.output?.statusCode
|
|
55
85
|
!== DisconnectReason.loggedOut;
|
|
56
86
|
if (shouldReconnect)
|
|
57
|
-
this.connect()
|
|
58
|
-
} else if (connection == 'open')
|
|
87
|
+
await this.connect()
|
|
88
|
+
} else if (connection == 'open'){
|
|
89
|
+
socketReady = true
|
|
90
|
+
this.userId = this.socket.user.id;
|
|
59
91
|
this.emit('ready');
|
|
92
|
+
}
|
|
60
93
|
});
|
|
61
94
|
}
|
|
62
95
|
|
|
@@ -77,32 +110,26 @@ export default class Socket extends EventEmitter {
|
|
|
77
110
|
|
|
78
111
|
// parse media for sticker creation
|
|
79
112
|
if (opts.asSticker){
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
opts.stickerProportion = 'full'
|
|
113
|
+
// WhatsApp limits: 100KB for static stickers, 500KB for animated ones
|
|
114
|
+
const isAnimated = mimetype.startsWith("video/");
|
|
115
|
+
const maxBytes = (isAnimated ? 500 : 100) * 1024;
|
|
84
116
|
|
|
85
117
|
// tries to create the sticker at the highest quality,
|
|
86
118
|
// if the resulting file exceeds WhatsApp's 1MB limit
|
|
87
119
|
// the quality is reduced by 10% and retried
|
|
88
120
|
// throws an exception if quality goes below 0%
|
|
89
|
-
let final_quality =
|
|
90
|
-
while (final_quality
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const webp = await sticker.toBuffer();
|
|
99
|
-
const size = (webp.length/1024)/1024;
|
|
100
|
-
if (size >= 1)
|
|
101
|
-
final_quality -= 0.1;
|
|
121
|
+
let final_quality = 100;
|
|
122
|
+
while (final_quality > 0) {
|
|
123
|
+
const webp = await Sticker(buffer, {...opts,
|
|
124
|
+
stickerQuality : final_quality})
|
|
125
|
+
|
|
126
|
+
if (webp.length > maxBytes)
|
|
127
|
+
final_quality -= 10;
|
|
102
128
|
else
|
|
103
129
|
return await this.socket.sendMessage(jid, {sticker: webp}, opts)
|
|
104
130
|
}
|
|
105
131
|
|
|
132
|
+
throw new Error(`SWAN send(): could not compress sticker below ${maxBytes / 1024}KB`);
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
//send regular files
|
package/Sticker.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
import ffmpegPath from "ffmpeg-static";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
5
|
+
|
|
6
|
+
export default async function Sticker(buffer, opts = {}) {
|
|
7
|
+
const author = opts.stickerAuthor ? opts.stickerAuthor : "";
|
|
8
|
+
const pack = opts.stickerPack ? opts.stickerPack : "";
|
|
9
|
+
const type = opts.stickerType ? opts.stickerType : "fill";
|
|
10
|
+
const quality = opts.stickerQuality ? opts.stickerQuality : 80;
|
|
11
|
+
|
|
12
|
+
const info = await fileTypeFromBuffer(buffer);
|
|
13
|
+
if (!info)
|
|
14
|
+
throw Error("Unknown file type");
|
|
15
|
+
|
|
16
|
+
if (info.mime.startsWith("video/"))
|
|
17
|
+
buffer = await convertVideo(buffer);
|
|
18
|
+
else if (!info.mime.startsWith("image/"))
|
|
19
|
+
throw Error(`Unsupported media type: ${info.mime}`);
|
|
20
|
+
|
|
21
|
+
const fit = type === "full" ? "contain" :
|
|
22
|
+
type === "fill" ? "fill" : "cover"; // crop, circle, rounded
|
|
23
|
+
|
|
24
|
+
let image = sharp(buffer).resize(512, 512, {fit,
|
|
25
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 }}).ensureAlpha();
|
|
26
|
+
|
|
27
|
+
if (type === "circle")
|
|
28
|
+
image = image.composite([{
|
|
29
|
+
input: Buffer.from(`<svg width="512" height="512">
|
|
30
|
+
<circle cx="256" cy="256" r="256" fill="white"/></svg>`),
|
|
31
|
+
blend: "dest-in"
|
|
32
|
+
}]);
|
|
33
|
+
|
|
34
|
+
if (type === "rounded")
|
|
35
|
+
image = image.composite([{
|
|
36
|
+
blend: "dest-in",
|
|
37
|
+
input: Buffer.from(`<svg width="512" height="512">
|
|
38
|
+
<rect x="0" y="0" width="512" height="512"
|
|
39
|
+
rx="64" ry="64" fill="white"/></svg>`),
|
|
40
|
+
}]);
|
|
41
|
+
|
|
42
|
+
const webp = await image.webp({ quality }).toBuffer();
|
|
43
|
+
return addExif(webp, author, pack)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// for animated stickers
|
|
47
|
+
async function convertVideo(buffer) {
|
|
48
|
+
const { stdout } = await execa(ffmpegPath, [
|
|
49
|
+
"-i", "pipe:0", "-vcodec", "libwebp", "-vf",
|
|
50
|
+
"scale=512:512:force_original_aspect_ratio=decrease",
|
|
51
|
+
"-loop", "0", "-an", "-f", "webp", "pipe:1"
|
|
52
|
+
], { input: buffer, encoding: null });
|
|
53
|
+
|
|
54
|
+
return stdout;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add metadata
|
|
58
|
+
function addExif(webp, author, pack) {
|
|
59
|
+
const json = Buffer.from(JSON.stringify({
|
|
60
|
+
"sticker-pack-id": "com.swan.sticker",
|
|
61
|
+
"sticker-pack-name": pack,
|
|
62
|
+
"sticker-pack-publisher": author,
|
|
63
|
+
"emojis": []
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const tiff = Buffer.alloc(22 + json.length);
|
|
67
|
+
tiff.write("II", 0);
|
|
68
|
+
tiff.writeUInt16LE(42, 2);
|
|
69
|
+
tiff.writeUInt32LE(8, 4);
|
|
70
|
+
tiff.writeUInt16LE(1, 8);
|
|
71
|
+
tiff.writeUInt16LE(0x5741, 10);
|
|
72
|
+
tiff.writeUInt16LE(7, 12);
|
|
73
|
+
tiff.writeUInt32LE(json.length, 14);
|
|
74
|
+
tiff.writeUInt32LE(22, 18);
|
|
75
|
+
json.copy(tiff, 22);
|
|
76
|
+
|
|
77
|
+
let exif = Buffer.concat([
|
|
78
|
+
Buffer.from("EXIF"),
|
|
79
|
+
Buffer.alloc(4),
|
|
80
|
+
tiff
|
|
81
|
+
]);
|
|
82
|
+
exif.writeUInt32LE(tiff.length, 4);
|
|
83
|
+
if (exif.length % 2)
|
|
84
|
+
exif = Buffer.concat([exif, Buffer.from([0])]);
|
|
85
|
+
|
|
86
|
+
// EXIF flag in VP8X
|
|
87
|
+
let body = Buffer.from(webp.subarray(12));
|
|
88
|
+
body = ensureVP8X(body);
|
|
89
|
+
if (body.subarray(0, 4).toString("ascii") === "VP8X")
|
|
90
|
+
body[8] |= 0x08;
|
|
91
|
+
|
|
92
|
+
const riffSize = Buffer.alloc(4);
|
|
93
|
+
riffSize.writeUInt32LE(4 + body.length + exif.length); // +4 = "WEBP"
|
|
94
|
+
|
|
95
|
+
return Buffer.concat([
|
|
96
|
+
webp.subarray(0, 4), // "RIFF"
|
|
97
|
+
riffSize,
|
|
98
|
+
webp.subarray(8, 12), // "WEBP"
|
|
99
|
+
body, // image data
|
|
100
|
+
exif // EXIF
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Adds VP8X container if sharp didn't generated it
|
|
105
|
+
function ensureVP8X(body) {
|
|
106
|
+
const fourcc = body.subarray(0, 4).toString("ascii");
|
|
107
|
+
if (fourcc === "VP8X") return body;
|
|
108
|
+
if (fourcc !== "VP8 " && fourcc !== "VP8L") return body;
|
|
109
|
+
|
|
110
|
+
const width = 512, height = 512; // always 512x512
|
|
111
|
+
|
|
112
|
+
const vp8x = Buffer.alloc(18);
|
|
113
|
+
vp8x.write("VP8X", 0);
|
|
114
|
+
vp8x.writeUInt32LE(10, 4); // payload size
|
|
115
|
+
|
|
116
|
+
let flags = 0;
|
|
117
|
+
if (fourcc === "VP8L") flags |= 0x10; // VP8L supports alpha
|
|
118
|
+
vp8x[8] = flags;
|
|
119
|
+
// bytes 9-11 (reserved) 0 by alloc
|
|
120
|
+
|
|
121
|
+
vp8x.writeUIntLE(width - 1, 12, 3);
|
|
122
|
+
vp8x.writeUIntLE(height - 1, 15, 3);
|
|
123
|
+
|
|
124
|
+
return Buffer.concat([vp8x, body]);
|
|
125
|
+
}
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swan-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Simple WhatsApp API for Node",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whatsapp",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"Socket.js",
|
|
31
31
|
"Message.js",
|
|
32
32
|
"MessageMedia.js",
|
|
33
|
+
"Sticker.js",
|
|
33
34
|
"README.md",
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
|
@@ -37,11 +38,18 @@
|
|
|
37
38
|
"node": ">=18"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@whiskeysockets/baileys": "^
|
|
41
|
-
"axios": "^1.11.0",
|
|
42
|
-
"pino": "^9.7.0",
|
|
43
|
-
"mime-types": "^3.0.1",
|
|
41
|
+
"@whiskeysockets/baileys": "^7.0.0-rc13",
|
|
44
42
|
"qrcode-terminal": "^0.12.0",
|
|
45
|
-
"
|
|
43
|
+
"pino": "^10.3.1",
|
|
44
|
+
"axios": "^1.18.1",
|
|
45
|
+
"mime-types": "^3.0.2",
|
|
46
|
+
"ffmpeg-static": "^5.2.0",
|
|
47
|
+
"execa": "^9.6.0"
|
|
48
|
+
},
|
|
49
|
+
"allowScripts": {
|
|
50
|
+
"@whiskeysockets/baileys@7.0.0-rc13": true,
|
|
51
|
+
"protobufjs@7.6.5": true,
|
|
52
|
+
"sharp@0.30.7": true,
|
|
53
|
+
"ffmpeg-static@5.3.0": true
|
|
46
54
|
}
|
|
47
55
|
}
|