Smart Serverless, Easy to set up WeChat Public Number intelligent background service

Posted by uwannadonkey on Fri, 22 May 2020 05:11:37 +0200

Generally speaking, to add more functions to the WeChat public number, you need a server to set up the public number background service.So is there a simpler way to implement such a public number background under the Serverless architecture?Let's try it?

Preliminary Setup

Serverless native development

First of all, of course, we must have a WeChat public number!

Next, we'll apply for a fixed IP for our Function Computing Service:

After clicking on the whitelist, we can fill out the form to complete the application for fixed public network export IP.

Next, you'll develop the code.

  1. Bind the function to the public number background, refer to the documentation: https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html We can first complete a basic authentication function in the function as documented:
def checkSignature(param):
    '''
    //Document address:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
    :param param:
    :return:
    '''
    signature = param['signature']
    timestamp = param['timestamp']
    nonce = param["nonce"]
    tmparr = [wxtoken, timestamp, nonce]
    tmparr.sort()
    tmpstr = ''.join(tmparr)
    tmpstr = hashlib.sha1(tmpstr.encode("utf-8")).hexdigest()
    return tmpstr == signature

Define a basic response method:

def response(body, status=200):
    return {
        "isBase64Encoded": False,
        "statusCode": status,
        "headers": {"Content-Type": "text/html"},
        "body": body
    }

Then at the function entry:

def main_handler(event, context):    
    if 'echostr' in event['queryString']:  # Check on Access
        return response(event['queryString']['echostr'] if checkSignature(event['queryString']) else False)

Configure our Yaml:

# serverless.yml
Weixin_GoServerless:
  component: "@serverless/tencent-scf"
  inputs:
    name: Weixin_GoServerless
    codeUri: ./Admin
    handler: index.main_handler
    runtime: Python3.6
    region: ap-shanghai
    description: WeChat Public Number Background Server Configuration
    memorySize: 128
    timeout: 20
    environment:
      variables:
        wxtoken: Customize a string
        appid: Don't write for now
        secret: Don't write for now
    events:
      - apigw:
          name: Weixin_GoServerless
          parameters:
            protocols:
              - https
            environment: release
            endpoints:
              - path: /
                method: ANY
                function:
                  isIntegratedResponse: TRUE

Execute code to complete deployment:

Next, in the Public Number background, select the basic configuration:

Choose to modify the configuration:

Note here:

  • URL, write the address we just returned to us after deployment, and add one/

  • Token, write wxtoken in our Yaml, two places to keep the same string

  • EncodingAESKey, click Random Generation

  • Message encryption method can choose plain text

After that, we can click Submit:

Seeing the submission succeed means that we have completed the binding for the first step.Next, let's go to the background of the function:

Open the fixed exit IP and copy the IP address when you see the IP address:

Click View->Modify and copy and paste the IP address into it to save.
We also look at the developer ID and password:

And copy and paste these two into our environment variables:

So far, we have completed a public number background service binding.

For the convenience of later operations, first get the global variables:

wxtoken = os.environ.get('wxtoken')
appid = os.environ.get('appid')
secret = os.environ.get('secret')
  1. Next, edit each module (only some simple and basic modules are provided in this article, and more functions can be achieved by referring to the WeChat public document).
  • Get the AccessToken module:
def getAccessToken():
    '''
    //Document address:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
    //Normal return: {"access_token":"ACCESS_TOKEN","expires_in":7200}
    //Exception returned: {"errcode":40013,"errmsg":"invalid appid"}
    :return:
    '''
    url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" % (appid, secret)
    accessToken = json.loads(urllib.request.urlopen(url).read().decode("utf-8"))
    print(accessToken)
    return None if "errcode" in accessToken else accessToken["access_token"]
  • Create a custom menu module:
def setMenu(menu):
    '''
    //Document address:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
    //Correct return: {"errcode":0,"errmsg":"ok"}
    //Exception returned: {"errcode":40018,"errmsg":"invalid button name size"}
    :return:
    '''
    accessToken = getAccessToken()
    if not accessToken:
        return "Get Access Token Error"

    url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s" % accessToken
    postData = urllib.parse.urlencode(menu).encode("utf-8")
    requestAttr = urllib.request.Request(url=url, data=postData)
    responseAttr = urllib.request.urlopen(requestAttr)
    responseData = json.loads(responseAttr.read())
    return responseData['errmsg'] if "errcode" in responseData else "success"
  • Common message reply module:
