Customer Service System for WeChat Subscription Accounts

Last updated: 2021-01-12 18:12:42

    This document describes how to use Node.js to develop a simple and common customer service agent demo as an example to introduce the basic flow for integrating Tencent Cloud IM on WeChat subscription accounts.

    Note:

    The demo is for reference only. Many aspects, such as server load balancing, concurrent API control, and persistent information storage, need to be further improved before launch. Such improvements are not covered in this document. Developers can make improvements based on their actual needs.

    Basic Flow and Effects of the Demo

    The basic flow of the demo described in this document is as follows:

    1. A customer inquires "When will new children's clothing be available?" through a clothing and apparel e-commerce subscription account.
    2. The customer's inquiry is transferred to the customer service agent of the clothing and apparel e-commerce account through Tencent Cloud IM.
    3. The customer service agent replies "New clothing will be available in May. Please follow our updates." The message is pushed to the customer through Tencent Cloud IM and WeChat.

    The following figure shows the effect of the demo at the customer side.

    The following figure shows the effect of the demo at the customer service agent side.

    The following figure shows the entire flowchart of the demo:

    Precautions

    • If the message transfer link is long, it may take longer to send and receive messages.
    • Subscription accounts registered by individuals cannot use the [customer service message] API of the WeChat Official Accounts Platform to push messages to subscribers.

    Prerequisites

    References

    Directions

    Step 1: create a development project and install dependencies

    npm init -y
    
    // Express framework
    npm i express@latest --save 
    
    // Encryption module
    npm i crypto@latest --save
    
    // xml parser
    npm i xml2js@latest --save
    
    // Initiate an HTTP request.
    npm i axios@latest --save
    
    // Calculate the UserSig.
    npm i tls-sig-api-v2@latest --save

    Step 2: enter the IM app information and calculate the UserSig

    // ------------ IM ------------
    const IMAxios = axios.create({
      timeout: 10000,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
      },
    });
    
    // The user ID mapping table imported into the IM account system is not persistently stored and used only for quick demo retrieval. For production environments, use another technical solution.
    const importedAccountMap = new Map();
    // IM app and app admin information can be obtained from the IM console.
    const SDKAppID = 0; // Enter the SDKAppID of the IM app.
    const secrectKey = ''; // Enter the secret key of the IM app.
    const AppAdmin = 'user0'; // Set user0 as the app admin account.
    const kfAccount1 = 'user1'; // Set user1 as a customer service agent account.
    // Calculate the UserSig, which will be used when calling RESTful APIs. For more information, see Github.
    const api = new TLSSigAPIv2.Api(SDKAppID, secrectKey);
    const userSig = api.genSig(AppAdmin, 86400*180);
    console.log('userSig:', userSig);

    Step 3: configure the URL and token

    Note:

    This document was written with reference to the Development Guide for the WeChat Official Accounts Platform. In case of any change, the latest information can be found in the Access Guide.

    1. Log in to the management backend of the subscription account.
    2. Select Basic Configuration and click the protocol to become a developer.
    3. Click Modify Configuration and enter relevant information.
      • URL: the server address. This is the API URL used for receiving WeChat messages and events. It is a required parameter.
      • Token: user-defined. This is used to generate a signature. The token will be compared with the token contained in the API URL for security verification. It is a required parameter.
      • EncodingAESKey: this is the manually entered or randomly generated key used for message body encryption. It is an optional parameter.

    Step 4: enable the web service listening port and correctly respond to the token verification sent by WeChat

    const express = require('express'); // Express framework
    const crypto =  require('crypto'); // Encryption module
    const util = require('util');
    const xml2js = require('xml2js'); // xml parser
    const axios = require('axios'); // Initiate an HTTP request.
    const TLSSigAPIv2 = require('tls-sig-api-v2'); // Calculate the UserSig.
    
    // ------------ Web service ------------
    var app = express();
    // You need to set the token on the **Subscription Account Management Backend** > **Basic Configuration** page.
    
    // Process all GET requests that enter port 80.
    app.get('/', function(req, res) {
      // ------------ Accessing the WeChat Official Accounts Platform ------------
      // For more information, see WeChat official documentation.
      // Obtain the parameters `signature`, `timestamp`, `nonce`, and `echostr` from the GET request of the WeChat server.
      var signature = req.query.signature; // WeChat encrypted signature
      var timestamp = req.query.timestamp; // Timestamp
      var nonce = req.query.nonce; // Radom number
      var echostr = req.query.echostr; // Random character string
    
      // Sort the `token`, `timestamp`, and `nonce` parameters in lexicographical order.
      var array = [myToken, timestamp, nonce];
      array.sort();
    
      // Concatenate the character strings of the three parameters into one string for SHA1 encryption.
      var tempStr = array.join('');
      const hashCode = crypto.createHash('sha1'); // Create an encryption type.
      var resultCode = hashCode.update(tempStr,'utf8').digest('hex'); // Encrypt the character string passed in.
    
      // After obtaining the encrypted character string, the developer can compare it with the signature, and flag the request to indicate that the request comes from WeChat.
      if (resultCode === signature) {
        res.send(echostr);
      } else {
        res.send('404 not found');
      }
    });
    
    // Listen on port 80.
    app.listen(80);

    Step 5: implement the service logic on the developer's server

    • When receiving a followed event pushed by WeChat, call the Importing a Single Account or Batch Importing Accounts API to import accounts into the account system.
    • When receiving a followed event pushed by WeChat, passively reply to the message.
    • When receiving an unfollowed event pushed by WeChat, call the Deleting Accounts API to delete the account from the account system.
    • When receiving a common message pushed by WeChat, call the Sending a One-to-One Message API to send a one-to-one message to the customer service agent account.
    const genRandom = function() {
      return  Math.floor(Math.random() * 10000000);
    }
    
    // Generate the xml for the WeChat text reply.
    const genWxTextReplyXML = function(to, from, content) {
      let xmlContent = '<xml><ToUserName><![CDATA[' + to + ']]></ToUserName>'
      xmlContent += '<FromUserName><![CDATA[' + from + ']]></FromUserName>'
      xmlContent += '<CreateTime>' + new Date().getTime() + '</CreateTime>'
      xmlContent += '<MsgType><![CDATA[text]]></MsgType>'
      xmlContent += '<Content><![CDATA[' + content + ']]></Content></xml>';
    
      return xmlContent;
    }
    
    /**
     * Import users into the IM account system.
     * @param {String} userID - ID of the user to be imported
     */
    const importAccount = function(userID) {
      console.log('importAccount:', userID);
      return new Promise(function(resolve, reject) {
        var url = util.format('https://console.tim.qq.com/v4/im_open_login_svc/account_import?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json',
          SDKAppID, AppAdmin, userSig, genRandom());
        console.log('importAccount url:', url);
        IMAxios({
          url: url,
          data: {
            "Identifier": userID
          },
          method: 'POST'
        }).then((res) => {
          if (res.data.ErrorCode === 0) {
            console.log('importAccount ok.', res.data);
            resolve();
          } else {
            reject(res.data);
          }
        }).catch((error) => {
          console.log('importAccount failed.', error);
          reject(error);
        })
      });
    }
    
    /**
     * Delete users from the IM account system.
     * @param {String} userID - ID of the user to be deleted
     */
    const deleteAccount = function(userID) {
      console.log('deleteAccount', userID);
      return new Promise(function(resolve, reject) {
        var url = util.format('https://console.tim.qq.com/v4/im_open_login_svc/account_delete?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json',
          SDKAppID, AppAdmin, userSig, genRandom());
        console.log('deleteAccount url:', url);
        IMAxios({
          url: url,
          data: {
            "DeleteItem": [
              {
                "UserID": userID,
              },
            ]
          },
          method: 'POST'
        }).then((res) => {
          if (res.data.ErrorCode === 0) {
            console.log('deleteAccount ok.', res.data);
            resolve();
          } else {
            reject(res.data);
          }
        }).catch((error) => {
          console.log('deleteAccount failed.', error);
          reject(error);
        })
      });
    }
    
    /**
     * Sending a one-to-one message
     */
    const sendC2CTextMessage = function(userID, content) {
      console.log('sendC2CTextMessage:', userID, content);
      return new Promise(function(resolve, reject) {
        var url = util.format('https://console.tim.qq.com/v4/openim/sendmsg?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json',
          SDKAppID, AppAdmin, userSig, genRandom());
        console.log('sendC2CTextMessage url:', url);
        IMAxios({
          url: url,
          data: {
            "SyncOtherMachine": 2, // The message will not be synchronized to the sender. If you want it to be synchronized to `From_Account`, set `SyncOtherMachine` to 1.
            "To_Account": userID,
            "MsgLifeTime":60, // Messages are retained for 60 seconds.
            "MsgRandom": 1287657,
            "MsgTimeStamp": Math.floor(Date.now() / 1000), // The unit is second and the value must be an integer.
            "MsgBody": [
              {
                "MsgType": "TIMTextElem",
                "MsgContent": {
                  "Text": content
                }
              }
            ]
          },
          method: 'POST'
        }).then((res) => {
          if (res.data.ErrorCode === 0) {
            console.log('sendC2CTextMessage ok.', res.data);
            resolve();
          } else {
            reject(res.data);
          }
        }).catch((error) => {
          console.log('sendC2CTextMessage failed.', error);
          reject(error);
        });
      });
    }
    
    // Process the POST requests from WeChat.
    app.post('/', function(req, res) {
      var buffer = [];
      // Listen to data events, which are used to receive data.
      req.on('data', function(data) {
        buffer.push(data);
      });
      // Listen to end events, which are used to process the received data.
      req.on('end', function() {
        const tmpStr = Buffer.concat(buffer).toString('utf-8');
        xml2js.parseString(tmpStr, { explicitArray: false }, function(err, result) {
          if (err) {
            console.log(err);
            res.send("success");
          } else {
            if (!result) {
              res.send("success");
              return;
            }
            console.log('wx post data:', result.xml);
            var wxXMLData = result.xml;
            var toUser = wxXMLData.ToUserName; // Recipient’s WeChat
            var fromUser = wxXMLData.FromUserName;// Sender’s WeChat
            if (wxXMLData.Event) {  // Process the event type.
              switch (wxXMLData.Event) {
                case "subscribe": // Follow the subscription account.
                  res.send(genWxTextReplyXML(fromUser, toUser, 'Thank you for following us! XX will always provide you with the best service!'));
                  importAccount(fromUser).then(() => {
                    // Record the ID of the imported user.
                    importedAccountMap.set(fromUser, 1);
                  });
                  break;
                case "unsubscribe": // Unfollow the account.
                  deleteAccount(fromUser).then(() => {
                    importedAccountMap.delete(fromUser);
                  });
                  res.send("success");
                  break;
              }
            } else { // Process the message type.
              switch (wxXMLData.MsgType) {
                case "text":
                  // Process text messages.
                  sendC2CTextMessage(kfAccount1, 'Inquiry from the WeChat subscription account:' + wxXMLData.Content).then(() => {
                    console.log('C2C message sent successfully');
                  }).catch((error) => {
                    console.log('C2C message failed to be sent');
                  });
                  break;
                case "image":
                  // Process image messages.
                  break;
                case "voice":
                  // Process voice messages.
                  break;
                case "video":
                  // Process video messages.
                  break;
                case "shortvideo":
                  // Process short video messages.
                  break;
                case "location":
                  // Process send location messages.
                  break;
                case "link":
                  // Process link messages.
                  break;
                default:
                  break;  
              }
              res.send(genWxTextReplyXML(fromUser, toUser, 'Transferring to a customer service agent, please wait'));
            }
          }
        })
      });
    });

    Step 6: register and process IM third-party callbacks

    // Process POST requests from IM third-party callbacks.
    app.post('/imcallback', function(req, res) {
      var buffer = [];
      // Listen to data events, which are used to receive data.
      req.on('data', function(data) {
        buffer.push(data);
      });
      // Listen to end events, which are used to process the received data.
      req.on('end', function() {
        const tmpStr = Buffer.concat(buffer).toString('utf-8');
        console.log('imcallback', tmpStr);
        const imData = JSON.parse(tmpStr);
        // Push the message sent by kfAccount1 to the customer.
        if (imData.From_Account === kfAccount1) {
          // Package the message and push it through the **customer service message** API of WeChat to the specified user.
          // Note: subscription accounts registered by individuals are not allowed to use this API. For more information, see Customer Service Messages.
        }
    
        res.send({
          "ActionStatus": "OK",
          "ErrorInfo": "",
          "ErrorCode": 0 // 0: not muted. 1: muted.
        });
      });
    });