Displays bot's name, status, email and site. Add `--format` to pretty-print the list of bots.
		
			
				
	
	
		
			318 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#!/usr/bin/env python
 | 
						|
 | 
						|
from typing import Any, List
 | 
						|
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import argparse
 | 
						|
import zipfile
 | 
						|
import textwrap
 | 
						|
import requests
 | 
						|
import urllib
 | 
						|
import json
 | 
						|
from urllib import parse
 | 
						|
 | 
						|
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.key:
 | 
						|
        print('tools/deploy: Botfarm deploy token not specified.')
 | 
						|
        sys.exit(1)
 | 
						|
 | 
						|
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.key}
 | 
						|
    url = urllib.parse.urljoin(options.server, 'bots/upload')
 | 
						|
    r = requests.post(url, files=files, headers=headers)
 | 
						|
    if r.status_code == requests.codes.ok:
 | 
						|
        print('upload: Uploaded the bot package to botfarm.')
 | 
						|
        return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('upload: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('upload: Error {}. Aborting.'.format(r.status_code))
 | 
						|
    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.key}
 | 
						|
    url = urllib.parse.urljoin(options.server, 'bots/process')
 | 
						|
    payload = {'name': options.botname}
 | 
						|
    r = requests.post(url, headers=headers, json=payload)
 | 
						|
    if r.status_code == requests.codes.ok and r.text == 'done':
 | 
						|
        print('process: The bot has been processed by the botfarm.')
 | 
						|
        return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('process: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('process: Error {}: {}. Aborting.'.format(r.status_code, r.text))
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
def start(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {'key': options.key}
 | 
						|
    url = urllib.parse.urljoin(options.server, 'bots/start')
 | 
						|
    payload = {'name': options.botname}
 | 
						|
    r = requests.post(url, headers=headers, json=payload)
 | 
						|
    if r.status_code == requests.codes.ok and r.text == 'done':
 | 
						|
        print('start: The bot has been started by the botfarm.')
 | 
						|
        return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('start: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('start: Error {}: {}. Aborting.'.format(r.status_code, r.text))
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
def stop(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {'key': options.key}
 | 
						|
    url = urllib.parse.urljoin(options.server, 'bots/stop')
 | 
						|
    payload = {'name': options.botname}
 | 
						|
    r = requests.post(url, headers=headers, json=payload)
 | 
						|
    if r.status_code == requests.codes.ok and r.text == 'done':
 | 
						|
        print('stop: The bot has been stopped by the botfarm.')
 | 
						|
        return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('stop: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('stop: Error {}: {}. Aborting.'.format(r.status_code, r.text))
 | 
						|
    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.key}
 | 
						|
    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)
 | 
						|
    r = requests.get(url, json=payload, headers=headers)
 | 
						|
    if r.status_code == requests.codes.ok:
 | 
						|
        print(r.text)
 | 
						|
        return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('log: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('log: Error {}: {}. Aborting.'.format(r.status_code, r.text))
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
def delete(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {'key': options.key}
 | 
						|
    url = urllib.parse.urljoin(options.server, 'bots/delete')
 | 
						|
    payload = {'name': options.botname}
 | 
						|
    r = requests.post(url, headers=headers, json=payload)
 | 
						|
    if r.status_code == requests.codes.ok and r.text == 'done':
 | 
						|
        print('delete: The bot has been removed from the botfarm.')
 | 
						|
        return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('delete: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('delete: Error {}: {}. Aborting.'.format(r.status_code, r.text))
 | 
						|
    sys.exit(1)
 | 
						|
 | 
						|
def list_bots(options: argparse.Namespace) -> None:
 | 
						|
    check_common_options(options)
 | 
						|
    headers = {'key': options.key}
 | 
						|
    if options.format:
 | 
						|
        pretty_print = True
 | 
						|
    else:
 | 
						|
        pretty_print = False
 | 
						|
    url = urllib.parse.urljoin(options.server, 'bots/list')
 | 
						|
    r = requests.get(url, headers=headers)
 | 
						|
    if r.status_code == requests.codes.ok:
 | 
						|
        data = json.loads(r.text)
 | 
						|
        if 'bots' in data:
 | 
						|
            print_bots(data['bots'], pretty_print)
 | 
						|
            return
 | 
						|
    if r.status_code == 401:
 | 
						|
        print('ls: Authentication error with the server. Aborting.')
 | 
						|
    else:
 | 
						|
        print('ls: Error {}: {}. Aborting.'.format(r.status_code, r.text))
 | 
						|
    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('{0}\t{1}\t{2}\t{3}'.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. To deploy, run:
 | 
						|
 | 
						|
    tools/deploy prepare mybot --server=$SERVER --key=$TOKEN \\
 | 
						|
        --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 --server=$SERVER --key=$TOKEN
 | 
						|
 | 
						|
To stop the bot, use:
 | 
						|
 | 
						|
    tools/deploy stop mybot --server=$SERVER --key=$TOKEN
 | 
						|
 | 
						|
To get logs of the bot, use:
 | 
						|
    tools/deploy log mybot --server=$SERVER --key=$TOKEN
 | 
						|
 | 
						|
To delete the bot, use:
 | 
						|
 | 
						|
    tools/deploy delete mybot --server=$SERVER --key=$TOKEN
 | 
						|
 | 
						|
To list user's bots, use:
 | 
						|
 | 
						|
    tools/deploy ls --server=$SERVER --key=$TOKEN
 | 
						|
 | 
						|
"""
 | 
						|
    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='https://botfarm.zulipdev.org',
 | 
						|
                        help='Url of the Zulip Botfarm server.')
 | 
						|
    parser.add_argument('--key', '-k',
 | 
						|
                        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()
 |