def textXML(body, event):
    '''
    :param body: {"msg": "test"}
        msg: Required, message content for reply (line break: in content WeChat client supports line-breaking display when line-breaking is enabled in
    :param event:
    :return:
    '''
    return """<xml><ToUserName><![CDATA[{toUser}]]></ToUserName>
              <FromUserName><![CDATA[{fromUser}]]></FromUserName>
              <CreateTime>{time}</CreateTime>
              <MsgType><![CDATA[text]]></MsgType>
              <Content><![CDATA[{msg}]]></Content></xml>""".format(toUser=event["FromUserName"],
                                                                   fromUser=event["ToUserName"],
                                                                   time=int(time.time()),
                                                                   msg=body["msg"])


def pictureXML(body, event):
    '''
    :param body:  {"media_id": 123}
        media_id: Required, obtained by uploading multimedia files through the interface in material management id. 
    :param event:
    :return:
    '''
    return """<xml><ToUserName><![CDATA[{toUser}]]></ToUserName>
              <FromUserName><![CDATA[{fromUser}]]]></FromUserName>
              <CreateTime>{time}</CreateTime>
              <MsgType><![CDATA[image]]></MsgType>
              <Image>
                <MediaId><![CDATA[{media_id}]]></MediaId>
              </Image></xml>""".format(toUser=event["FromUserName"],
                                       fromUser=event["ToUserName"],
                                       time=int(time.time()),
                                       media_id=body["media_id"])


def voiceXML(body, event):
    '''
    :param body: {"media_id": 123}
        media_id: Required, obtained by uploading multimedia files through the interface in material management id
    :param event:
    :return:
    '''
    return """<xml><ToUserName><![CDATA[{toUser}]]></ToUserName>
              <FromUserName><![CDATA[{fromUser}]]></FromUserName>
              <CreateTime>{time}</CreateTime>
              <MsgType><![CDATA[voice]]></MsgType>
              <Voice>
                <MediaId><![CDATA[{media_id}]]></MediaId>
              </Voice></xml>""".format(toUser=event["FromUserName"],
                                       fromUser=event["ToUserName"],
                                       time=int(time.time()),
                                       media_id=body["media_id"])


def videoXML(body, event):
    '''
    :param body: {"media_id": 123, "title": "test", "description": "test}
        media_id: Required, obtained by uploading multimedia files through the interface in material management id
        title:: Optional, title of video message
        description: Optional, description of video message
    :param event:
    :return:
    '''
    return """<xml><ToUserName><![CDATA[{toUser}]]></ToUserName>
              <FromUserName><![CDATA[{fromUser}]]></FromUserName>
              <CreateTime>{time}</CreateTime>
              <MsgType><![CDATA[video]]></MsgType>
              <Video>
                <MediaId><![CDATA[{media_id}]]></MediaId>
                <Title><![CDATA[{title}]]></Title>
                <Description><![CDATA[{description}]]></Description>
              </Video></xml>""".format(toUser=event["FromUserName"],
                                       fromUser=event["ToUserName"],
                                       time=int(time.time()),
                                       media_id=body["media_id"],
                                       title=body.get('title', ''),
                                       description=body.get('description', ''))


def musicXML(body, event):
    '''
    :param body:  {"media_id": 123, "title": "test", "description": "test}
        media_id: Required, Thumbnail Media id,By uploading multimedia files through the interface of material management, id
        title: Optional, Music Title
        description: Optional, Music Description
        url: Optional, Music Link
        hq_url: Optional, high quality music links, WIFI Environment prefers this link to play music
    :param event:
    :return:
    '''
    return """<xml><ToUserName><![CDATA[{toUser}]]></ToUserName>
              <FromUserName><![CDATA[{fromUser}]]></FromUserName>
              <CreateTime>{time}</CreateTime>
              <MsgType><![CDATA[music]]></MsgType>
              <Music>
                <Title><![CDATA[{title}]]></Title>
                <Description><![CDATA[{description}]]></Description>
                <MusicUrl><![CDATA[{url}]]></MusicUrl>
                <HQMusicUrl><![CDATA[{hq_url}]]></HQMusicUrl>
                <ThumbMediaId><![CDATA[{media_id}]]></ThumbMediaId>
              </Music></xml>""".format(toUser=event["FromUserName"],
                                       fromUser=event["ToUserName"],
                                       time=int(time.time()),
                                       media_id=body["media_id"],
                                       title=body.get('title', ''),
                                       url=body.get('url', ''),
                                       hq_url=body.get('hq_url', ''),
                                       description=body.get('description', ''))


