diff --git a/.travis.yml b/.travis.yml
index 5119576..95d175d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,7 @@
-sudo: required
-
 language: python
 
 python:
-  - "3.5"
-  - "3.6"
+  - "3.8"
 
 install:
   - pip install -e .
diff --git a/Dockerfile b/Dockerfile
index 6d61b65..d45bd7e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,17 +1,19 @@
 FROM ubuntu:xenial
 
 # Mostly Mike Grima: mgrima@netflix.com
-MAINTAINER NetflixOSS <netflixoss@netflix.com>
-
-# Install the Python RTM bot itself:
-ARG RTM_VERSION
-ADD python-rtmbot-${RTM_VERSION}.tar.gz /
+LABEL maintainer="netflixoss@netflix.com"
 
 RUN \
   # Install Python:
   apt-get update && \
   apt-get upgrade -y && \
-  apt-get install python3 python3-venv nano -y
+  apt-get install python3 python3-venv nano curl -y
+
+# Install the Python RTM bot itself:
+ARG RTM_VERSION="0.4.0"
+ARG RTM_PATH="python-rtmbot-${RTM_VERSION}"
+RUN curl -L https://github.com/slackhq/python-rtmbot/archive/${RTM_VERSION}.tar.gz > /${RTM_PATH}.tar.gz && tar xvzf python-rtmbot-0.4.0.tar.gz
+
 
 # Add all the other stuff to the plugins:
 COPY / /python-rtmbot-${RTM_VERSION}/hubcommander
@@ -20,19 +22,16 @@ COPY / /python-rtmbot-${RTM_VERSION}/hubcommander
 RUN \
   # Rename the rtmbot:
   mv /python-rtmbot-${RTM_VERSION} /rtmbot && \
-
   # Set up the VENV:
   pyvenv /venv && \
-
   # Install all the deps:
   /bin/bash -c "source /venv/bin/activate && pip install --upgrade pip" && \
   /bin/bash -c "source /venv/bin/activate && pip install --upgrade setuptools" && \
   /bin/bash -c "source /venv/bin/activate && pip install wheel" && \
   /bin/bash -c "source /venv/bin/activate && pip install /rtmbot/hubcommander" && \
-
   # The launcher script:
   mv /rtmbot/hubcommander/launch_in_docker.sh / && chmod +x /launch_in_docker.sh && \
-  rm /rtmbot/hubcommander/python-rtmbot-${RTM_VERSION}.tar.gz
+  rm /python-rtmbot-${RTM_VERSION}.tar.gz
 
 # DEFINE YOUR ENV VARS FOR SECRETS HERE:
 ENV SLACK_TOKEN="REPLACEMEINCMDLINE" \
diff --git a/README.md b/README.md
index 28e8a48..4b3e169 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,10 @@ administrative or `owner` privileges to your GitHub organization members.
 
 | Service   | Master | Develop |
 |:-----------:|:--------:|:---------:|
