跳到主要内容

Room Bot

Powered by Wechaty JavaScript

Room bot is a practical example which illustrates how to do room handling. With room bot you can do the following:

  • Find a room
  • Add people to the room
  • Delete people from the room
  • Change the room topic
  • Monitor room events

Try out the bot

Edit wechaty-room-bot

You can try out the Wechaty Room bot using this interactive CodeSandbox.

There is a TODO where you have to specify one of your friends' name (the CodeSandbox can be edited by using the above button). After that just scan the generated QR code with the WeChat app, and you are ready to play with the bot!

Requirements

  1. Node.js v16+
  2. Wechaty Puppet Service TOKEN (if you want to use RPA protocols other than Web)

Getting Started

Before getting started make sure you have Node.js installed on your system. If you do not have Node.js installed (or have a version below 12), then you need to install the latest version of Node.js by following the links below:

Node.js installation docs

Installation guide for Node.js in other platforms can be found here.

You can head over to Building the bot section to learn how to build the bot on your own.

Otherwise if you want to try out the bot on your local system, follow the steps below:

1. Clone the repository

Use the following command to clone the Github repository and navigate to the directory.

git clone https://github.com/wechaty/wechaty-getting-started.git
cd wechaty-getting-started

2. Install dependencies

Install the npm packages required for running the bot using the following command:

npm install

3. Run the bot

You have to export/set environment variables.

export WECHATY_LOG=verbose
export WECHATY_PUPPET=wechaty-puppet-wechat

There are various Wechaty puppets available, you can know more about them here

For running the bot, use the following command:

npx ts-node examples/advanced/room-bot.js

This will generate a QR code. Scan it using Wechat/Whatsapp and you are ready to go.

Building the bot

Let's get started with building room-bot using Wechaty.

1. Initializse project

Create a new folder called room-bot and move into that directory.

mkdir room-bot
cd room-bot

Use the following command to initialize an npm project

npm init -y

2. Install dependencies

For building the room bot, you will require these dependencies:

For installing these dependencies run the following commands:

  • For installing wechaty
npm install wechaty
  • For installing qrcode-terminal
npm install qrcode-terminal

You will also need to add dependencies for using any Wechaty Puppet which helps to integrate Wechaty with various instant messaging (IM) systems (such as WeChat, WhatsApp, and WeCom):

  1. If you want to use WhatsApp, install wechaty-puppet-whatsapp:

    npm install wechaty-puppet-whatsapp
  2. If you want to use WeChat, you can try the following puppets:

    • Web Protocol: Install wechaty-puppet-wechat:

      npm install wechaty-puppet-wechat
    • iPad Protocol:

      • padlocal: Install wechaty-puppet-padlocal:

        npm install wechaty-puppet-padlocal

        Then get a token like puppet_padlocal_XXX, know more about puppet service padlocal here.

      • paimon: Install wechaty-puppet-service:

        npm install wechaty-puppet-service

        Then get a token like puppet_paimon_XXX, know more about puppet service paimon here.

  3. If you want to use WeCom, install wechaty-puppet-service:

    npm install wechaty-puppet-service

    Then get a token like puppet_wxwork_XXXXX, more about puppet service wxwork here.

You can find more information about the puppets here.

Now, you are ready to write the main code for the bot.

3. Writing code for the bot

Create a new file room-bot.js. You will be writing code here.

Let's import required packages in room-bot.js file and create a HELPER_CONTACT_NAME variable which will be used to create a room.

// TODO: Put the name of one of your friend here,
// or room create function will not work.
const HELPER_CONTACT_NAME = "huan";

import qrTerm from 'qrcode-terminal';

import { Wechaty, log } from 'wechaty';

Now, we will write some functions which will be required for handling different events returned by bot.

manageDingRoom

This function will help you find a room, join it, leave it, and change the topic of the room.

async function manageDingRoom() {
log.info("Bot", "manageDingRoom()");

/**
* Find Room
*/
try {
const room = await bot.Room.find({ topic: /^ding/i });
if (!room) {
log.warn("Bot", "there is no room topic ding(yet)");
return;
}
log.info("Bot", 'start monitor "ding" room join/leave/topic event');

/**
* Event: Join
*/
room.on("join", function (inviteeList, inviter) {
log.verbose(
"Bot",
'Room EVENT: join - "%s", "%s"',
inviteeList.map((c) => c.name()).join(", "),
inviter.name()
);
console.log("room.on(join) id:", this.id);
checkRoomJoin.call(this, room, inviteeList, inviter);
});

/**
* Event: Leave
*/
room.on("leave", (leaverList, remover) => {
log.info(
"Bot",
'Room EVENT: leave - "%s" leave(remover "%s"), byebye',
leaverList.join(","),
remover || "unknown"
);
});

/**
* Event: Topic Change
*/
room.on("topic", (topic, oldTopic, changer) => {
log.info(
"Bot",
'Room EVENT: topic - changed from "%s" to "%s" by member "%s"',
oldTopic,
topic,
changer.name()
);
});
} catch (e) {
log.warn("Bot", 'Room.find rejected: "%s"', e.stack);
}
}

