swan-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Message.js +82 -0
- package/MessageMedia.js +25 -0
- package/README.md +183 -0
- package/Socket.js +113 -0
- package/index.js +3 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cauê Samonek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Message.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
|
|
4
|
+
import MessageMedia from './MessageMedia.js'
|
|
5
|
+
import { downloadContentFromMessage } from '@whiskeysockets/baileys'
|
|
6
|
+
|
|
7
|
+
// Wrapper class for baileys API message object
|
|
8
|
+
export default class Message {
|
|
9
|
+
// each Message stores its socket, chat/user information and context
|
|
10
|
+
constructor(socket, baileysMessage){
|
|
11
|
+
// saves socket and original message object
|
|
12
|
+
this.socket = socket;
|
|
13
|
+
this.baileysMsg = baileysMessage;
|
|
14
|
+
|
|
15
|
+
// simplify subsequent access
|
|
16
|
+
const m = this.baileysMsg.message;
|
|
17
|
+
const k = this.baileysMsg.key;
|
|
18
|
+
|
|
19
|
+
// simplify and standardize attribute names
|
|
20
|
+
this.fromId = k.remoteJid;
|
|
21
|
+
this.fromMe = k.fromMe;
|
|
22
|
+
this.author = k.participant || k.remoteJid;
|
|
23
|
+
this.fromName = this.baileysMsg.pushName;
|
|
24
|
+
this.type = Object.keys(m)[0].replace("Message","").toLowerCase() || 'conversation'
|
|
25
|
+
this.text = m.conversation || m.extendedTextMessage?.text || m.imageMessage?.caption || m.videoMessage?.caption || '';
|
|
26
|
+
|
|
27
|
+
this.media = m.imageMessage || m.videoMessage || m.documentMessage || m.stickerMessage;
|
|
28
|
+
|
|
29
|
+
// build the quoted message, if any
|
|
30
|
+
this.quoted = null;
|
|
31
|
+
const ctx = m.extendedTextMessage?.contextInfo;
|
|
32
|
+
if (ctx?.quotedMessage){
|
|
33
|
+
this.quoted = new Message(socket, {
|
|
34
|
+
key: {remoteJid: this.fromId, fromMe: ctx.participant === socket.user.id},
|
|
35
|
+
message: ctx.quotedMessage
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Returns the attached media as a MessageMedia object, or null if none exists
|
|
41
|
+
async downloadMedia() {
|
|
42
|
+
if (!this.media) return null;
|
|
43
|
+
const stream = await downloadContentFromMessage(this.media, this.type);
|
|
44
|
+
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of stream)
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
const buffer = Buffer.concat(chunks);
|
|
49
|
+
|
|
50
|
+
return new MessageMedia(buffer, this.media.mimetype);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// returns the sender's name and phone number
|
|
54
|
+
async getContact() {
|
|
55
|
+
const jid = this.fromMe ? this.socket.user.id : this.author;
|
|
56
|
+
const number = jid.split('@')[0];
|
|
57
|
+
return {name: this.fromName, number}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// get chat name and if it is a group or not
|
|
61
|
+
async getChat(){
|
|
62
|
+
const isGroup = this.fromId.endsWith("@g.us");
|
|
63
|
+
let name = this.fromName;
|
|
64
|
+
|
|
65
|
+
if (isGroup) {
|
|
66
|
+
const metadata = await this.socket.groupMetadata(this.fromId);
|
|
67
|
+
name = metadata.subject;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {name, isGroup}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// send message as a reply by adding 'quoted' in the options and call 'send()'
|
|
74
|
+
async reply(content, options = {}){
|
|
75
|
+
return this.send(content, {...options, quoted: this.baileysMsg})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// calls the socket's send function
|
|
79
|
+
async send(content, options = {}){
|
|
80
|
+
return this.socket.send(this.fromId, content, options)
|
|
81
|
+
}
|
|
82
|
+
}
|
package/MessageMedia.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
import mime from 'mime-types'
|
|
4
|
+
|
|
5
|
+
// Wrapper for media content
|
|
6
|
+
export default class MessageMedia {
|
|
7
|
+
// stores data and mimetype
|
|
8
|
+
constructor(data, mimetype) {
|
|
9
|
+
this.data = data
|
|
10
|
+
this.mimetype = mimetype;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Creates MessageMedia from a local file
|
|
14
|
+
static fromFile(path) {
|
|
15
|
+
const buffer = fs.readFileSync(path)
|
|
16
|
+
const mimetype = mime.lookup(path);
|
|
17
|
+
return new MessageMedia(buffer, mimetype)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Creates MessageMedia from a URL (must point to a valid media file)
|
|
21
|
+
static async fromUrl(url) {
|
|
22
|
+
const res = await axios.get(url, { responseType: 'arraybuffer' })
|
|
23
|
+
return new MessageMedia(Buffer.from(res.data), res.headers['content-type'])
|
|
24
|
+
}
|
|
25
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SWAN
|
|
2
|
+
|
|
3
|
+
**SWAN** (**S**imple **W**hatsApp **A**PI for **N**ode) is a lightweight wrapper around the Baileys library that provides a simpler and more intuitive interface for building WhatsApp bots in Node.js.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Simple socket management with automatic reconnection.
|
|
10
|
+
- QR code authentication.
|
|
11
|
+
- Easy message sending.
|
|
12
|
+
- Built-in reply support.
|
|
13
|
+
- Media download and upload.
|
|
14
|
+
- Sticker creation from images and videos.
|
|
15
|
+
- Convenient wrappers for contacts, chats and messages.
|
|
16
|
+
- Minimal API with no unnecessary abstractions.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install swan-api
|
|
22
|
+
```
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Basic bot
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import {Socket} from "swan-api";
|
|
29
|
+
|
|
30
|
+
const socket = new Socket();
|
|
31
|
+
|
|
32
|
+
socket.on("ready", () => console.log('Socket Ready!'))
|
|
33
|
+
|
|
34
|
+
socket.on("newMessage", async (msg) => {
|
|
35
|
+
if (msg.text.toLowerCase() === "ping")
|
|
36
|
+
await msg.reply("pong!");
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Media and message handling
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import {Socket, MessageMedia} from "swan-api";
|
|
44
|
+
|
|
45
|
+
const socket = new Socket();
|
|
46
|
+
|
|
47
|
+
socket.on("ready", () => {
|
|
48
|
+
console.log("Bot connected!");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
socket.on("newMessage", async (msg) => {
|
|
52
|
+
// Avoids infinite loop
|
|
53
|
+
if (msg.fromMe)
|
|
54
|
+
return;
|
|
55
|
+
|
|
56
|
+
console.log(`Message from ${msg.author}: ${msg.text}`);
|
|
57
|
+
|
|
58
|
+
await msg.reply("Processing your message...");
|
|
59
|
+
|
|
60
|
+
// If an image was sent, sends it back as a sticker
|
|
61
|
+
const media = await msg.downloadMedia();
|
|
62
|
+
if (media)
|
|
63
|
+
await msg.reply(media, {asSticker: true});
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### Socket
|
|
70
|
+
|
|
71
|
+
Creates and manages the WhatsApp connection.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const socket = new Socket();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Events
|
|
78
|
+
|
|
79
|
+
| Event | Description |
|
|
80
|
+
| ------ | ----------- |
|
|
81
|
+
| `ready` | Fired after a successful connection. |
|
|
82
|
+
| `newMessage` | Fired whenever a new message is received. |
|
|
83
|
+
|
|
84
|
+
### Message
|
|
85
|
+
|
|
86
|
+
Represents a received WhatsApp message.
|
|
87
|
+
|
|
88
|
+
#### Properties
|
|
89
|
+
|
|
90
|
+
| Property | Description |
|
|
91
|
+
| -------- | ----------- |
|
|
92
|
+
| `text` | Message text. |
|
|
93
|
+
| `type` | Message type. |
|
|
94
|
+
| `fromId` | Chat JID. |
|
|
95
|
+
| `fromName` | Sender push name. |
|
|
96
|
+
| `fromMe` | Whether the message was sent by the current user. |
|
|
97
|
+
| `author` | Sender JID. |
|
|
98
|
+
| `media` | Attached media information, if any. |
|
|
99
|
+
| `quoted` | Quoted `Message`, if present. |
|
|
100
|
+
|
|
101
|
+
#### Methods
|
|
102
|
+
|
|
103
|
+
##### `reply(content, options?)`
|
|
104
|
+
|
|
105
|
+
Replies to the current message.
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
await msg.reply("Hi!");
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
##### `send(content, options?)`
|
|
112
|
+
|
|
113
|
+
Sends a message to the current chat.
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
await msg.send("Hello everyone!");
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
##### `downloadMedia()`
|
|
120
|
+
|
|
121
|
+
Downloads the attached media.
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const media = await msg.downloadMedia();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
##### `getContact()`
|
|
128
|
+
|
|
129
|
+
Returns sender information.
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
const contact = await msg.getContact();
|
|
133
|
+
|
|
134
|
+
console.log(contact.name);
|
|
135
|
+
console.log(contact.number);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
##### `getChat()`
|
|
139
|
+
|
|
140
|
+
Returns chat information.
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
const chat = await msg.getChat();
|
|
144
|
+
|
|
145
|
+
console.log(chat.name);
|
|
146
|
+
console.log(chat.isGroup);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### MessageMedia
|
|
152
|
+
|
|
153
|
+
Represents a media file.
|
|
154
|
+
|
|
155
|
+
#### Static methods
|
|
156
|
+
|
|
157
|
+
##### `MessageMedia.fromFile(path)`
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
const media = MessageMedia.fromFile("./photo.jpg");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
##### `MessageMedia.fromUrl(url)`
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const media = await MessageMedia.fromUrl("https://example.com/image.png");
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Exports
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import {
|
|
175
|
+
Socket,
|
|
176
|
+
Message,
|
|
177
|
+
MessageMedia
|
|
178
|
+
} from "swan-api";
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
package/Socket.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import makeWASocket, { useMultiFileAuthState, fetchLatestBaileysVersion, DisconnectReason } from '@whiskeysockets/baileys'
|
|
2
|
+
|
|
3
|
+
import P from 'pino'
|
|
4
|
+
import qrcode from 'qrcode-terminal'
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import { Sticker } from 'wa-sticker-formatter'
|
|
7
|
+
|
|
8
|
+
import Message from './Message.js';
|
|
9
|
+
import MessageMedia from './MessageMedia.js';
|
|
10
|
+
|
|
11
|
+
//Wrapper Class For Baileys Socket
|
|
12
|
+
export default class Socket extends EventEmitter {
|
|
13
|
+
// creates a socket instance
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.connect();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// tries to authenticate and bind default events
|
|
20
|
+
async connect() {
|
|
21
|
+
await this.authenticate();
|
|
22
|
+
this.bindEvents();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//creates new baileys socket and authenticate
|
|
26
|
+
async authenticate(){
|
|
27
|
+
const { state, saveCreds } = await useMultiFileAuthState('auth')
|
|
28
|
+
const { version } = await fetchLatestBaileysVersion()
|
|
29
|
+
|
|
30
|
+
this.socket = makeWASocket({version, auth: state, logger: P({level: 'silent'})});
|
|
31
|
+
this.user = this.socket.user;
|
|
32
|
+
this.socket.ev.on('creds.update', saveCreds)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//bind events from baileys to default treatment functions
|
|
36
|
+
bindEvents(){
|
|
37
|
+
//each new message emits a 'newMessage' event and a 'Message' object
|
|
38
|
+
this.socket.ev.on('messages.upsert', ({messages}) => {
|
|
39
|
+
for (const msg of messages){
|
|
40
|
+
if (!msg.message)
|
|
41
|
+
continue;
|
|
42
|
+
this.emit("newMessage", new Message(this, msg));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
//emits 'ready' when opens connection
|
|
47
|
+
//on connnection lost, tries to reconnect
|
|
48
|
+
this.socket.ev.on('connection.update', (update) => {
|
|
49
|
+
const { connection, lastDisconnect, qr } = update;
|
|
50
|
+
if (qr)
|
|
51
|
+
qrcode.generate(qr, {small: true})
|
|
52
|
+
|
|
53
|
+
if (connection == "close"){
|
|
54
|
+
const shouldReconnect = lastDisconnect?.error?.output?.statusCode
|
|
55
|
+
!== DisconnectReason.loggedOut;
|
|
56
|
+
if (shouldReconnect)
|
|
57
|
+
this.connect();
|
|
58
|
+
} else if (connection == 'open')
|
|
59
|
+
this.emit('ready');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// send message wrapper function
|
|
65
|
+
async send(jid, content, opts = {}){
|
|
66
|
+
// replies text messages directly
|
|
67
|
+
if (typeof content === 'string')
|
|
68
|
+
return this.socket.sendMessage(jid, {text: content}, opts)
|
|
69
|
+
|
|
70
|
+
// from this point on 'content' must be 'MessageMedia'
|
|
71
|
+
if (!(content instanceof MessageMedia))
|
|
72
|
+
throw new Error(`SWAN send(): invalid content type: ${typeof(content)}`);
|
|
73
|
+
|
|
74
|
+
// simplify data access
|
|
75
|
+
const buffer = content.data
|
|
76
|
+
const mimetype = content.mimetype
|
|
77
|
+
|
|
78
|
+
// parse media for sticker creation
|
|
79
|
+
if (opts.asSticker){
|
|
80
|
+
const isVideo = mimetype?.startsWith('video') || mimetype === 'image/gif'
|
|
81
|
+
// video proportion must be 'full' to not get corrupted frames
|
|
82
|
+
if (isVideo)
|
|
83
|
+
opts.stickerProportion = 'full'
|
|
84
|
+
|
|
85
|
+
// tries to create the sticker at the highest quality,
|
|
86
|
+
// if the resulting file exceeds WhatsApp's 1MB limit
|
|
87
|
+
// the quality is reduced by 10% and retried
|
|
88
|
+
// throws an exception if quality goes below 0%
|
|
89
|
+
let final_quality = 1;
|
|
90
|
+
while (final_quality >= 0) {
|
|
91
|
+
const sticker = new Sticker(buffer, {
|
|
92
|
+
pack: opts.stickerName,
|
|
93
|
+
author: opts.stickerAuthor,
|
|
94
|
+
type: opts.stickerProportion,
|
|
95
|
+
quality: final_quality,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const webp = await sticker.toBuffer();
|
|
99
|
+
const size = (webp.length/1024)/1024;
|
|
100
|
+
if (size >= 1)
|
|
101
|
+
final_quality -= 0.1;
|
|
102
|
+
else
|
|
103
|
+
return await this.socket.sendMessage(jid, {sticker: webp}, opts)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//send regular files
|
|
109
|
+
for (const type of ['image', 'video', 'application'])
|
|
110
|
+
if (mimetype.startsWith(`${type}/`))
|
|
111
|
+
return this.socket.sendMessage(jid, {[type]: buffer, mimetype}, opts)
|
|
112
|
+
}
|
|
113
|
+
}
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "swan-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple WhatsApp API for Node",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"whatsapp",
|
|
7
|
+
"baileys",
|
|
8
|
+
"bot",
|
|
9
|
+
"api",
|
|
10
|
+
"wrapper",
|
|
11
|
+
"node"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/cauesamonek/SWAN",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/cauesamonek/SWAN.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/cauesamonek/SWAN/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Cauê Mateus Gonçalves Venturin Samonek",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"Socket.js",
|
|
31
|
+
"Message.js",
|
|
32
|
+
"MessageMedia.js",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@whiskeysockets/baileys": "^6.7.18",
|
|
41
|
+
"axios": "^1.11.0",
|
|
42
|
+
"pino": "^9.7.0",
|
|
43
|
+
"mime-types": "^3.0.1",
|
|
44
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"wa-sticker-formatter": "^4.4.4"
|
|
46
|
+
}
|
|
47
|
+
}
|