-|Travis CI|[![Build Status](https://travis-ci.org/Netflix/hubcommander.svg?branch=master)](https://travis-ci.org/Netflix/hubcommander)|[![Build Status](https://travis-ci.org/Netflix/hubcommander.svg?branch=develop)](https://travis-ci.org/Netflix/hubcommander)|
+|Travis CI|[![Build Status](https://travis-ci.com/Netflix/hubcommander.svg?branch=master)](https://travis-ci.com/Netflix/hubcommander)|[![Build Status](https://travis-ci.com/Netflix/hubcommander.svg?branch=develop)](https://travis-ci.com/Netflix/hubcommander)|
 
 
-How it works?
+How does it work?
 -------------
 HubCommander is based on [slackhq/python-rtmbot](https://github.com/slackhq/python-rtmbot)
 (currently, dependent on release [0.4.0](https://github.com/slackhq/python-rtmbot/releases/tag/0.4.0))
@@ -66,4 +66,4 @@ Please see the documentation [here](docs/installation.md) for details.
 Contributing
 ---------------
 If you are interested in contributing to HubCommander, please review the [contributing documentation](docs/contributing.md).
- 
\ No newline at end of file
+ 
diff --git a/auth_plugins/duo/plugin.py b/auth_plugins/duo/plugin.py
index 0136749..d16a2ac 100644
--- a/auth_plugins/duo/plugin.py
+++ b/auth_plugins/duo/plugin.py
@@ -30,35 +30,46 @@ class DuoPlugin(BotAuthPlugin):
     def __init__(self):
         super().__init__()
 
-        self.client = None
+        self.clients = {}
 
     def setup(self, secrets, **kwargs):
-        if not secrets.get("DUO_IKEY") or not secrets.get("DUO_SKEY") or not secrets.get("DUO_HOST"):
-            raise NoSecretsProvidedError("Must provide secrets to enable authentication.")
+        for variable, secret in secrets.items():
+            if "DUO_" in variable:
+                domain, host, ikey, skey = secret.split(",")
+                self.clients[domain] = Client(ikey, skey, host)
 
-        self.client = Client(secrets["DUO_IKEY"], secrets["DUO_SKEY"], secrets["DUO_HOST"])
+        if not len(self.clients):
+            raise NoSecretsProvidedError("Must provide secrets to enable authentication.")
 
     def authenticate(self, data, user_data, **kwargs):
+        # Which domain does this user belong to?
+        domain = user_data["profile"]["email"].split("@")[1]
+        if not self.clients.get(domain):
+            send_error(data["channel"], "💀 @{}: Duo in this bot is not configured for the domain: `{}`. It needs "
+                                        "to be configured for you to run this command."
+                       .format(user_data["name"], domain), markdown=True, thread=data["ts"])
+            return False
+
         send_info(data["channel"], "🎟 @{}: Sending a Duo notification to your device. You must approve!"
                   .format(user_data["name"]), markdown=True, ephemeral_user=user_data["id"])
 
         try:
-            result = self._perform_auth(user_data)
+            result = self._perform_auth(user_data, self.clients[domain])
         except InvalidDuoResponseError as idre:
             send_error(data["channel"], "💀 @{}: There was a problem communicating with Duo. Got this status: {}. "
                                         "Aborting..."
-                       .format(user_data["name"], str(idre)), markdown=True)
+                       .format(user_data["name"], str(idre)), thread=data["ts"], markdown=True)
             return False
 
         except CantDuoUserError as _:
             send_error(data["channel"], "💀 @{}: I can't Duo authenticate you. Please consult with your identity team."
                                         " Aborting..."
-                       .format(user_data["name"]), markdown=True)
+                       .format(user_data["name"]), thread=data["ts"], markdown=True)
             return False
 
         except Exception as e:
             send_error(data["channel"], "💀 @{}: I encountered some issue with Duo... Here are the details: ```{}```"
-                       .format(user_data["name"], str(e)), markdown=True)
+                       .format(user_data["name"], str(e)), thread=data["ts"], markdown=True)
             return False
 
         if not result:
@@ -71,14 +82,14 @@ def authenticate(self, data, user_data, **kwargs):
                      .format(user_data["name"]), markdown=True, ephemeral_user=user_data["id"])
         return True
 
-    def _perform_auth(self, user_data):
+    def _perform_auth(self, user_data, client):
         # Push to devices:
         duo_params = {
             "username": user_data["profile"]["email"],
             "factor": "push",
             "device": "auto"
         }
-        response, data = self.client.api_call("POST", "/auth/v2/auth", duo_params)
+        response, data = client.api_call("POST", "/auth/v2/auth", duo_params)
         result = json.loads(data.decode("utf-8"))
 
         if response.status != 200:
diff --git a/bot_components/decorators.py b/bot_components/decorators.py
index 62d6fa0..8e75e71 100644
--- a/bot_components/decorators.py
+++ b/bot_components/decorators.py
@@ -81,13 +81,10 @@ def perform_additional_verification(plugin_obj, args, **kwargs):
                         elif argument.get("lowercase", True):
                             args[real_arg_name] = args[real_arg_name].lower()
 
-                        # Perform cleanups? This will remove things like the annoying macOS "smart quotes",
-                        # and the <>, {}, &lt;&gt; from the variables if `cleanup=False` not set.
+                        # Perform cleanups? Removes <>, {}, &lt;&gt; from the variables if `cleanup=False` not set.
                         if argument.get("cleanup", True):
                             args[real_arg_name] = args[real_arg_name].replace("<", "") \
                                 .replace(">", "").replace("{", "").replace("}", "") \
-                                .replace(u'\u201C', "\"").replace(u'\u201D', "\"") \
-                                .replace(u'\u2018', "\'").replace(u'\u2019', "\'") \
                                 .replace("[", "").replace("]", "") \
                                 .replace("&lt;", "").replace("&gt;", "")
 
@@ -140,6 +137,10 @@ def decorated_command(plugin_obj, data, user_data):
 
                         parser.add_argument(argument["name"], **argument["properties"])
 
+            # Remove all the macOS "Smart Quotes":
+            data["text"] = data["text"].replace(u'\u201C', "\"").replace(u'\u201D', "\"") \
+                .replace(u'\u2018', "\'").replace(u'\u2019', "\'")
+
             # Remove the command from the command string:
             split_args = shlex.split(data["text"])[1:]
             try:
diff --git a/bot_components/parse_functions.py b/bot_components/parse_functions.py
index c88c0d9..7038477 100644
--- a/bot_components/parse_functions.py
+++ b/bot_components/parse_functions.py
@@ -121,6 +121,24 @@ def extract_repo_name(plugin_obj, reponame, **kwargs):
     return split_repo.replace(">", "")
 
 
+def extract_multiple_repo_names(plugin_obj, repos, **kwargs):
+    """
+    Does what the above does, but does it for a comma separated list of repos.
+    :param plugin_obj:
+    :param repos:
+    :param kwargs:
+    :return:
+    """
+    repo_list = repos.split(",")
+
+    parsed_repos = []
+
+    for repo in repo_list:
+        parsed_repos.append(extract_repo_name(plugin_obj, repo, **kwargs))
+
+    return parsed_repos
+
+
 def parse_toggles(plugin_obj, toggle, toggle_type="toggle", **kwargs):
     """
     Parses typical toggle values, like off, on, enabled, disabled, true, false, etc.
diff --git a/build_docker.sh b/build_docker.sh
index bb79671..c8f7126 100755
--- a/build_docker.sh
+++ b/build_docker.sh
@@ -26,12 +26,12 @@ if [ -z ${BUILD_TAG} ]; then
   BUILD_TAG="latest"
 fi
 
-# If this is running in Travis, AND the Python version IS NOT 3.5, then don't build
+# If this is running in Travis, AND the Python version IS NOT 3.6, then don't build
 # the Docker image:
 if [ $TRAVIS ]; then
     PYTHON_VERSION=$( python --version )
-    if [[ $PYTHON_VERSION != *"3.5"* ]]; then
-        echo "This only builds Docker images in the Python 3.5 Travis job"
+    if [[ $PYTHON_VERSION != *"3.6"* ]]; then
+        echo "This only builds Docker images in the Python 3.6 Travis job"
         exit 0
     fi
 fi
@@ -55,5 +55,12 @@ echo "[-->] Now building the Docker image..."
 # Build that Docker image...
 docker build  -t netflixoss/hubcommander:${BUILD_TAG} --rm=true . --build-arg RTM_VERSION=${RTM_VERSION}
 
+cmd_st="$?"
+if [ $cmd_st -gt 0 ]
+then
+  echo "Error building image. Exiting."
+  exit $cmd_st
+fi
+
 echo
 echo "DONE!"
diff --git a/command_plugins/github/config.py b/command_plugins/github/config.py
index 10ac55d..3edae67 100644
--- a/command_plugins/github/config.py
+++ b/command_plugins/github/config.py
@@ -13,6 +13,11 @@
                 "perm": "push",         # The permission, either "push", "pull", or "admin"...
                 "name": "TeamName"      # The name of the team here...
             }
+        ],
+        "collab_validation_teams": [
+            # If users are members of any of the teams in this list, they will
+            # not be added as outside collaborators
+            "TeamName"
         ]
     }
 }
diff --git a/command_plugins/github/decorators.py b/command_plugins/github/decorators.py
index 0968b8a..5729a21 100644
--- a/command_plugins/github/decorators.py
+++ b/command_plugins/github/decorators.py
@@ -9,13 +9,38 @@
 from hubcommander.bot_components.slack_comm import send_error
 
 
-def repo_must_exist(org_arg="org", repo_arg="repo"):
+def repo_must_exist(org_arg="org"):
     def command_decorator(func):
         def decorated_command(github_plugin, data, user_data, *args, **kwargs):
+            # Just 1 repo -- or multiple?
+            if kwargs.get("repo"):
+                repos = [kwargs["repo"]]
+            else:
+                repos = kwargs["repos"]
+
             # Check if the specified GitHub repo exists:
-            if not github_plugin.check_if_repo_exists(data, user_data, kwargs[repo_arg], kwargs[org_arg]):
-                return
+            for repo in repos:
+                if not github_plugin.check_if_repo_exists(data, user_data, repo, kwargs[org_arg]):
+                    return
+
+            # Run the next function:
+            return func(github_plugin, data, user_data, *args, **kwargs)
+
+        return decorated_command
+
+    return command_decorator
+
 
+def team_must_exist(org_arg="org", team_arg="team"):
+    def command_decorator(func):
+        def decorated_command(github_plugin, data, user_data, *args, **kwargs):
+            # Check if the specified GitHub team exists:
+            team_id = github_plugin.find_team_id_by_name(kwargs[org_arg], kwargs[team_arg])
+            if not team_id:
+                send_error(data["channel"], "@{}: The GitHub team: {} does not exist.".format(user_data["name"],
+                                                                                              kwargs[team_arg]),
+                           thread=data["ts"])
+                return
             # Run the next function:
             return func(github_plugin, data, user_data, *args, **kwargs)
 
@@ -33,13 +58,15 @@ def decorated_command(github_plugin, data, user_data, *args, **kwargs):
 
                 if not found_user:
                     send_error(data["channel"], "@{}: The GitHub user: {} does not exist.".format(user_data["name"],
-                                                                                                  kwargs[user_arg]))
+                                                                                                  kwargs[user_arg]),
+                               thread=data["ts"])
                     return
 
             except Exception as e:
                 send_error(data["channel"],
                            "@{}: A problem was encountered communicating with GitHub to verify the user's GitHub "
-                           "id. Here are the details:\n{}".format(user_data["name"], str(e)))
+                           "id. Here are the details:\n{}".format(user_data["name"], str(e)),
+                           thread=data["ts"])
                 return
 
             # Run the next function:
@@ -66,7 +93,7 @@ def decorated_command(github_plugin, data, user_data, *args, **kwargs):
                 send_error(data["channel"],
                            "@{}: This repository does not have the branch: `{}`.".format(user_data["name"],
                                                                                          kwargs[branch_arg]),
-                           markdown=True)
+                           markdown=True, thread=data["ts"])
                 return
 
             # Run the next function:
diff --git a/command_plugins/github/plugin.py b/command_plugins/github/plugin.py
index d962780..a294fc5 100644
--- a/command_plugins/github/plugin.py
+++ b/command_plugins/github/plugin.py
@@ -15,10 +15,11 @@
 from hubcommander.bot_components.bot_classes import BotCommander
 from hubcommander.bot_components.decorators import hubcommander_command, auth
 from hubcommander.bot_components.slack_comm import send_info, send_success, send_error, send_raw
-from hubcommander.bot_components.parse_functions import extract_repo_name, parse_toggles
+from hubcommander.bot_components.parse_functions import extract_repo_name, parse_toggles, extract_multiple_repo_names
 from hubcommander.command_plugins.github.config import GITHUB_URL, GITHUB_VERSION, ORGS, USER_COMMAND_DICT
 from hubcommander.command_plugins.github.parse_functions import lookup_real_org, validate_homepage
-from hubcommander.command_plugins.github.decorators import repo_must_exist, github_user_exists, branch_must_exist
+from hubcommander.command_plugins.github.decorators import repo_must_exist, github_user_exists, branch_must_exist, \
+    team_must_exist
 
 
 class GitHubPlugin(BotCommander):
@@ -48,6 +49,21 @@ def __init__(self):
                 "permitted_permissions": ["push", "pull"],  # To grant admin, add this to the config for
                 "enabled": True  # this command in the config.py.
             },
+            "!RemoveCollab": {
+                "command": "!RemoveCollab",
+                "func": self.remove_outside_collab_command,
+                "user_data_required": True,
+                "help": "Removes an outside collaborator from a specific repository in a specific GitHub organization.",
+                "enabled": True  # this command in the config.py.
+            },
+            "!SetRepoPermissions": {
+                "command": "!SetRepoPermissions",
+                "func": self.set_repo_permissions_command,
+                "user_data_required": True,
+                "help": "Sets team permissions to a specific repository in a specific GitHub organization.",
+                "permitted_permissions": ["push", "pull"],  # To grant admin, add this to the config for
+                "enabled": True  # this command in the config.py.
+            },
             "!SetDescription": {
                 "command": "!SetDescription",
                 "func": self.set_description_command,
@@ -265,14 +281,15 @@ def set_repo_homepage_command(self, data, user_data, org, repo, homepage):
 
     @hubcommander_command(
         name="!AddCollab",
-        usage="!AddCollab <OutsideCollabId> <OrgWithRepo> <Repo> <Permission>",
+        usage="!AddCollab <OutsideCollabId> <OrgWithRepo> <Repos(Comma separated if more than 1)> <Permission>",
         description="This will add an outside collaborator to a repository with the given permission.",
         required=[
             dict(name="collab", properties=dict(type=str, help="The outside collaborator's GitHub ID.")),
             dict(name="org", properties=dict(type=str, help="The organization that contains the repo."),
                  validation_func=lookup_real_org, validation_func_kwargs={}),
-            dict(name="repo", properties=dict(type=str, help="The repository to add the outside collaborator to."),
-                 validation_func=extract_repo_name, validation_func_kwargs={}),
+            dict(name="repos", properties=dict(type=str, help="A comma separated list (or not if just 1) of repos to "
+                                                              "add the collaborator to."),
+                 validation_func=extract_multiple_repo_names, validation_func_kwargs={}),
             dict(name="permission", properties=dict(type=str.lower, help="The permission to grant, must be one "
                                                                          "of: `{values}`"),
                  choices="permitted_permissions"),
@@ -282,9 +299,9 @@ def set_repo_homepage_command(self, data, user_data, org, repo, homepage):
     @auth()
     @repo_must_exist()
     @github_user_exists("collab")
-    def add_outside_collab_command(self, data, user_data, collab, org, repo, permission):
+    def add_outside_collab_command(self, data, user_data, collab, org, repos, permission):
         """
-        Adds an outside collaborator a repository with a specified permission.
+        Adds an outside collaborator to repository (or multiple repos) with a specified permission.
 
         Command is as follows: !addcollab <outside_collab_id> <organization> <repo> <permission>
         :param permission:
@@ -300,7 +317,8 @@ def add_outside_collab_command(self, data, user_data, collab, org, repo, permiss
 
         # Grant access:
         try:
-            self.add_outside_collab_to_repo(collab, repo, org, permission)
+            for r in repos:
+                self.add_outside_collab_to_repo(collab, r, org, permission)
 
         except ValueError as ve:
             send_error(data["channel"],
@@ -317,7 +335,121 @@ def add_outside_collab_command(self, data, user_data, collab, org, repo, permiss
         # Done:
         send_success(data["channel"],
                      "@{}: The GitHub user: `{}` has been added as an outside collaborator with `{}` "
-                     "permissions to {}/{}.".format(user_data["name"], collab, permission,
+                     "permissions to {} in {}.".format(user_data["name"], collab, permission,
+                                                       ", ".join(repos), org),
+                     markdown=True, thread=data["ts"])
+
+    @hubcommander_command(
+        name="!RemoveCollab",
+        usage="!RemoveCollab <OutsideCollabId> <OrgWithRepo> <Repos(Comma separated if more than 1)>",
+        description="This will remove an outside collaborator from a repository.",
+        required=[
+            dict(name="collab", properties=dict(type=str, help="The outside collaborator's GitHub ID.")),
+            dict(name="org", properties=dict(type=str, help="The organization that contains the repo."),
+                 validation_func=lookup_real_org, validation_func_kwargs={}),
+            dict(name="repos", properties=dict(type=str, help="A comma separated list (or not if just 1) of repos to "
+                                                              "add the collaborator to."),
+                 validation_func=extract_multiple_repo_names, validation_func_kwargs={}),
+        ],
+        optional=[]
+    )
+    @auth()
+    @repo_must_exist()
+    @github_user_exists("collab")
+    def remove_outside_collab_command(self, data, user_data, collab, org, repos):
+        """
+        Removes an outside collaborator to repository (or multiple repos).
+
+        Command is as follows: !removecollab <outside_collab_id> <organization> <repo>
+        :param repo:
+        :param org:
+        :param collab:
+        :param user_data:
+        :param data:
+        :return:
+        """
+        # Output that we are doing work:
+        send_info(data["channel"], "@{}: Working, Please wait...".format(user_data["name"]), thread=data["ts"])
+
+        # Grant access:
+        try:
+            for r in repos:
+                self.remove_outside_collab_from_repo(collab, r, org)
+
+        except ValueError as ve:
+            send_error(data["channel"],
+                       "@{}: Problem encountered removing the user as an outside collaborator.\n"
+                       "The response code from GitHub was: {}".format(user_data["name"], str(ve)), thread=data["ts"])
+            return
+
+        except Exception as e:
+            send_error(data["channel"],
+                       "@{}: Problem encountered removing the user as an outside collaborator.\n"
+                       "Here are the details: {}".format(user_data["name"], str(e)), thread=data["ts"])
+            return
+
+        # Done:
+        send_success(data["channel"],
+                     "@{}: The GitHub user: `{}` has been removed as an outside collaborator "
+                     "from {} in {}.".format(user_data["name"], collab,
+                                             ", ".join(repos), org),
+                     markdown=True, thread=data["ts"])
+
+    @hubcommander_command(
+        name="!SetRepoPermissions",
+        usage="!SetRepoPermissions <OrgWithRepo> <Repo> <Team> <Permission>",
+        description="This will set team permissions on a repository .",
+        required=[
+            dict(name="org", properties=dict(type=str, help="The organization that contains the repo."),
+                 validation_func=lookup_real_org, validation_func_kwargs={}),
+            dict(name="repo", properties=dict(type=str, help="The repository to add the team to."),
+                 validation_func=extract_repo_name, validation_func_kwargs={}),
+            dict(name="team", properties=dict(type=str, help="The team's name.")),
+            dict(name="permission", properties=dict(type=str.lower, help="The permission to grant, must be one "
+                                                                         "of: `{values}`"),
+                 choices="permitted_permissions")
+        ],
+        optional=[]
+    )
+    @auth()
+    @repo_must_exist()
+    @team_must_exist()
+    def set_repo_permissions_command(self, data, user_data, team, org, repo, permission):
+        """
+        Adds a team to a repository with a specified permission.
+
+        Command is as follows: !SetRepoPermissions <Team> <OrgWithRepo> <Repo> <Permission>
+        :param permission:
+        :param repo:
+        :param org:
+        :param team:
+        :param user_data:
+        :param data:
+        :return:
+        """
+        # Output that we are doing work:
+        send_info(data["channel"], "@{}: Working, Please wait...".format(user_data["name"]), thread=data["ts"])
+
+        # Grant access:
+        try:
+            self.set_repo_permissions(repo, org, team, permission)
+
+        except ValueError as ve:
+            send_error(data["channel"],
+                       "@{}: Problem encountered adding the team.\n"
+                       "The response code from GitHub was: {}".format(user_data["name"], str(ve)), thread=data["ts"])
+            return
+
+        except Exception as e:
+            send_error(data["channel"],
+                       "@{}: Problem encountered adding the team.\n"
+                       "Here are the details: {}".format(user_data["name"], str(e)), thread=data["ts"])
+            return
+
+        # Done:
+        send_success(data["channel"],
+                     "@{}: The GitHub team: `{}` has been added to the repo with `{}` "
+                     "permissions to {}/{}.".format(user_data["name"], team, permission,
                                                     org, repo),
                      markdown=True, thread=data["ts"])
 
@@ -338,6 +470,7 @@ def add_outside_collab_command(self, data, user_data, collab, org, repo, permiss
     )
     @auth()
     @github_user_exists("user_id")
+    @team_must_exist()
     def add_user_to_team_command(self, data, user_data, user_id, org, team, role):
         """
         Adds a GitHub user to a team with a specified role.
@@ -354,16 +487,9 @@ def add_user_to_team_command(self, data, user_data, user_id, org, team, role):
         # Output that we are doing work:
         send_info(data["channel"], "@{}: Working, Please wait...".format(user_data["name"]), thread=data["ts"])
 
-        # Check if team exists, if it does return the id
-        team_id = self.find_team_id_by_name(org, team)
-
-        if not team_id:
-            send_error(data["channel"], "The GitHub team does not exist.", thread=data["ts"])
-            return
-
         # Do it:
         try:
-            self.invite_user_to_gh_org_team(user_id, team_id, role)
+            self.invite_user_to_gh_org_team(org, team, user_id, role)
 
         except ValueError as ve:
             send_error(data["channel"],
@@ -446,7 +572,7 @@ def create_repo_command(self, data, user_data, org, repo):
         # Grant the proper teams access to the repository:
         try:
             for perm_dict in ORGS[org]["new_repo_teams"]:
-                self.set_repo_permissions(repo, org, perm_dict["id"], perm_dict["perm"])
+                self.set_repo_permissions(repo, org, perm_dict["name"], perm_dict["perm"])
 
         except Exception as e:
             send_error(data["channel"],
@@ -920,7 +1046,7 @@ def check_if_repo_exists(self, data, user_data, reponame, real_org):
 
             if not result:
                 send_error(data["channel"],
-                           "@{}: This repository does not exist in {}.".format(user_data["name"], real_org),
+                           "@{}: The repository {}/{} does not exist.".format(user_data["name"], real_org, reponame),
                            thread=data["ts"])
                 return False
 
@@ -1167,6 +1293,17 @@ def set_repo_topics_http(self, org, repo, topics, **kwargs):
         return True
 
     def add_outside_collab_to_repo(self, outside_collab_id, repo_name, real_org, permission):
+        # Make sure the user is not a member of any of the "validation" teams
+        # in an org; this prevents us from accidentally adding collaborators who
+        # are already given the permissions they need via a different mechanism.
+        if "collab_validation_teams" in ORGS[real_org]:
+            for team in ORGS[real_org]["collab_validation_teams"]:
+                if self.check_if_user_is_member_of_team(real_org, outside_collab_id, team):
+                    raise Exception(("User {} is already a member of the {} "
+                        "team in {}. You should not add them as an external "
+                        "collaborator as well. Consider using the !InviteMeTo command "
+                        "instead.").format(outside_collab_id, team, real_org))
+
         headers = {
             'Authorization': 'token {}'.format(self.token),
             'Accept': GITHUB_VERSION
@@ -1182,6 +1319,22 @@ def add_outside_collab_to_repo(self, outside_collab_id, repo_name, real_org, per
         if response.status_code not in [201, 204]:
             raise ValueError(response.status_code)
 
+    def remove_outside_collab_from_repo(self, outside_collab_id, repo_name, real_org):
+        headers = {
+            'Authorization': 'token {}'.format(self.token),
+            'Accept': GITHUB_VERSION
+        }
+
+        # Add the outside collab to the repo:
+        api_part = 'repos/{}/{}/collaborators/{}'.format(real_org, repo_name, outside_collab_id)
+        response = requests.delete('{}{}'.format(GITHUB_URL, api_part),
+                                   headers=headers,
+                                   timeout=10)
+
+        # GitHub response code flakiness...
+        if response.status_code not in [201, 204]:
+            raise ValueError(response.status_code)
+
     def create_new_repo(self, repo_to_create, org, visibility):
         headers = {
             'Authorization': 'token {}'.format(self.token),
@@ -1231,7 +1384,7 @@ def set_repo_permissions(self, repo_to_set, org, team, permission):
             'Authorization': 'token {}'.format(self.token),
             'Accept': GITHUB_VERSION
         }
-        api_part = 'teams/{}/repos/{}/{}'.format(team, org, repo_to_set)
+        api_part = 'orgs/{}/teams/{}/repos/{}/{}'.format(org, team, org, repo_to_set)
 
         data = {
             "permission": permission
@@ -1275,23 +1428,29 @@ def set_branch_protection(self, repo, org, branch, enabled):
         # See: https://developer.github.com/v3/repos/branches/#enabling-and-disabling-branch-protection
         headers = {
             'Authorization': 'token {}'.format(self.token),
-            'Accept': "application/vnd.github.loki-preview+json"
         }
-        api_part = 'repos/{}/{}/branches/{}'.format(org, repo, branch)
-
-        data = {
-            "protection": {
-                "enabled": enabled
+        api_part = 'repos/{}/{}/branches/{}/protection'.format(org, repo, branch)
+        if enabled:
+            data = {
+                "required_status_checks": None,
+                "enforce_admins": None,
+                "required_pull_request_reviews": None,
+                "restrictions": None
             }
-        }
+            response = requests.put('{}{}'.format(GITHUB_URL, api_part), json=data, headers=headers, timeout=10)
 
-        response = requests.patch('{}{}'.format(GITHUB_URL, api_part), data=json.dumps(data), headers=headers,
-                                  timeout=10)
+            if response.status_code != 200:
+                message = 'An error was encountered communicating with GitHub: Status Code: {}' \
+                    .format(response.status_code)
+                raise requests.exceptions.RequestException(message)
 
-        if response.status_code != 200:
-            message = 'An error was encountered communicating with GitHub: Status Code: {}' \
-                .format(response.status_code)
-            raise requests.exceptions.RequestException(message)
+        else:
+            response = requests.delete('{}{}'.format(GITHUB_URL, api_part), headers=headers, timeout=10)
+
+            if response.status_code != 204:
+                message = 'An error was encountered communicating with GitHub: Status Code: {}' \
+                    .format(response.status_code)
+                raise requests.exceptions.RequestException(message)
 
     def check_if_user_is_member_of_org(self, github_id, org):
         # Check if the user exists first:
@@ -1317,7 +1476,37 @@ def check_if_user_is_member_of_org(self, github_id, org):
 
         return False
 
-    def invite_user_to_gh_org_team(self, github_id, team_id, role):
+    def check_if_user_is_member_of_team(self, org, github_id, team_name):
+        """
+        This will connect to GitHub, and try to retrieve the membership status of a
+        single user for a team in a given org.
+        """
+
+        # Check if the user exists first:
+        user = self.get_github_user(github_id)
+
+        if not user:
+            return None
+
+
+        headers = {
+            'Authorization': 'token {}'.format(self.token),
+            'Accept': GITHUB_VERSION
+        }
+
+        # Retrieve a users membership details.
+        api_part = 'orgs/{}/teams/{}/memberships/{}'.format(org, team_name, github_id)
+        response = requests.get('{}{}'.format(GITHUB_URL, api_part), headers=headers, timeout=10)
+
+        if response.status_code == 200:
+            return True
+
+        elif response.status_code != 404:
+            raise ValueError("GitHub Problem: Checking membership, status code: {}".format(response.status_code))
+
+        return False
+
+    def invite_user_to_gh_org_team(self, org, team, username, role):
         headers = {
             'Authorization': 'token {}'.format(self.token),
             'Accept': GITHUB_VERSION
@@ -1326,7 +1515,7 @@ def invite_user_to_gh_org_team(self, github_id, team_id, role):
         data = {"role": role}
 
         # Add the GitHub user to the team:
-        api_part = 'teams/{}/memberships/{}'.format(team_id, github_id)
+        api_part = 'orgs/{}/teams/{}/memberships/{}'.format(org, team, username)
         response = requests.put('{}{}'.format(GITHUB_URL, api_part), data=json.dumps(data), headers=headers, timeout=10)
 
         if response.status_code != 200:
diff --git a/command_plugins/travis_ci/plugin.py b/command_plugins/travis_ci/plugin.py
index 83e939c..2111ec3 100644
--- a/command_plugins/travis_ci/plugin.py
+++ b/command_plugins/travis_ci/plugin.py
@@ -119,7 +119,7 @@ def list_org_command(data):
 
     @hubcommander_command(
         name="!EnableTravis",
-        usage="!EnableTravis <OrgWithRepo> <Repo>",
+        usage="!EnableTravis <OrgWithRepo> <Repo> [--public=true]",
         description="This will enable Travis CI on a GitHub repository.",
         required=[
             dict(name="org", properties=dict(type=str, help="The organization that contains the repo."),
@@ -127,14 +127,16 @@ def list_org_command(data):
             dict(name="repo", properties=dict(type=str, help="The repository to enable Travis CI on."),
                  validation_func=extract_repo_name, validation_func_kwargs={})
         ],
-        optional=[]
+        optional=[dict(name="--public",
+                       properties=dict(type=str, help="When set to true - attempts to enable Travis CI using the public travis-ci.org"))]
     )
     @auth()
-    def enable_travis_command(self, data, user_data, org, repo):
+    def enable_travis_command(self, data, user_data, org, repo, public):
         """
         Enables Travis CI on a repository within the organization.
 
-        Command is as follows: !enabletravis <organization> <repo>
+        Command is as follows: !enabletravis <organization> <repo> [--public=true]
+        :param public:
         :param repo:
         :param org:
         :param user_data:
@@ -162,7 +164,7 @@ def enable_travis_command(self, data, user_data, org, repo):
                        "@{}: I encountered a problem:\n\n{}".format(user_data["name"], e), thread=data["ts"])
             return
 
-        which = "pro" if repo_result["private"] else "public"
+        which = "public" if (public and public.lower() == 'true') else "pro"
 
         try:
             # Sync with Travis CI so that it knows about the repo:
diff --git a/decrypt_creds.py b/decrypt_creds.py
index b136742..7df65c7 100644
--- a/decrypt_creds.py
+++ b/decrypt_creds.py
@@ -10,25 +10,34 @@ def get_credentials():
     # For Docker, encryption is assumed to be happening outside of this, and the secrets
     # are instead being passed in as environment variables:
     import os
-    return {
+
+    creds = {
         # Minimum
         "SLACK": os.environ["SLACK_TOKEN"],
 
         # Optional:
         "GITHUB": os.environ.get("GITHUB_TOKEN"),
-        "TRAVIS_PRO_USER": os.environ.get("TRAVIS_PRO_USER"),
-        "TRAVIS_PRO_ID": os.environ.get("TRAVIS_PRO_ID"),
-        "TRAVIS_PRO_TOKEN": os.environ.get("TRAVIS_PRO_TOKEN"),
-        "TRAVIS_PUBLIC_USER": os.environ.get("TRAVIS_PUBLIC_USER"),
-        "TRAVIS_PUBLIC_ID": os.environ.get("TRAVIS_PUBLIC_ID"),
-        "TRAVIS_PUBLIC_TOKEN": os.environ.get("TRAVIS_PUBLIC_TOKEN"),
-        "DUO_HOST": os.environ.get("DUO_HOST"),
-        "DUO_IKEY": os.environ.get("DUO_IKEY"),
-        "DUO_SKEY": os.environ.get("DUO_SKEY"),
+
+        # These are named the same as the env var, but these are the env vars should you
+        # want to leverage the feature:
+        # "TRAVIS_PRO_USER": os.environ.get("TRAVIS_PRO_USER"),
+        # "TRAVIS_PRO_ID": os.environ.get("TRAVIS_PRO_ID"),
+        # "TRAVIS_PRO_TOKEN": os.environ.get("TRAVIS_PRO_TOKEN"),
+        # "TRAVIS_PUBLIC_USER": os.environ.get("TRAVIS_PUBLIC_USER"),
+        # "TRAVIS_PUBLIC_ID": os.environ.get("TRAVIS_PUBLIC_ID"),
+        # "TRAVIS_PUBLIC_TOKEN": os.environ.get("TRAVIS_PUBLIC_TOKEN"),
+
+        # DUO_...NAME_OF_DUO_CRED: "domain-that-is-duod.com,duo_host,duo_ikey,duo_skey"
 
         # ADD MORE HERE...
     }
 
+    # Just adds the rest for freely-named ones (Like for Duo):
+    for variable, value in os.environ.items():
+        creds[variable] = value
+
+    return creds
+
 
 # def kms_decrypt():
 #     """
@@ -69,9 +78,7 @@ def get_credentials():
     "TRAVIS_PUBLIC_USER": "GitHub ID of GitHub account with access to Travis Public",
     "TRAVIS_PUBLIC_ID": "The ID of the Travis user. Use the Travis API to get this (for Public)",
     "TRAVIS_PUBLIC_TOKEN": Use the Travis API to get the Travis token (for the Travis Public account)",
-    "DUO-HOST": "xxxxxxxx.duosecurity.com",
-    "DUO-IKEY": "The IKEY for Duo",
-    "DUO-SKEY": "The SKEY for Duo"
+    "DUO_YOUR_DOMAIN": "your-domain-here.com,xxxxxxxx.duosecurity.com,THEDUOIKEY,THEDUOSKEY"
 }
 
 encrypt_res = kms_client.encrypt(KeyId=kms_arn, Plaintext=bytes(json.dumps(secrets_to_encrypt, indent=4), "utf-8"))
diff --git a/docs/authentication.md b/docs/authentication.md
index ebd606f..ec03cd9 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -1,6 +1,6 @@
 Authentication Plugins
 ===================
-HubCommander supports the ability to add authentication to the command flow. This is useful to safegurad
+HubCommander supports the ability to add authentication to the command flow. This is useful to safeguard
 specific commands, and additionally, add a speedbump to privileged commands.
 
 For organizations making use of Duo, a plugin is supplied that will prompt a user's device for approval
@@ -25,10 +25,19 @@ An example for how this is configured can be found in
 Enabling Duo
 ------------
 Duo is disabled by default. To enable Duo, you will need the following information:
+   1. The domain name that is Duo protected
+   1. The Duo Host
+   1. The "IKEY"
+   1. The "SKEY"
 
-   - `DUO-HOST`: Your administrator needs to provide you with this
-   - `DUO-IKEY`: The `IKEY`, provided to you by your administrator
-   - `DUO-SKEY`: The `SKEY`, provided to you by your administrator
+HubCommander supports multiple Duo domains. For this to work, you will need the information above
+for the given domain. Additionally, the secrets dictionary needs to be updated such that it has a key that starts
+with `DUO_`.  This key needs a comma-separated list of the domain, duo host, `ikey`, and `skey`.  It needs to look like:
+
+    "DUO_DOMAIN_ONE": "domainone.com,YOURHOST.duosecurity.com,THEIKEY,THESKEY"
+    "DUO_DOMAIN_TWO": "domaintwo.com,YOUROTHERHOST.duosecurity.com,THEOTHERIKEY,THEOTHERSKEY"
+
+The email address of the Slack user will determine which domain gets used.
 
 With the above information, you need to modify the secrets `dict` that is decrypted by the application
 on startup.
diff --git a/docs/command_config.md b/docs/command_config.md
index cf91472..e5e4b4e 100644
--- a/docs/command_config.md
+++ b/docs/command_config.md
@@ -44,3 +44,12 @@ By default, authentication is disabled. An example of enabling authentication ca
 [`config.py`](https://github.com/Netflix/hubcommander/blob/master/github/config.py) file.
 
 _For more details on authentication plugins, please read the [authentication plugin documentation](authentication.md)._
+
+
+### Hidden Commands
+
+If you ever retire a command, you can make it hidden from the `!Help` output. You can modify the command so that it
+outputs information redirecting the user to the new and supported command to utilize.
+
+To do this, in the command configuration, simply remove the `help` text. This command can stil be
+executed, but won't appear as a command.
diff --git a/docs/installation.md b/docs/installation.md
index 8d8ce2c..b34ab7a 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -83,6 +83,8 @@ enable the plugins to make authenticated calls to their respective services. You
 required for plugins to work.
 
 1. Contact your Slack administrator to obtain a Slack token that can be used for bots.
+    - More specifically, you will need to create a Slack app, and give the app the permissions to be a bot.
+    - You will need to use the bot token for Oauth in order to get `rtm:stream` permissions.
 2. If you haven't already, create a GitHub bot account, and invite it to all the organizations that 
    you manage with the `owner` role. Also, please configure this account with a strong password
    and 2FA! (Don't forget to back up those recovery codes into a safe place!)
@@ -94,7 +96,7 @@ configuration that the plugin supports.
 
 For the GitHub plugin, you are required to define a Python `dict` with the organizations that you manage. An example
 of what this `dict` looks like can be found in the sample
-[`github/config.py`](https://github.com/Netflix/hubcommander/blob/master/github/config.py) file.
+[`command_plugins/github/config.py`](https://github.com/Netflix/hubcommander/blob/develop/command_plugins/github/config.py) file.
 
 At a minimum, you need to specify the real name of the organization, a list of aliases for the orgs (or an empty list),
 whether the organization can only create public repos (via the `public_only` boolean), as well as 
diff --git a/docs/travis_ci.md b/docs/travis_ci.md
index 85b1c35..6d6e96f 100644
--- a/docs/travis_ci.md
+++ b/docs/travis_ci.md
@@ -1,14 +1,13 @@
 Travis CI Plugin
 =================
 
-The [Travis CI](https://travis-ci.org/) plugin features an `!EnableTravis` command which will enable Travis CI 
+The [Travis CI](https://travis-ci.com/) plugin features an `!EnableTravis` command which will enable Travis CI 
 on a given repository. By default, this plugin is disabled.
 
-This plugin makes an assumption that you have Travis CI Public (for public repos) and 
-Professional (for private repos) enabled for your GitHub organizations.
+This plugin makes an assumption that you have Travis CI enabled for your GitHub organizations.
 
-This plugin will automatically detect which Travis CI (Public vs. Pro) to use based on the public/private 
-visibility of a given GitHub repository.
+Since Travis CI Public (travis-ci.org) [is moving](https://blog.travis-ci.com/2018-05-02-open-source-projects-on-travis-ci-com-with-github-apps) to Travis CI Pro (travis-ci.com), 
+the plugin will default to using Travis CI Pro.
 
 How does it work?
 ----------------
@@ -26,11 +25,9 @@ then run the API command to enable Travis CI on the repo.
 
 Configuration
 -------------
-This plugin requires access to the Travis CI API version 3 
-([currently in closed BETA](https://developer.travis-ci.org/)). You must contact Travis CI's support 
-and request a GitHub ID to be added into the beta for access to the methods utilized by this plugin. 
+This plugin requires access to [Travis CI API version 3](https://developer.travis-ci.com/). 
 
-Once you are added into the closed beta, you will need to get your Travis CI tokens. These tokens
+You will need to get your Travis CI tokens. These tokens
 are _different_ for public and professional Travis CI.
 
 ### GitHub API Token for Travis
diff --git a/hubcommander.py b/hubcommander.py
index fc98d5f..f2cc67c 100644
--- a/hubcommander.py
+++ b/hubcommander.py
@@ -108,7 +108,12 @@ def setup(slackclient):
             if cmd["enabled"]:
                 print("\t[+] Adding command: \'{cmd}\'".format(cmd=cmd["command"]))
                 COMMANDS[cmd["command"].lower()] = cmd
-                HELP_TEXT.append("`{cmd}` - {help}\n".format(cmd=cmd["command"], help=cmd["help"]))
+
+                # Hidden commands: don't show on the help:
+                if cmd.get("help"):
+                    HELP_TEXT.append("`{cmd}` - {help}\n".format(cmd=cmd["command"], help=cmd["help"]))
+                else:
+                    print("\t[!] Not adding help text for hidden command: {}".format(cmd["command"]))
             else:
                 print("\t[/] Skipping disabled command: \'{cmd}\'".format(cmd=cmd["command"]))
         print("[+] Successfully enabled command plugin \"{}\"".format(name))
diff --git a/publish_via_travis.sh b/publish_via_travis.sh
index b8d34aa..4134dff 100755
--- a/publish_via_travis.sh
+++ b/publish_via_travis.sh
@@ -20,12 +20,12 @@
 #
 ################################################################################
 
-# If this is running in Travis, AND the Python version IS NOT 3.5, then don't build
+# If this is running in Travis, AND the Python version IS NOT 3.6, then don't build
 # the Docker image:
 if [ $TRAVIS ]; then
     PYTHON_VERSION=$( python --version )
-    if [[ $PYTHON_VERSION != *"3.5"* ]]; then
-        echo "This only publishes Docker images in the Python 3.5 Travis job"
+    if [[ $PYTHON_VERSION != *"3.6"* ]]; then
+        echo "This only publishes Docker images in the Python 3.6 Travis job"
         exit 0
     fi
 else
diff --git a/setup.py b/setup.py
index 802b76f..944fa03 100644
--- a/setup.py
+++ b/setup.py
@@ -25,10 +25,10 @@
 
 install_requires = [
     'boto3>=1.4.3',     # For KMS support
-    'duo_client==3.0',
+    'duo_client>=4.0',
     'tabulate>=0.7.7',
     'validators>=0.11.1',
-    'rtmbot==0.4.0'
+    'rtmbot>=0.4.0'
 ]
 
 tests_require = [
diff --git a/tests/test_parse_functions.py b/tests/test_parse_functions.py
index 94bf346..85e42ad 100644
--- a/tests/test_parse_functions.py
+++ b/tests/test_parse_functions.py
@@ -53,6 +53,16 @@ def test_extract_repo_name():
         assert extract_repo_name(None, uri) == repo
 
 
+def test_extract_multiple_repo_names():
+    from hubcommander.bot_components.parse_functions import extract_multiple_repo_names
+
+    test_repos = ["www.foo.com", "foo", "HubCommander", "netflix.github.io"]
+    test_repo_strings = "<http://www.foo.com|www.foo.com>,foo,HubCommander," \
+                        "<https://netflix.github.io|netflix.github.io>"
+
+    assert extract_multiple_repo_names(None, test_repo_strings) == test_repos
+
+
 def test_parse_toggles():
     from hubcommander.bot_components.parse_functions import parse_toggles, TOGGLE_ON_VALUES, \
         TOGGLE_OFF_VALUES, ParseException