checkRoomJoin

This function checks who can send an invitation and who can't. If the invitation is sent by the owner, then the person could join the room. Otherwise, the invitation will be canceled and the person has to resend ding to the owner so that the owner can send him/her a new valid invitation.

async function checkRoomJoin(room, inviteeList, inviter) {
log.info(
"Bot",
'checkRoomJoin("%s", "%s", "%s")',
await room.topic(),
inviteeList.map((c) => c.name()).join(","),
inviter.name()
);

try {
// let to, content
const userSelf = bot.userSelf();

if (inviter.id !== userSelf.id) {
await room.say(
"RULE1: Invitation is limited to me, the owner only. Please do not invite people without notifying me.",
inviter
);
await room.say(
'Please contact me: by send "ding" to me, I will re-send you an invitation. Now I will remove you out, sorry.',
inviteeList
);

await room.topic("ding - warn " + inviter.name());
setTimeout((_) => inviteeList.forEach((c) => room.del(c)), 10 * 1000);
} else {
await room.say("Welcome to my room! :)");

let welcomeTopic;
welcomeTopic = inviteeList.map((c) => c.name()).join(", ");
await room.topic("ding - welcome " + welcomeTopic);
}
} catch (e) {
log.error("Bot", "checkRoomJoin() exception: %s", e.stack);
}
}

putInRoom

This function adds people to the room.

async function putInRoom(contact, room) {
log.info("Bot", 'putInRoom("%s", "%s")', contact.name(), await room.topic());

try {
await room.add(contact);
setTimeout((_) => room.say("Welcome ", contact), 10 * 1000);
} catch (e) {
log.error("Bot", "putInRoom() exception: " + e.stack);
}
}

getOutRoom

This function removes people from room, if they said ding inside a room.

async function getOutRoom(contact, room) {
log.info("Bot", 'getOutRoom("%s", "%s")', contact, room);

try {
await room.say('You said "ding" in my room, I will remove you out.');
await room.del(contact);
} catch (e) {
log.error("Bot", "getOutRoom() exception: " + e.stack);
}
}

getHelperContact

This function returns a Helper contact that you defined in the beginning.

function getHelperContact() {
log.info("Bot", "getHelperContact()");

// create a new room at least need 3 contacts
return bot.Contact.find({ name: HELPER_CONTACT_NAME });
}

createDingRoom

This function is used to create a room but if Helper contact is not found, then it asks you to set it first.

async function createDingRoom(contact) {
log.info("Bot", 'createDingRoom("%s")', contact);

try {
const helperContact = await getHelperContact();

if (!helperContact) {
log.warn("Bot", "getHelperContact() found nobody");
await contact.say(`You don't have a friend called "${HELPER_CONTACT_NAME}",
because create a new room at least need 3 contacts, please set [HELPER_CONTACT_NAME] in the code first!`);
return;
}

log.info("Bot", 'getHelperContact() ok. got: "%s"', helperContact.name());

const contactList = [contact, helperContact];
log.verbose("Bot", 'contactList: "%s"', contactList.join(","));

await contact.say(
`There isn't ding room. I'm trying to create a room with "${helperContact.name()}" and you`
);
const room = await bot.Room.create(contactList, "ding");
log.info("Bot", 'createDingRoom() new ding room created: "%s"', room);

await room.topic("ding - created");
await room.say("ding - created");

return room;
} catch (e) {
log.error("Bot", "getHelperContact() exception:", e.stack);
throw e;
}
}

Now initializing the bot by providing the name.

const bot = new Wechaty({
name: "room-bot",
})

Assigning proper functions to call when an event is triggered.

bot
.on("scan", (qrcode, status) => {
qrTerm.generate(qrcode, { small: true });
console.log(`${qrcode}\n[${status}] Scan QR Code in above url to login: `);
})
.on("logout", (user) => log.info("Bot", `"${user.name()}" logouted`))
.on("error", (e) => log.info("Bot", "error: %s", e))
  • When scan is triggered, it generates QR code.
  • logout will display the name of the user along with the status logouted.
  • error is used to notify if the bot encounters an error.

Global Event: login

This event gets the user and displays the login status of the user. After that, it displays the msg saying setting to manageDingRoom() after 3 seconds ... which means it will call manageDingRoom function after 3 seconds.

.on("login", async function (user) {
let msg = `${user.name()} logined`;

log.info("Bot", msg);
await this.say(msg);

msg = `setting to manageDingRoom() after 3 seconds ... `;
log.info("Bot", msg);
await this.say(msg);

setTimeout(manageDingRoom.bind(this), 3000);
})

