348 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			348 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
import argparse
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import textwrap
 | 
						|
import urllib.parse
 | 
						|
import zipfile
 | 
						|
from typing import Any, Callable, Dict, List
 | 
						|
 | 
						|
import requests
 | 
						|
from requests import Response
 | 
						|
 | 
						|
red = "\033[91m"  # type: str
 | 
						|
green = "\033[92m"  # type: str
 | 
						|
end_format = "\033[0m"  # type: str
 | 
						|
bold = "\033[1m"  # type: str
 | 
						|
 | 
						|
bots_dir = ".bots"  # type: str
 | 
						|
 | 
						|
 | 
						|
def pack(options: argparse.Namespace) -> None:
 | 
						|
    # Basic sanity checks for input.
 | 
						|
    if not options.path:
 | 
						|
        print("tools/deploy: Path to bot folder not specified.")
 | 
						|
        sys.exit(1)
 | 
						|
    if not options.config:
 | 
						|
        print("tools/deploy: Path to zuliprc not specified.")
 | 
						|
        sys.exit(1)
 | 
						|
    if not options.main:
 | 
						|
        print("tools/deploy: No main bot file specified.")
 | 
						|
        sys.exit(1)
 | 
						|
    if not os.path.isfile(options.config):
 | 
						|
        print("pack: Config file not found at path: {}.".format(options.config))
 | 
						|
        sys.exit(1)
 | 
						|
    if not os.path.isdir(options.path):
 | 
						|
        print("pack: Bot folder not found at path: {}.".format(options.path))
 | 
						|
        sys.exit(1)
 | 
						|
    main_path = os.path.join(options.path, options.main)
 | 
						|
    if not os.path.isfile(main_path):
 | 
						|
        print("pack: Bot main file not found at path: {}.".format(main_path))
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
    # Main logic for packing the bot.
 | 
						|
    if not os.path.exists(bots_dir):
 | 
						|
        os.makedirs(bots_dir)
 | 
						|
    zip_file_path = os.path.join(bots_dir, options.botname + ".zip")
 | 
						|
    zip_file = zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED)
 | 
						|
    # Pack the complete bot folder
 | 
						|
    for root, dirs, files in os.walk(options.path):
 | 
						|
        for file in files:
 | 
						|
            file_path = os.path.join(root, file)
 | 
						|
            zip_file.write(file_path, os.path.relpath(file_path, options.path))
 | 
						|
    # Pack the zuliprc
 | 
						|
    zip_file.write(options.config, "zuliprc")
 | 
						|
    # Pack the config file for the botfarm.
 | 
						|
    bot_config = textwrap.dedent(
 | 
						|
        """\
 | 
						|
        [deploy]
 | 
						|
        bot={}
 | 
						|
        zuliprc=zuliprc
 | 
						|
        """.format(
 | 
						|
            options.main
 | 
						|
        )
 | 
						|
    )
 | 
						|
    zip_file.writestr("config.ini", bot_config)
 | 
						|
    zip_file.close()
 | 
						|
    print("pack: Created zip file at: {}.".format(zip_file_path))
 | 
						|
 | 
						|
 | 
						|
