Slack is almost used everywhere nowadays, especially because of their bot integration called Slack App. During a hackathon day, my coworkers and I decided to start building a bot using Python that will generate a JHipster app. The workflow is pretty simple, you send a message to the bot with the JHipster's configuration you want to use. Then the bot uses JHipster Online to generate your application and replies with the GitHub repository's link.
Here is an example of a command: @hipslacker generate a microservice with mongodb named my-awesome-app
. As you can see, the message is very simple and looks like a real sentence. The reason is because we wanted to make the usage of the bot very simple and avoid having to specify all the JHipster's configuration.
All the Python code from this blog post is on the GitHub repository of HipSlacker. You can easily setup your own bot by following the instructions in the README.md file.
Main method and loop
The main method is very simple, the bot connects to your workspace and parses messages with a given interval. When a message is mentioning the bot, the method handle_command
is called and a HipSlacker object is created to process the command.
def run():
"""
Main loop, messages are read with a given interval
"""
if slack_client.rtm_connect(auto_reconnect=True):
logger.info("Bot connected")
while True:
parse_slack_output(slack_client.rtm_read())
time.sleep(constants.READ_WEBSOCKET_DELAY)
else:
logger.error("Connection failed")
def parse_slack_output(slack_rtm_output):
"""
Each message is parsed
The method 'handle_command' is called if a message is directed to the Bot
"""
if slack_rtm_output and len(slack_rtm_output) > 0:
for output in slack_rtm_output:
if output and 'text' in output and constants.AT_BOT in output['text']:
handle_command(output['text'], output['channel'], output['user'])
def handle_command(command, channel, user):
"""
Handle the command sent to the bot and process it
"""
logger.info(f"Processing command: {command}")
hipslacker = HipSlacker(slack_client, command, channel, user)
hipslacker.process_command()
Command analysis and configuration generation
The constructor of the class HipSlacker
splits the command into multiple ones and the configuration (payload) is initialized with a default one.
def __init__(self, slack_client, command, channel, user):
self.slack_client = slack_client
# take the command after bot's name
self.command = command.split(constants.AT_BOT)[1].strip().lower()
self.channel = channel
self.user = user
# get logger
self.logger = logging.getLogger("hipslacker")
# split command to commands using spaces as delimiter
self.commands = re.split("\s+", command)
# init payload with default values
self.payload = {
"generator-jhipster": {
"applicationType": "monolith",
"baseName": "my-awesome-app",
...
"clientFramework": "react",
"jhiPrefix": "jhi"
},
"git-provider": "GitHub",
"git-company": constants.JHIPSTER_ONLINE_USER,
"repository-name": "my-awesome-app"
}
self.payload_generator = self.payload["generator-jhipster"]
Then the bot parses the command's content and updates the configuration. The command can contain the application type, the name, the database type and the port. If you're familiar with JHipster, those parameters are no secret for you!
For the application and database type, the bot simply looks for keywords and updates the configuration with correct values. For the port and the name, the bot uses the command after the keyword (ex: named my-awesome-app
or port 8080
).
def generate_payload(self):
for command in self.commands:
# application type
if(command in ["monolith", "microservice", "gateway", "uaa"]):
self.set_application_type(command)
# base name
if(command == "named"):
self.set_app_name()
# sql database
if(command in ["mysql", "mariadb", "postgresql", "oracle", "mssql"]):
self.set_database("sql", "h2Disk", command)
# nosql database
if(command in ["mongodb", "cassandra"]):
self.set_database(command, command, command)
# port
if(command == "port"):
self.set_port()
# repository name
self.payload["repository-name"] = self.payload_generator["baseName"]
self.logger.info("Payload: %s", json.dumps(self.payload, indent=4))
def set_application_type(self, value):
self.payload_generator["applicationType"] = value
def set_app_name(self):
index = self.commands.index("named") + 1
if index < len(self.commands):
self.payload_generator["baseName"] = self.commands[index]
def set_database(self, db_type, dev_type, prod_type):
self.payload_generator["databaseType"] = db_type
self.payload_generator["devDatabaseType"] = dev_type
self.payload_generator["prodDatabaseType"] = prod_type
def set_port(self):
index = self.commands.index("port") + 1
if index < len(self.commands):
self.payload_generator["serverPort"] = int(self.commands[index])
Application generation with JHipster Online
To generate your JHipster application with JHipster Online, the bot needs to retrieve a JSON Web Token (JWT) and then make a POST request to https://start.jhipster.tech/api/generate-application
with a payload (which is the JHipster configuration). The library requests
is used to make HTTP calls and the implementation is pretty simple.
def get_token(self):
"""
Get a JWT using credentials
"""
data = {"password": constants.JHIPSTER_ONLINE_PWD, "username": constants.JHIPSTER_ONLINE_USER}
r = requests.post("https://start.jhipster.tech/api/authenticate", json=data)
if r.status_code != 200:
self.log_http("Error while getting the token", r)
return None
else:
return r.json()["id_token"]
def generate_application(self):
self.generate_payload()
# get token
token = self.get_token()
if token:
# start generation
headers = {"Authorization": f"Bearer {token}"}
r = requests.post("https://start.jhipster.tech/api/generate-application", data=json.dumps(self.payload), headers=headers)
# post error if generation failed
if r.status_code != 201:
self.log_http("Generation error", r)
self.post_fail_msg()
return
self.post_generation_status(r.text, token)
else:
self.post_fail_msg()
The response from the generate-application
endpoint is an id and we can use this id to get the status of the generation. That way the bot will post the status of the generation on your Slack channel to keep you updated. Then the bot will post the link of the GitHub repository if the generation succeeded or an error if the generation failed.
def post_generation_status(self, app_id, token):
"""
Get status of generation every 500ms during 1min
"""
timeout = time.time() + 60
while True:
# get status of generation
headers = {"Authorization": f"Bearer {token}"}
r = requests.get(f"https://start.jhipster.tech/api/generate-application/{app_id}", headers=headers)
# post error if getting status failed
if r.status_code != 200:
self.log_http("Unable to get generation's status", r)
self.post_with_username("error while getting generation's status :boom:")
return
# post status
self.post_msg(r.text)
# post repository's link
if "Generation finished" in r.text:
self.logger.info("Generation finished")
self.post_with_username("here the link of your application: https://github.com/hipslacker/" + self.payload["repository-name"])
return
# post error message
if "Generation failed" in r.text:
self.logger.info("Generation failed")
self.post_fail_msg()
return
# break the loop after a specific timeout
if time.time() > timeout:
self.post_with_username("the generation timed out :boom:")
return
time.sleep(0.5)
Improvements
Since JHipster Online is on GitHub, you can host your own version. That is pretty useful if you want to use a specific version of JHipster or use a private version control service instead of GitHub.
The main idea of the bot was to be user-friendly and not having to specify all the JHipster's configuration in one command. However multiple improvements can be done to the bot to extend its functionalities, here are a few examples:
- Add support for GitLab
- Extend the command parser to enable more JHipster's configuration (ex: translation, cache, authentication, etc)
- Make the bot more interactive with a question/answer pattern
Conclusion
Creating a Slack bot using Python is pretty easy and by dedicating the application's generation to JHipster Online you will avoid a lot of extra work. The main challenge is how the bot parses commands and generates a JHipster's configuration from them. That all depends on how the bot generates the configuration and on how complex a command can be. A flow with questions/answers can be used to give the user more control on the JHipster's configuration. But in this case, why not simply use JHipster Online?
Here is the GitHub repository for HipSlacker, feel free to open a pull request or maintain your own fork!