Global Event: room-join

room-join event lets people join a respective room.

.on("room-join", async function (room, inviteeList, inviter) {
log.info(
"Bot",
'EVENT: room-join - Room "%s" got new member "%s", invited by "%s"',
await room.topic(),
inviteeList.map((c) => c.name()).join(","),
inviter.name()
);
console.log("bot room-join room id:", room.id);
const topic = await room.topic();
await room.say(`welcome to "${topic}"!`, inviteeList[0]);
})

Global Event: room-leave

This event removes people from the room.

.on("room-leave", async function (room, leaverList) {
log.info(
"Bot",
'EVENT: room-leave - Room "%s" lost member "%s"',
await room.topic(),
leaverList.map((c) => c.name()).join(",")
);
const topic = await room.topic();
const name = leaverList[0] ? leaverList[0].name() : "no contact!";
await room.say(`kick off "${name}" from "${topic}"!`);
})

Global Event: room-topic

Using this event, you can change the topic of the room.

.on("room-topic", async function (room, topic, oldTopic, changer) {
try {
log.info(
"Bot",
'EVENT: room-topic - Room "%s" change topic from "%s" to "%s" by member "%s"',
room,
oldTopic,
topic,
changer
);
await room.say(
`room-topic - change topic from "${oldTopic}" to "${topic}" by member "${changer.name()}"`
);
} catch (e) {
log.error("Bot", "room-topic event exception: %s", e.stack);
}
})

Global Event: message

This event handles entry and exit in the room. If you send ding the first time, then you will get an invitation but saying ding in a room leads to exiting the room. Similarly, if you say dong it will quit the room.

.on("message", async function (msg) {
if (msg.age() > 3 * 60) {
log.info(
"Bot",
'on(message) skip age("%d") > 3 * 60 seconds: "%s"',
msg.age(),
msg
);
return;
}

const room = msg.room();
const from = msg.from();
const text = msg.text();

if (!from) {
return;
}

console.log(
(room ? "[" + (await room.topic()) + "]" : "") +
"<" +
from.name() +
">" +
":" +
msg
);

if (msg.self()) {
return; // skip self
}

/**
* `dong` will be the magic(toggle) word:
* bot will quit current room by herself.
*/
if (/^dong$/i.test(text)) {
if (room) {
await room.say(
"You said dong in the room, I will quit by myself!",
from
);
await room.quit();
} else {
await from.say(
'Nothing to do. If you say "dong" in a room, I will quit from the room.'
);
}
return;
}

/**
* `ding` will be the magic(toggle) word:
* 1. say ding first time, will got a room invitation
* 2. say ding in room, will be removed out
*/
if (/^ding$/i.test(text)) {
/**
* in-room message
*/
if (room) {
if (/^ding/i.test(await room.topic())) {
/**
* move contact out of room
*/
await getOutRoom(from, room);
}

/**
* peer to peer message
*/
} else {
/**
* find room name start with "ding"
*/
try {
const dingRoom = await this.Room.find({ topic: /^ding/i });
if (dingRoom) {
/**
* room found
*/
log.info(
"Bot",
'onMessage: got dingRoom: "%s"',
await dingRoom.topic()
);

if (await dingRoom.has(from)) {
/**
* speaker is already in room
*/
const topic = await dingRoom.topic();
log.info("Bot", "onMessage: sender has already in dingRoom");
await dingRoom.say(
`I found you have joined in room "${topic}"!`,
from
);
await from.say(
`no need to ding again, because you are already in room: "${topic}"`
);
// sendMessage({
// content: 'no need to ding again, because you are already in ding room'
// , to: sender
// })
} else {
/**
* put speaker into room
*/
log.info(
"Bot",
'onMessage: add sender("%s") to dingRoom("%s")',
from.name(),
dingRoom.topic()
);
await from.say("ok, I will put you in ding room!");
await putInRoom(from, dingRoom);
}
} else {
/**
* room not found
*/
log.info("Bot", "onMessage: dingRoom not found, try to create one");
/**
* create the ding room
*/
const newRoom = await createDingRoom(from);
console.log("createDingRoom id:", newRoom.id);
/**
* listen events from ding room
*/
await manageDingRoom();
}
} catch (e) {
log.error(e);
}
}
}
})

Now, finally for starting the bot

bot.start()
.catch((e) => console.error(e));

4. Running the bot

To run the bot, you have to export/set environment variables with the type of puppet you want to use.

export WECHATY_LOG=verbose
export WECHATY_PUPPET=wechaty-puppet-wechat

Run the bot using the following command.

npx ts-node room-bot.js

Scan the generated QR code with Wechat app and you are ready to play with the bot.

References