def articlesXML(body, event):
    '''
    :param body: One list [{"title":"test", "description": "test", "picUrl": "test", "url": "test"}]
        title: Required, Text Message Title
        description: Required, graphical message description
        picUrl: Required, Picture Link, Support JPG,PNG Format, good results as Figure 360*200,Figure 200*200
        url: Required, click Text Message to jump link
    :param event:
    :return:
    '''
    if len(body["articles"]) > 8:  # Only eight returns are allowed
        body["articles"] = body["articles"][0:8]
    tempArticle = """<item>
      <Title><![CDATA[{title}]]></Title>
      <Description><![CDATA[{description}]]></Description>
      <PicUrl><![CDATA[{picurl}]]></PicUrl>
      <Url><![CDATA[{url}]]></Url>
    </item>"""
    return """<xml><ToUserName><![CDATA[{toUser}]]></ToUserName>
              <FromUserName><![CDATA[{fromUser}]]></FromUserName>
              <CreateTime>{time}</CreateTime>
              <MsgType><![CDATA[news]]></MsgType>
              <ArticleCount>{count}</ArticleCount>
              <Articles>
                {articles}
              </Articles></xml>""".format(toUser=event["FromUserName"],
                                          fromUser=event["ToUserName"],
                                          time=int(time.time()),
                                          count=len(body["articles"]),
                                          articles="".join([tempArticle.format(
                                              title=eveArticle['title'],
                                              description=eveArticle['description'],
                                              picurl=eveArticle['picurl'],
                                              url=eveArticle['url']
                                          ) for eveArticle in body["articles"]]))
  • For main_handler is modified to:

    • Identify Binding Function

    • Identify basic information

    • Identify special extra requests (such as triggering updates to custom menus via a url)

Overall code:

def main_handler(event, context):
    print('event: ', event)

    if event["path"] == '/setMenu':  # Set menu interface
        menu = {
            "button": [
                {
                    "type": "view",
                    "name": "Brilliant Articles",
                    "url": "https://mp.weixin.qq.com/mp/homepage?__biz=Mzg2NzE4MDExNw==&hid=2&sn=168bd0620ee79cd35d0a80cddb9f2487"
                },
                {
                    "type": "view",
                    "name": "Open Source Project",
                    "url": "https://mp.weixin.qq.com/mp/homepage?__biz=Mzg2NzE4MDExNw==&hid=1&sn=69444401c5ed9746aeb1384fa6a9a201"
                },
                {
                    "type": "miniprogram",
                    "name": "Online programming",
                    "appid": "wx453cb539f9f963b2",
                    "pagepath": "/page/index"
                }]
        }
        return response(setMenu(menu))

    if 'echostr' in event['queryString']:  # Check on Access
        return response(event['queryString']['echostr'] if checkSignature(event['queryString']) else False)
    else:  # User Messages/Events
        event = getEvent(event)
        if event["MsgType"] == "text":
            # Text message
            return response(body=textXML({"msg": "This is a text message"}, event))
        elif event["MsgType"] == "image":
            # Picture message
            return response(body=textXML({"msg": "This is a picture message"}, event))
        elif event["MsgType"] == "voice":
            # Voice message
            pass
        elif event["MsgType"] == "video":
            # Video message
            pass
        elif event["MsgType"] == "shortvideo":
            # Small video message
            pass
        elif event["MsgType"] == "location":
            # Geographic location messages
            pass
        elif event["MsgType"] == "link":
            # Link Message
            pass
        elif event["MsgType"] == "event":
            # Event message
            if event["Event"] == "subscribe":
                # Subscription Events
                if event.get('EventKey', None):
                    # Event Push After Attention (QR code with parameters) when the user is not concerned
                    pass
                else:
                    # General concern
                    pass
            elif event["Event"] == "unsubscribe":
                # Unsubscribe Event
                pass
            elif event["Event"] == "SCAN":
                # Event push when the user is already interested (two-dimensional code with parameters)
                pass
            elif event["Event"] == "LOCATION":
                # Report geographic location events
                pass
            elif event["Event"] == "CLICK":
                # Event push when clicking menu pull to cancel message
                pass
            elif event["Event"] == "VIEW":
                # Event Push on Menu Jump Link
                pass

