547 lines
18 KiB
TypeScript
547 lines
18 KiB
TypeScript
import { PlatformAdapter, FetchOptions } from "./platform-adapter";
|
|
import {
|
|
Message as StdMessage,
|
|
User as StdUser,
|
|
SentMessage,
|
|
Attachment,
|
|
} from "./message";
|
|
import {
|
|
Client as WAClient,
|
|
Message as WAMessage,
|
|
LocalAuth,
|
|
MessageMedia,
|
|
} from "whatsapp-web.js";
|
|
import { UserConfig, userConfigs } from "../config";
|
|
import { eventManager } from "./events";
|
|
import { return_current_listeners } from "../tools/events";
|
|
import Fuse from "fuse.js";
|
|
|
|
// const allowedUsers = ["pooja", "raj"];
|
|
const allowedUsers: string[] = [];
|
|
|
|
export class WhatsAppAdapter implements PlatformAdapter {
|
|
private client: WAClient;
|
|
private botUserId: string = "918884016724@c.us";
|
|
|
|
public config = {
|
|
indicators: {
|
|
typing: false,
|
|
processing: false,
|
|
},
|
|
};
|
|
|
|
constructor() {
|
|
this.client = new WAClient({
|
|
authStrategy: new LocalAuth(),
|
|
});
|
|
try {
|
|
this.client.on("ready", () => {
|
|
console.log("WhatsApp Client is ready!");
|
|
});
|
|
|
|
this.client.initialize();
|
|
} catch (error) {
|
|
console.log(`Failed to initialize WhatsApp client: `, error);
|
|
}
|
|
}
|
|
public getUserById(userId: string): UserConfig | null {
|
|
const userConfig = userConfigs.find((user) =>
|
|
user.identities.some(
|
|
(identity) =>
|
|
identity.platform === "whatsapp" && `${identity.id}@c.us` === userId
|
|
)
|
|
);
|
|
|
|
if (!userConfig) {
|
|
// console.log(`User not found for WhatsApp ID: ${userId}`);
|
|
return null;
|
|
}
|
|
|
|
return userConfig;
|
|
}
|
|
|
|
public onMessage(callback: (message: StdMessage) => void): void {
|
|
this.client.on("message_create", async (waMessage: WAMessage) => {
|
|
// emit internal event only if text message and there is an active listener
|
|
const listeners = return_current_listeners();
|
|
if (
|
|
typeof waMessage.body === "string" &&
|
|
!waMessage.fromMe &&
|
|
listeners.find((l) => l.eventId.includes("whatsapp"))
|
|
) {
|
|
const contact = await this.client.getContactById(waMessage.from);
|
|
eventManager.emit("got_whatsapp_message", {
|
|
sender_id: waMessage.from,
|
|
sender_contact_name:
|
|
contact.name || contact.shortName || contact.pushname || "NA",
|
|
timestamp: waMessage.timestamp,
|
|
content: waMessage.body,
|
|
profile_image_url: await contact.getProfilePicUrl(),
|
|
is_group_message: contact.isGroup.toString(),
|
|
});
|
|
}
|
|
|
|
// user must exist in userConfigs
|
|
const usr = this.getUserById(waMessage.from);
|
|
if (!usr) {
|
|
console.log(`Ignoring ID: ${waMessage.from}`);
|
|
return;
|
|
}
|
|
|
|
// user must be in allowedUsers
|
|
if (!allowedUsers.includes(usr.name)) {
|
|
// console.log(`User not allowed: ${usr.name}`);
|
|
return;
|
|
}
|
|
|
|
// Ignore messages sent by the bot
|
|
if (waMessage.fromMe) return;
|
|
const message = await this.convertMessage(waMessage);
|
|
|
|
callback(message);
|
|
});
|
|
}
|
|
|
|
public async sendMessage(channelId: string, content: string): Promise<void> {
|
|
await this.client.sendMessage(channelId, content);
|
|
}
|
|
|
|
public async fetchMessages(
|
|
channelId: string,
|
|
options: FetchOptions
|
|
): Promise<StdMessage[]> {
|
|
const chat = await this.client.getChatById(channelId);
|
|
const messages = await chat.fetchMessages({ limit: options.limit || 10 });
|
|
|
|
const stdMessages: StdMessage[] = [];
|
|
|
|
for (const msg of messages) {
|
|
const stdMsg = await this.convertMessage(msg);
|
|
stdMessages.push(stdMsg);
|
|
}
|
|
|
|
// Return messages in chronological order
|
|
return stdMessages.reverse();
|
|
}
|
|
|
|
public getBotId(): string {
|
|
return this.botUserId;
|
|
}
|
|
|
|
public async createMessageInterface(userId: string): Promise<StdMessage> {
|
|
try {
|
|
const contact = await this.client.getContactById(userId);
|
|
const stdMessage: StdMessage = {
|
|
platform: "whatsapp",
|
|
platformAdapter: this,
|
|
id: userId,
|
|
author: {
|
|
id: userId,
|
|
username:
|
|
contact.name || contact.shortName || contact.pushname || "NA",
|
|
config: this.getUserById(userId),
|
|
},
|
|
content: "", // Placeholder content
|
|
timestamp: new Date(), // Placeholder timestamp
|
|
channelId: userId, // Assuming userId is the channelId
|
|
source: null, // Placeholder source
|
|
threadId: undefined, // Placeholder threadId
|
|
isDirectMessage: async () => true,
|
|
sendDirectMessage: async (recipientId, messageData) => {
|
|
const tcontact = await this.client.getContactById(recipientId);
|
|
const tchat = await tcontact.getChat();
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
|
|
await tchat.sendMessage(messageData.content || "", { media });
|
|
},
|
|
sendMessageToChannel: async (channelId, messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
await this.client.sendMessage(channelId, messageData.content || "", {
|
|
media,
|
|
});
|
|
},
|
|
sendFile: async (fileUrl, fileName) => {
|
|
const media = MessageMedia.fromFilePath(fileUrl);
|
|
await this.client.sendMessage(userId, media, {
|
|
caption: fileName,
|
|
});
|
|
},
|
|
fetchChannelMessages: async (limit: number) => {
|
|
const chat = await this.client.getChatById(userId);
|
|
const messages = await chat.fetchMessages({ limit });
|
|
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
|
},
|
|
getUserRoles: () => {
|
|
const userConfig = userConfigs.find((user) =>
|
|
user.identities.some(
|
|
(identity) =>
|
|
identity.platform === "whatsapp" && identity.id === userId
|
|
)
|
|
);
|
|
return userConfig ? userConfig.roles : ["user"];
|
|
},
|
|
send: async (messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
const sentMessage = await this.client.sendMessage(
|
|
userId,
|
|
messageData.content || "",
|
|
{
|
|
media,
|
|
}
|
|
);
|
|
return this.convertSentMessage(sentMessage);
|
|
},
|
|
reply: async (messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
|
|
const sentMessage = await this.client.sendMessage(
|
|
userId,
|
|
messageData.content || "",
|
|
{
|
|
media,
|
|
}
|
|
);
|
|
return this.convertSentMessage(sentMessage);
|
|
},
|
|
sendTyping: async () => {
|
|
// WhatsApp Web API does not support sending typing indicators directly
|
|
// This method can be left as a no-op or you can implement a workaround if possible
|
|
},
|
|
};
|
|
|
|
return stdMessage;
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to create message interface for WhatsApp user ${userId}: ${error}`
|
|
);
|
|
}
|
|
}
|
|
async searchUser(query: string): Promise<StdUser[]> {
|
|
try {
|
|
const contacts = await this.client.getContacts();
|
|
|
|
const stdcontacts = await Promise.all(
|
|
contacts
|
|
.filter((c) => c.isMyContact)
|
|
.map(async (contact) => {
|
|
return {
|
|
id: contact.id._serialized,
|
|
username:
|
|
contact.pushname || contact.name || contact.shortName || "NA",
|
|
config: this.getUserById(contact.id._serialized),
|
|
meta: {
|
|
about: contact.getAbout(),
|
|
verifiedName: contact.verifiedName,
|
|
shortName: contact.shortName,
|
|
pushname: contact.pushname,
|
|
name: contact.name,
|
|
profilePicUrl: await contact.getProfilePicUrl(),
|
|
},
|
|
};
|
|
})
|
|
);
|
|
|
|
console.log("Starting search");
|
|
const fuse = new Fuse(stdcontacts, {
|
|
keys: ["id", "username"],
|
|
threshold: 0.3,
|
|
});
|
|
const results = fuse.search(query);
|
|
console.log("search done", results.length);
|
|
return results.map((result) => result.item);
|
|
} catch (error) {
|
|
throw new Error(`Failed to search for WhatsApp contacts: ${error}`);
|
|
}
|
|
}
|
|
// Expose this method so it can be accessed elsewhere
|
|
public getMessageInterface = this.createMessageInterface;
|
|
|
|
private async convertMessage(waMessage: WAMessage): Promise<StdMessage> {
|
|
const contact = await waMessage.getContact();
|
|
|
|
const stdUser: StdUser = {
|
|
id: contact.id._serialized,
|
|
username: contact.name || contact.shortName || contact.pushname || "NA",
|
|
config: this.getUserById(contact.id._serialized),
|
|
};
|
|
|
|
// Convert attachments
|
|
let attachments: Attachment[] = [];
|
|
if (waMessage.hasMedia) {
|
|
const media = await waMessage.downloadMedia();
|
|
|
|
attachments.push({
|
|
url: "", // WhatsApp does not provide a direct URL to the media
|
|
data: media.data,
|
|
contentType: media.mimetype,
|
|
type: waMessage.type,
|
|
});
|
|
}
|
|
|
|
const stdMessage: StdMessage = {
|
|
id: waMessage.id._serialized,
|
|
content: waMessage.body,
|
|
platformAdapter: this,
|
|
author: stdUser,
|
|
timestamp: new Date(waMessage.timestamp * 1000),
|
|
channelId: waMessage.from,
|
|
threadId: waMessage.hasQuotedMsg
|
|
? (await waMessage.getQuotedMessage()).id._serialized
|
|
: undefined,
|
|
source: waMessage,
|
|
platform: "whatsapp",
|
|
isDirectMessage: async () => {
|
|
const chat = await this.client.getChatById(waMessage.from);
|
|
return !chat.isGroup; // Returns true if not a group chat
|
|
},
|
|
sendDirectMessage: async (recipientId, messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
await this.client.sendMessage(recipientId, messageData.content || "", {
|
|
media,
|
|
});
|
|
},
|
|
sendMessageToChannel: async (channelId, messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
await this.client.sendMessage(channelId, messageData.content || "", {
|
|
media,
|
|
});
|
|
},
|
|
sendFile: async (fileUrl, fileName) => {
|
|
const media = MessageMedia.fromFilePath(fileUrl);
|
|
await this.client.sendMessage(waMessage.from, media, {
|
|
caption: fileName,
|
|
});
|
|
},
|
|
fetchChannelMessages: async (limit: number) => {
|
|
const chat = await this.client.getChatById(waMessage.from);
|
|
const messages = await chat.fetchMessages({ limit });
|
|
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
|
},
|
|
getUserRoles: () => {
|
|
const userConfig = userConfigs.find((user) =>
|
|
user.identities.some(
|
|
(identity) =>
|
|
identity.platform === "whatsapp" &&
|
|
identity.id === contact.id._serialized
|
|
)
|
|
);
|
|
return userConfig ? userConfig.roles : ["user"];
|
|
},
|
|
send: async (messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
const sentMessage = await this.client.sendMessage(
|
|
waMessage.from,
|
|
messageData.content || "",
|
|
{
|
|
media,
|
|
}
|
|
);
|
|
return this.convertSentMessage(sentMessage);
|
|
},
|
|
reply: async (messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
const sentMessage = await this.client.sendMessage(
|
|
waMessage.from,
|
|
messageData.content || "",
|
|
{
|
|
media,
|
|
}
|
|
);
|
|
return this.convertSentMessage(sentMessage);
|
|
},
|
|
sendTyping: async () => {
|
|
// WhatsApp Web API does not support sending typing indicators directly
|
|
// You may leave this as a no-op
|
|
},
|
|
};
|
|
|
|
return stdMessage;
|
|
}
|
|
|
|
public async fetchMessageById(
|
|
channelId: string,
|
|
messageId: string
|
|
): Promise<StdMessage | null> {
|
|
try {
|
|
const waMessage = await this.client.getMessageById(messageId);
|
|
if (waMessage) {
|
|
const stdMessage = await this.convertMessage(waMessage);
|
|
return stdMessage;
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to fetch message by ID: ${error}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private async convertSentMessage(
|
|
sentWAMessage: WAMessage
|
|
): Promise<SentMessage> {
|
|
const contact = await sentWAMessage.getContact();
|
|
|
|
return {
|
|
id: sentWAMessage.id._serialized,
|
|
platformAdapter: this,
|
|
content: sentWAMessage.body,
|
|
author: {
|
|
id: contact.id._serialized,
|
|
username:
|
|
contact.name ||
|
|
contact.shortName ||
|
|
contact.pushname ||
|
|
contact.number,
|
|
config: this.getUserById(contact.id._serialized),
|
|
},
|
|
timestamp: new Date(sentWAMessage.timestamp * 1000),
|
|
channelId: sentWAMessage.from,
|
|
threadId: sentWAMessage.hasQuotedMsg
|
|
? (await sentWAMessage.getQuotedMessage()).id._serialized
|
|
: undefined,
|
|
source: sentWAMessage,
|
|
platform: "whatsapp",
|
|
deletable: true,
|
|
delete: async () => {
|
|
await sentWAMessage.delete();
|
|
},
|
|
edit: async (messageData) => {
|
|
sentWAMessage.edit(messageData.content || "");
|
|
},
|
|
reply: async (messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
const replyMessage = await sentWAMessage.reply(
|
|
messageData.content || "",
|
|
sentWAMessage.id._serialized,
|
|
{ media }
|
|
);
|
|
return this.convertSentMessage(replyMessage);
|
|
},
|
|
send: async (messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
const sentMessage = await this.client.sendMessage(
|
|
sentWAMessage.from,
|
|
messageData.content || "",
|
|
{
|
|
media,
|
|
}
|
|
);
|
|
return this.convertSentMessage(sentMessage);
|
|
},
|
|
getUserRoles: () => {
|
|
const userConfig = userConfigs.find((user) =>
|
|
user.identities.some(
|
|
(identity) =>
|
|
identity.platform === "whatsapp" &&
|
|
identity.id === contact.id._serialized
|
|
)
|
|
);
|
|
return userConfig ? userConfig.roles : ["user"];
|
|
},
|
|
isDirectMessage: async () => {
|
|
const chat = await this.client.getChatById(sentWAMessage.from);
|
|
return !chat.isGroup; // Returns true if not a group chat
|
|
},
|
|
sendDirectMessage: async (recipientId, messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
await this.client.sendMessage(recipientId, messageData.content || "", {
|
|
media,
|
|
});
|
|
},
|
|
sendMessageToChannel: async (channelId, messageData) => {
|
|
let media;
|
|
if (messageData.file && "url" in messageData.file) {
|
|
media = await MessageMedia.fromUrl(messageData.file.url);
|
|
}
|
|
if (messageData.file && "path" in messageData.file) {
|
|
media = MessageMedia.fromFilePath(messageData.file.path);
|
|
}
|
|
await this.client.sendMessage(channelId, messageData.content || "", {
|
|
media,
|
|
});
|
|
},
|
|
sendFile: async (fileUrl, fileName) => {
|
|
const media = MessageMedia.fromFilePath(fileUrl);
|
|
await this.client.sendMessage(sentWAMessage.from, media, {
|
|
caption: fileName,
|
|
});
|
|
},
|
|
fetchChannelMessages: async (limit: number) => {
|
|
const chat = await this.client.getChatById(sentWAMessage.from);
|
|
const messages = await chat.fetchMessages({ limit });
|
|
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
|
},
|
|
sendTyping: async () => {
|
|
// WhatsApp Web API does not support sending typing indicators directly
|
|
// You may leave this as a no-op
|
|
},
|
|
};
|
|
}
|
|
}
|