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 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
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ export { default as Socket } from './Socket.js';
2
+ export { default as Message } from './Message.js';
3
+ export { default as MessageMedia } from './MessageMedia.js';
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
+ }