You can see in the code above:

if event["MsgType"] == "text":
    # Text message
    return response(body=textXML({"msg": "This is a text message"}, event))
elif event["MsgType"] == "image":
    # Picture message
    return response(body=textXML({"msg": "This is a picture message"}, event))

That is, when the user sends a text message, we reply to the user with a text message: this is a text message.When the user sends a picture, we return the user a picture message and test the connectivity of our background with these two functions:

You can see that the system has returned normally.

Someone asked, what is the meaning of such a simple Demo?You can tell us that we can lightly use a function to implement the back-end service of WeChat Public Number; these are basic capabilities on which we can add innovation, such as:

  1. The user is sending a picture message. We can tell the user what this picture contains through some Mapping API s?

  2. Users are sending text messages. We can set some help/retrieve information to compare and turn on chat if it is not found (this involves natural language processing in AI, such as text similarity detection)

  3. If the user sends a voice, we can also turn it into text, generate a conversation message, and then turn it back to the user.

  4. If the user sends the geographic location information, we can return the street view information in the latitude and longitude of the user, or the information about the surrounding living services, etc.

  5. Leave it to your imagination!

Using the Werobot framework

The above method is docked using Serverless's native development method.In addition, we can choose some existing frameworks, such as werobot s.

WeRoBot is a WeChat public number development framework.Quickly deploy the framework through the tencent-werobot s component in Serverless Component:

Weixin_Werobot:
  component: "@serverless/tencent-werobot"
  inputs:
    functionName: Weixin_Werobot
    code: ./test
    werobotProjectName: app
    werobotAttrName: robot
    functionConf:
      timeout: 10
      memorySize: 256
      environment:
        variables:
          wxtoken: Your token
      apigatewayConf:
        protocols:
          - http
        environment: release

Then create a new code:

import os
import werobot

robot = werobot.WeRoBot(token=os.environ.get('wxtoken'))

robot.config['SESSION_STORAGE'] = False
robot.config["APP_ID"] = os.environ.get('appid')
robot.config["APP_SECRET"] = os.environ.get('secret')

# @Robot.handlerProcess all messages
@robot.handler
def hello(message):
    return 'Hello World!'

if __name__ == "__main__":
    # Let the server listen at 0.0.0:80
    robot.config['HOST'] = '0.0.0.0'
    robot.config['PORT'] = 80
    robot.run()


And install werobot s-related dependencies locally, then deploy:

Copy the address below to the public number background and open the call.

Reference Git: https://github.com/serverless-tencent/tencent-werobot
It is important to note that we must turn off Session or change Session to a cloud database and not use local files, for example, to turn off Session configuration:

robot.config['SESSION_STORAGE'] = False

Text Similarity for Graphic and Text Retrieval

Sometimes the user doesn't know what articles we've posted or the specific content of each article. He may just need simple keywords to see if this public number has something he wants.

For example, he searches: How do I upload files?Or search: How do I develop Component s?It will be very convenient to quickly push the most relevant historical articles to users by using the text retrieval function.The results are as follows:

Find the target result through a simple problem description, which is the article search function we do.Of course, we can also expand it into a "customer service system", which is later.

Back to the point, we added two new functions to the previous code:

  • Function 1: Indexing function

Main functions: By triggering this function, you can organize the existing public number data, and establish an appropriate index file for storage in COS.


# -*- coding: utf8 -*-
import os
import re
import json
import random
from snownlp import SnowNLP
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client

bucket = os.environ.get('bucket')
secret_id = os.environ.get('secret_id')
secret_key = os.environ.get('secret_key')
region = os.environ.get('region')
client = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key))