def check_common_options(options: argparse.Namespace) -> None:
 | 
						|
    if not options.server:
 | 
						|
        print("tools/deploy: URL to Botfarm server not specified.")
 | 
						|
        sys.exit(1)
 | 
						|
    if not options.token:
 | 
						|
        print("tools/deploy: Botfarm deploy token not specified.")
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def handle_common_response_without_data(
 | 
						|
    response: Response, operation: str, success_message: str
 | 
						|
) -> bool:
 | 
						|
    return handle_common_response(
 | 
						|
        response=response,
 | 
						|
        operation=operation,
 | 
						|
        success_handler=lambda r: print("{}: {}".format(operation, success_message)),
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def handle_common_response(
 | 
						|
    response: Response, operation: str, success_handler: Callable[[Dict[str, Any]], Any]
 | 
						|
) -> bool:
 | 
						|
    if response.status_code == requests.codes.ok:
 | 
						|
        response_data = response.json()
 | 
						|
        if response_data["status"] == "success":
 | 
						|
            success_handler(response_data)
 | 
						|
            return True
 | 
						|
        elif response_data["status"] == "error":
 | 
						|
            print("{}: {}".format(operation, response_data["message"]))
 | 
						|
            return False
 | 
						|
        else:
 | 
						|
            print("{}: Unexpected success response format".format(operation))
 | 
						|
            return False
 | 
						|
    if response.status_code == requests.codes.unauthorized:
 | 
						|
        print("{}: Authentication error with the server. Aborting.".format(operation))
 | 
						|
    else:
 | 
						|
        print("{}: Error {}. Aborting.".format(operation, response.status_code))
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def upload(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    file_path = os.path.join(bots_dir, options.botname + ".zip")
 | 
						|
    if not os.path.exists(file_path):
 | 
						|
        print("upload: Could not find bot package at {}.".format(file_path))
 | 
						|
        sys.exit(1)
 | 
						|
    files = {"file": open(file_path, "rb")}
 | 
						|
    headers = {"key": options.token}
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/upload")
 | 
						|
    response = requests.post(url, files=files, headers=headers)
 | 
						|
    result = handle_common_response_without_data(
 | 
						|
        response, "upload", "Uploaded the bot package to botfarm."
 | 
						|
    )
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def clean(options: argparse.Namespace) -> None:
 | 
						|
    file_path = os.path.join(bots_dir, options.botname + ".zip")
 | 
						|
    if os.path.exists(file_path):
 | 
						|
        os.remove(file_path)
 | 
						|
        print("clean: Removed {}.".format(file_path))
 | 
						|
    else:
 | 
						|
        print("clean: File '{}' not found.".format(file_path))
 | 
						|
 | 
						|
 | 
						|
def process(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {"key": options.token}
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/process")
 | 
						|
    payload = {"name": options.botname}
 | 
						|
    response = requests.post(url, headers=headers, json=payload)
 | 
						|
    result = handle_common_response_without_data(
 | 
						|
        response, "process", "The bot has been processed by the botfarm."
 | 
						|
    )
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def start(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {"key": options.token}
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/start")
 | 
						|
    payload = {"name": options.botname}
 | 
						|
    response = requests.post(url, headers=headers, json=payload)
 | 
						|
    result = handle_common_response_without_data(
 | 
						|
        response, "start", "The bot has been started by the botfarm."
 | 
						|
    )
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def stop(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {"key": options.token}
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/stop")
 | 
						|
    payload = {"name": options.botname}
 | 
						|
    response = requests.post(url, headers=headers, json=payload)
 | 
						|
    result = handle_common_response_without_data(
 | 
						|
        response, "stop", "The bot has been stopped by the botfarm."
 | 
						|
    )
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def prepare(options: argparse.Namespace) -> None:
 | 
						|
    pack(options)
 | 
						|
    upload(options)
 | 
						|
    clean(options)
 | 
						|
    process(options)
 | 
						|
 | 
						|
 | 
						|
def log(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {"key": options.token}
 | 
						|
    if options.lines:
 | 
						|
        lines = options.lines
 | 
						|
    else:
 | 
						|
        lines = None
 | 
						|
    payload = {"name": options.botname, "lines": lines}
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/logs/" + options.botname)
 | 
						|
    response = requests.get(url, json=payload, headers=headers)
 | 
						|
    result = handle_common_response(response, "log", lambda r: print(r["logs"]["content"]))
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def delete(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {"key": options.token}
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/delete")
 | 
						|
    payload = {"name": options.botname}
 | 
						|
    response = requests.post(url, headers=headers, json=payload)
 | 
						|
    result = handle_common_response_without_data(
 | 
						|
        response, "delete", "The bot has been removed from the botfarm."
 | 
						|
    )
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def list_bots(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {"key": options.token}
 | 
						|
    if options.format:
 | 
						|
        pretty_print = True
 | 
						|
    else:
 | 
						|
        pretty_print = False
 | 
						|
    url = urllib.parse.urljoin(options.server, "bots/list")
 | 
						|
    response = requests.get(url, headers=headers)
 | 
						|
    result = handle_common_response(
 | 
						|
        response, "ls", lambda r: print_bots(r["bots"]["list"], pretty_print)
 | 
						|
    )
 | 
						|
    if result is False:
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
 | 
						|
def print_bots(bots: List[Any], pretty_print: bool) -> None:
 | 
						|
    if pretty_print:
 | 
						|
        print_bots_pretty(bots)
 | 
						|
    else:
 | 
						|
        for bot in bots:
 | 
						|
            print("{}\t{}\t{}\t{}".format(bot["name"], bot["status"], bot["email"], bot["site"]))
 | 
						|
 | 
						|
 | 
						|
def print_bots_pretty(bots: List[Any]) -> None:
 | 
						|
    if len(bots) == 0:
 | 
						|
        print("ls: No bots found on the botfarm")
 | 
						|
    else:
 | 
						|
        print("ls: There are the following bots on the botfarm:")
 | 
						|
        name_col_len, status_col_len, email_col_len, site_col_len = 25, 15, 35, 35
 | 
						|
        row_format = "{0} {1} {2} {3}"
 | 
						|
        header = row_format.format(
 | 
						|
            "NAME".rjust(name_col_len),
 | 
						|
            "STATUS".rjust(status_col_len),
 | 
						|
            "EMAIL".rjust(email_col_len),
 | 
						|
            "SITE".rjust(site_col_len),
 | 
						|
        )
 | 
						|
        header_bottom = row_format.format(
 | 
						|
            "-" * name_col_len,
 | 
						|
            "-" * status_col_len,
 | 
						|
            "-" * email_col_len,
 | 
						|
            "-" * site_col_len,
 | 
						|
        )
 | 
						|
        print(header)
 | 
						|
        print(header_bottom)
 | 
						|
        for bot in bots:
 | 
						|
            row = row_format.format(
 | 
						|
                bot["name"].rjust(name_col_len),
 | 
						|
                bot["status"].rjust(status_col_len),
 | 
						|
                bot["email"].rjust(email_col_len),
 | 
						|
                bot["site"].rjust(site_col_len),
 | 
						|
            )
 | 
						|
            print(row)
 | 
						|
 | 
						|
 | 
						|
def main() -> None:
 | 
						|
    usage = """tools/deploy <command> <bot-name> [options]
 | 
						|
 | 
						|
This is tool meant to easily deploy bots to a Zulip Bot Farm.
 | 
						|
 | 
						|
First, get your deploy token from the Botfarm server. We recommend saving your
 | 
						|
deploy-token as $TOKEN and the bot-farm server as $SERVER. If you want to manually
 | 
						|
provide the SERVER and TOKEN values, use the --server="https://my-server.com"
 | 
						|
and --token="my-access-token" flags with each command. To deploy, run:
 | 
						|
 | 
						|
    tools/deploy prepare mybot \\
 | 
						|
        --path=/path/to/bot/directory --config=/path/to/zuliprc --main=main_bot_file.py
 | 
						|
 | 
						|
Now, your bot is ready to start.
 | 
						|
 | 
						|
    tools/deploy start mybot
 | 
						|
 | 
						|
To stop the bot, use:
 | 
						|
 | 
						|
    tools/deploy stop mybot
 | 
						|
 | 
						|
To get logs of the bot, use:
 | 
						|
    tools/deploy log mybot
 | 
						|
 | 
						|
To delete the bot, use:
 | 
						|
 | 
						|
    tools/deploy delete mybot
 | 
						|
 | 
						|
To list user's bots, use:
 | 
						|
 | 
						|
    tools/deploy ls
 | 
						|
 | 
						|
"""
 | 
						|
    parser = argparse.ArgumentParser(usage=usage)
 | 
						|
    parser.add_argument("command", help="Command to run.")
 | 
						|
    parser.add_argument("botname", nargs="?", help="Name of bot to operate on.")
 | 
						|
    parser.add_argument(
 | 
						|
        "--server",
 | 
						|
        "-s",
 | 
						|
        metavar="SERVERURL",
 | 
						|
        default=os.environ.get("SERVER", ""),
 | 
						|
        help="Url of the Zulip Botfarm server.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "--token", "-t", default=os.environ.get("TOKEN", ""), help="Deploy Token for the Botfarm."
 | 
						|
    )
 | 
						|
    parser.add_argument("--path", "-p", help="Path to the bot directory.")
 | 
						|
    parser.add_argument("--config", "-c", help="Path to the zuliprc file.")
 | 
						|
    parser.add_argument(
 | 
						|
        "--main", "-m", help="Path to the bot's main file, relative to the bot's directory."
 | 
						|
    )
 | 
						|
    parser.add_argument("--lines", "-l", help="Number of lines in log required.")
 | 
						|
    parser.add_argument(
 | 
						|
        "--format", "-f", action="store_true", help="Print user's bots in human readable format"
 | 
						|
    )
 | 
						|
    options = parser.parse_args()
 | 
						|
    if not options.command:
 | 
						|
        print("tools/deploy: No command specified.")
 | 
						|
        sys.exit(1)
 | 
						|
    if not options.botname and options.command not in ["ls"]:
 | 
						|
        print("tools/deploy: No bot name specified. Please specify a name like 'my-custom-bot'")
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
    commands = {
 | 
						|
        "pack": pack,
 | 
						|
        "upload": upload,
 | 
						|
        "clean": clean,
 | 
						|
        "prepare": prepare,
 | 
						|
        "process": process,
 | 
						|
        "start": start,
 | 
						|
        "stop": stop,
 | 
						|
        "log": log,
 | 
						|
        "delete": delete,
 | 
						|
        "ls": list_bots,
 | 
						|
    }
 | 
						|
    if options.command in commands:
 | 
						|
        commands[options.command](options)
 | 
						|
    else:
 | 
						|
        print("tools/deploy: No command '{}' found.".format(options.command))
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    main()
 |