This is a “How to” post on hosting Webex Teams bots.

In our team, we are developing variety of bots for different functions/departments. The easiest way for our “customers” to host those bots, is to use our teams servers. However, it becomes complicate to manages all those bots on the same server, with different dependencies and packages (also we are guilty of not using virtual env properly).

So, we’ve decided to try out Docker for hosting bots, with the further idea to transfer them to other servers.

In this post we will explore a procedure and caveats for a simple echo bot, just to show the methodology.

Let’s get started then.

Environment

We are using a AWS EC2 AMI Linux instance (t2.small).

The Docker was installed according to this doc Docker Basics for Amazon ECS.

The bot’s identity was created from Webex Teams Developer Portal. A very detailed step-by-stem on how to create it could be found at Automating Webex Teams (Python) tutorial.

Docker configuration

After reading this helpful post about Building Minimal Docker Containers for Python Applications we’ve decided to use alpine image.

Create an empty directory where files for the container will be located:

mkdir webex-bot
cd webex-bot

For our simple bot to function we need just to non-standard packages, that are defined in requirements.txt:

Flask>=1.0.2
requests>=2.18.4

The Docker file is pretty simple as well:

FROM python:3.6-alpine
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt

In order to build the image:

sudo docker build -t "dmkravch:Dockerfile" .

And to start the container:

sudo docker run -i --net=host -p 8090:8090 -t dmkravch:Dockerfile /bin/sh

Note1: alpine supports only /bin/sh shell. Note2: we are using port 8090 on both, host and container, to receive connections. Note3: Important key –net=host enables the Docker to use the host’s network stack for the container.

In the same directory we are storing the actual file for the bot, echo.py:

import json, requests
from flask import Flask, request

app = Flask(__name__)


# functions:

def post_request(url, dane, headers):
    created = requests.post(url, data=dane, headers=headers)
    x = created.content.decode("utf-8")
    x = json.loads(x)
    id = (x["id"])
    return id


def create_message(roomId, message):
    messageUrl = "https://api.ciscospark.com/v1/messages"
    dane = {
        "roomId": roomId,
        "text": message
    }
    dane = json.dumps(dane)
    messageId = post_request(messageUrl, dane, headers)
    return messageId


def get_message(msgid, accesstoken):
    url = "https://api.ciscospark.com/v1/messages/" + msgid
    headers = {
        'Authorization': accesstoken
    }
    obtained = requests.get(url, headers=headers)
    # print(obtained)
    dict = json.loads(obtained.text)
    dict['statuscode'] = str(obtained.status_code)
    return dict


# ------------------------------------------------------------
at = "BOT ACCESS TOKEN from developer.webex.com"
accesstoken = "Bearer " + at
webhook_id = "WEBHOOK ID created on developer.webex.com"

# This is the ID, that you can see for example in message as creator. It is not the same as botID, that you can see when creating bot:
bot_id = "BOT ID to avoid looping of messages"

headers = {
    'Authorization': accesstoken,
    'Content-Type': "application/json; charset=utf-8",
    'Cache-Control': "no-cache"
}


# ------------------------------------------------------------

@app.route("/", methods=['POST'])
def handle_message():
    data = request.get_json()
    personid = data["data"]["personId"]

    if personid == bot_id:
        return 'ok'
    else:
        if data["id"] != webhook_id:
            return 'ok'
        else:
            msgid = data["data"]["id"]
            roomId = data["data"]["roomId"]
            txt = get_message(msgid, accesstoken)
            create_message(roomId, txt['text'])

            return "ok"

app.run(host='IP or DNS NAME OF THE EC2 INSTANCE',port=8090)

Webex Teams bot configuration

Very important to create a webex teams webhook to that EC2 instance and specify the correct port, in our case it’s http/8090. If you want to use https, please, take care of the certificates, which are describe in the Running Your Flask Application Over HTTPS blog post.

An example of how to create a webhook pointed to the EC2 instance:

alt text

Running the both

As you remember, we’ve started a Docker container with:

sudo docker run -i --net=host -p 8090:8090 -t dmkravch:Dockerfile /bin/sh

which will automatically put us inside of the Docker container. From there we could simply issue a command to run the bot, which will start listening on the port 8090:

python echo.py
WARNING: Published ports are discarded when using host network mode
/app # python echo.py
 * Serving Flask app "echo" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://XXX.XX.XX.XXX:8090/ (Press CTRL+C to quit)

And when a new request from the bot is received, it should be shown in the console:

...
...
 * Running on http://XXX.XX.XX.XXX:8090/ (Press CTRL+C to quit)
18.221.216.175 - - [09/Jun/2018 13:38:43] "POST / HTTP/1.1" 200 -

That’s it! Now you could transfer that container to any server (given that the webhook destination is changed) and start the bot.

Troubleshooting

Here you could find some useful commands to troubleshoot, in case something goes wrong.

To check available images:

sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
dmkravch            Dockerfile          f2dc6382956f        19 hours ago        104MB

To check if the needed port is opened:

sudo netstat -tulpn
tcp        0      0 XXX.XX.XX.XXX:8090          0.0.0.0:*                   LISTEN      20613/python

To check running containers:

sudo docker ps
CONTAINER ID        IMAGE                 COMMAND             CREATED             STATUS              PORTS               NAMES
51e84984a404        dmkravch:Dockerfile   "/bin/sh"           8 minutes ago       Up 8 minutes                            angry_tesla

If you want to check if the container and flask could reply, use the curl command:

curl http://XXX.XX.XX.XXX:8090/
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>

In the case above, the error is obvious, as the method GET is not allowed, only POST.

That’s a simple “proof-of-concept” example, which could be further enhanced and scripted.

I hope it will be useful for someone and could save some time.

Thank you.