def main_handler(event, context):
    response = client.get_object(
        Bucket=bucket,
        Key=event["key"],
    )
    response['Body'].get_stream_to_file('/tmp/output.txt')

    with open('/tmp/output.txt') as f:
        data = json.loads(f.read())

    articlesIndex = []
    articles = {}
    tempContentList = [
        "_", "&nbsp;",
    ]
    for eveItem in data:
        for i in range(0, len(eveItem['content']['news_item'])):
            content = eveItem['content']['news_item'][i]['content']
            content = re.sub(r'<code(.*?)</code>', '_', content)
            content = re.sub(r'<.*?>', '', content)
            for eve in tempContentList:
                content = content.replace(eve, "")
            desc = "%s. %s. %s" % (
                eveItem['content']['news_item'][i]['title'],
                eveItem['content']['news_item'][i]['digest'],
                ". ".join(SnowNLP(content).summary(3))
            )
            tempKey = "".join(random.sample('zyxwvutsrqponmlkjihgfedcba', 5))
            articlesIndex.append(
                {
                    "media_id": tempKey,
                    "description": desc
                }
            )
            articles[tempKey] = eveItem['content']['news_item'][i]

    client.put_object(
        Bucket=bucket,
        Body=json.dumps(articlesIndex).encode("utf-8"),
        Key=event['index_key'],
        EnableMD5=False
    )
    client.put_object(
        Bucket=bucket,
        Body=json.dumps(articles).encode("utf-8"),
        Key=event['key'],
        EnableMD5=False
    )

This part may be more customized.The first is the tempContentList variable, where you can write something that may appear in the public number but is not important, such as a guide at the end of the public number that pays attention to the copy, which generally does not participate in the search, so it is best to replace and remove it when indexing.We then removed the contents of the code tag from the above code, because the code also affects the results.I also removed the html tag.

The original file looks like this:

Processed file (abstract extracted by title+description+SnowNLP):

These files will be stored in COS.

The core of this part is to correctly let our extracted description s describe the content of the article as accurately as possible.In general, the title is the core of the article, but the title may have lost some information.

For example, the article "Use Tencent Cloud Serverless to know the difference between them", but actually describes the difference between Plugin and Component.Although the title knows two things, it lacks a core goal, so add to our description below: What is Serverless Framework Plugin?What is Component?What is the difference between Plugin and Component?To get started with Serverless CLI, these two products must be clearly distinguished. This article will share the differences between them and their corresponding features and functions.

Of course, the content becomes fairly accurate after adding the description, but there may be more accurate description or additional content in the body, so it uses the title + description + summary (the first three sentences extracted by textRank are extractive text).

  • Function 2: Search function

Main function: When the user sends a specified keyword to the micro-signal, the result is obtained through this function.

Think: Both function 1 and function 2 can be integrated in the previous function. Why should these two functions be taken out separately to make a separate function exist?Wouldn't it be good to put them in the same function?

The reason is that - the main function triggers relatively often, and the function itself does not require too much resource allocation (64M is enough), while functions 1 and 2 may consume more resources. If the three functions are combined, the memory size of the function may need to be increased to meet the needs of the three functions.This may consume more resources,

For example, the main function triggers 10 times (64M, 1S each), function 1 triggers 2 times (512 M, 5S each), and function 2 triggers 4 times (384M, 3S each)

If you put the three functions together, the resource consumption is:

If you turn it into three functions to execute, the resource consumption is:

The former consumes 13308 in total and the latter 10432 in total.The more calls you make, the larger the proportion of calls to the main function, so the more resources you save.Therefore, it is recommended that modules with large resource consumption gaps be deployed in different functions.

import os
import json
import jieba
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
from collections import defaultdict
from gensim import corpora, models, similarities

bucket = os.environ.get('bucket')
secret_id = os.environ.get('secret_id')
secret_key = os.environ.get('secret_key')
region = os.environ.get('region')
client = CosS3Client(CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key))


def main_handler(event, context):
    response = client.get_object(
        Bucket=bucket,
        Key=event["key"],
    )
    response['Body'].get_stream_to_file('/tmp/output.txt')

    with open('/tmp/output.txt') as f:
        data = json.loads(f.read())

    articles = []
    articlesDict = {}
    for eve in data:
        articles.append(eve['description'])
        articlesDict[eve['description']] = eve['media_id']

    sentence = event["sentence"]

    documents = []
    for eve_sentence in articles:
        tempData = " ".join(jieba.cut(eve_sentence))
        documents.append(tempData)
    texts = [[word for word in document.split()] for document in documents]
    frequency = defaultdict(int)
    for text in texts:
        for word in text:
            frequency[word] += 1
    dictionary = corpora.Dictionary(texts)
    new_xs = dictionary.doc2bow(jieba.cut(sentence))
    corpus = [dictionary.doc2bow(text) for text in texts]
    tfidf = models.TfidfModel(corpus)
    featurenum = len(dictionary.token2id.keys())
    sim = similarities.SparseMatrixSimilarity(
        tfidf[corpus],
        num_features=featurenum
    )[tfidf[new_xs]]
    answer_list = [(sim[i], articles[i]) for i in range(1, len(articles))]
    answer_list.sort(key=lambda x: x[0], reverse=True)
    result = []
    print(answer_list)
    for eve in answer_list:
        if eve[0] > 0.10:
            result.append(articlesDict[eve[1]])
    if len(result) >= 8:
        result = result[0:8]
    return {"result": json.dumps(result)}

This part of the code is also simple, primarily by rating each text by its similarity, then sorting it from highest to lowest, giving a threshold (0.1 set here) that outputs data before the threshold.

Also note that the two dependencies referenced here are jieba and gensim, both of which may involve binary files, so packaging under the CentOS system is strongly recommended.

Next comes the call in the main function, which requires a new method:

  1. Get all text messages
def getTheTotalOfAllMaterials():
    '''
    //Document address:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_the_total_of_all_materials.html
    :return:
    '''
    accessToken = getAccessToken()
    if not accessToken:
        return "Get Access Token Error"
    url = "https://api.weixin.qq.com/cgi-bin/material/get_materialcount?access_token=%s" % accessToken
    responseAttr = urllib.request.urlopen(url=url)
    return json.loads(responseAttr.read())


def getMaterialsList(listType, count):
    '''
    //Document address:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
    :return:
    '''
    accessToken = getAccessToken()
    if not accessToken:
        return "Get Access Token Error"

    url = "https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=%s" % accessToken
    materialsList = []
    for i in range(1, int(count / 20) + 2):
        requestAttr = urllib.request.Request(url=url, data=json.dumps({
            "type": listType,
            "offset": 20 * (i - 1),
            "count": 20
        }).encode("utf-8"), headers={
            "Content-Type": "application/json"
        })
        responseAttr = urllib.request.urlopen(requestAttr)
        responseData = json.loads(responseAttr.read().decode("utf-8"))
        materialsList = materialsList + responseData["item"]
    return materialsList

You can call it from the following code:

rticlesList = getMaterialsList("news", getTheTotalOfAllMaterials()['news_count'])
  1. Store graphical messages in COS and make interfunction calls through the function's Invoke interface:
def saveNewsToCos():
    global articlesList
    articlesList = getMaterialsList("news", getTheTotalOfAllMaterials()['news_count'])
    try:
        cosClient.put_object(
            Bucket=bucket,
            Body=json.dumps(articlesList).encode("utf-8"),
            Key=key,
            EnableMD5=False
        )
        req = models.InvokeRequest()
        params = '{"FunctionName":"Weixin_GoServerless_GetIndexFile", "ClientContext":"{\\"key\\": \\"%s\\", \\"index_key\\": \\"%s\\"}"}' % (
            key, indexKey)
        req.from_json_string(params)
        resp = scfClient.Invoke(req)
        resp.to_json_string()
        response = cosClient.get_object(
            Bucket=bucket,
            Key=key,
        )
        response['Body'].get_stream_to_file('/tmp/content.json')
        with open('/tmp/content.json') as f:
            articlesList = json.loads(f.read())
        return True
    except Exception as e:
        print(e)
        return False
  1. Key from search feedback matches the content of the article
def searchNews(sentence):
    req = models.InvokeRequest()
    params = '{"FunctionName":"Weixin_GoServerless_SearchNews", "ClientContext":"{\\"sentence\\": \\"%s\\", \\"key\\": \\"%s\\"}"}' % (
        sentence, indexKey)
    req.from_json_string(params)
    resp = scfClient.Invoke(req)
    print(json.loads(json.loads(resp.to_json_string())['Result']["RetMsg"]))
    media_id = json.loads(json.loads(json.loads(resp.to_json_string())['Result']["RetMsg"])["result"])
    return media_id if media_id else None

Last at main_In handler, add usage logic:

The logic is very simple, that is, to find the corresponding results based on the message sent by the user. After getting the results, judge the number of results, if there is one similar content, return a text, if there are more than one, return text with links.

Another logic is to create an index, triggered directly through an API gateway.Of course, if you are afraid of insecurity or need it, you can add firm parameters with privileges:

Additional optimization:

In the list of interfaces, we can see that the access Token's interface is actually limited in number of times, and each access is valid for two hours.So we're going to make this part of the function persistent.For this little thing, it's not cost effective to have a MySQL, so I decided to use COS:

def getAccessToken():
    '''
    //Document address:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
    //Normal return: {"access_token":"ACCESS_TOKEN","expires_in":7200}
    //Exception returned: {"errcode":40013,"errmsg":"invalid appid"}
    :return:
    '''
    global accessToken

    # The first judgment is to determine if an accessToken already exists locally, taking into account container reuse
    if accessToken:
        if int(time.time()) - int(accessToken["time"]) <= 7000:
            return accessToken["access_token"]

    # If there is no accessToken locally, go to cos to get it
    try:
        response = cosClient.get_object(
            Bucket=bucket,
            Key=accessTokenKey,
        )
        response['Body'].get_stream_to_file('/tmp/token.json')
        with open('/tmp/token.json') as f:
            accessToken = json.loads(f.read())
    except:
        pass

    # This time to see if there is any in cos, and if there is one in cos, to make a second judgment
    if accessToken:
        if int(time.time()) - int(accessToken["time"]) <= 7000:
            return accessToken["access_token"]

    # If the process is not stopped at this point, accessToken is not yet available, it needs to be obtained from the interface and synchronized to cos
    url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" % (appid, secret)
    accessTokenResult = json.loads(urllib.request.urlopen(url).read().decode("utf-8"))
    accessToken = {"time": int(time.time()), "access_token": accessTokenResult["access_token"]}
    print(accessToken)
    response = cosClient.put_object(
        Bucket=bucket,
        Body=json.dumps(accessToken).encode("utf-8"),
        Key=accessTokenKey,
        EnableMD5=False
    )
    return None if "errcode" in accessToken else accessToken["access_token"]

Of course, this code can continue to be optimized, this is just a thought.

In addition to text similarity for text retrieval, we can also use AI capabilities provided by cloud vendors to add robotic capabilities to public numbers, limited to a short paragraph, so interested readers can explore for themselves.

summary

So far, we have completed a simple public number development.Deploy the Public Number Background Service to the Serverless architecture using Serverless's native development ideas (or using public number development frameworks such as Werobot s).Then, through natural language processing technology (specifically text similarity, etc.), a text retrieval function is implemented.

Serverless architecture has great advantages in developing trigger scenarios of event-driven class such as WeChat Public Number. This article is just a small exploration, more functions and applications, capabilities and value, or specific business.We hope that readers will have a better understanding of the Serverless architecture through this article.

Serverless Framework 30-day Trial Plan

We invite you to experience the most convenient way to develop and deploy Serverless.During the trial period, the associated products and services provide free resources and professional technical support to help your business achieve Serverless quickly and easily!

Details are available: Serverless Framework Trial Plan

One More Thing

What can you do in 3 seconds?Have a drink of water, see an email, or - deploy a full Serverless app?

Copy Link to PC Browser Access: https://serverless.cloud.tencent.com/deploy/express

Fast deployment in 3 seconds for the fastest ever experience Serverless HTTP Actual development!

Port:

Welcome to: Serverless Chinese Network , you can Best Practices Experience more development of Serverless apps here!

Recommended reading: Serverless Architecture: From Principles, Design to Project Practice

Topics: JSON xml github Session