Building a GitHub Bot on Cliq

Build an extension on Zoho Cliq using Catalyst Integration Functions, that enables you to access and work with your GitHub repositories using a Cliq bot

Configure the Integration Function

We will now configure the Integration function and add code to it.

Note: We will need to code and deploy the function to the Catalyst remote console before configuring the extension on Cliq, because we have to associate the extension with an existing function from the Catalyst console.

The Integration function's directory, functions/GithubExtension, contains:

  • The MainClass.java main function file
  • Handler files in the com/handlers folder
  • The catalyst-config.json configuration file
  • Java library files in the lib folder
  • .classpath and .project dependency files

We will be coding BotHandler.java, CommandHandler.java and FunctionHandler.java in the handlers folder. We will also create a new class file GithubConstants.java in this directory and add code in it.

Note: Please go through the code in this section to make sure you fully understand it. We will discuss the code at the end of this section.

Copy the code below and paste it into the respective files located in the functions/GithubExtension/com/handlers directory of your project using an IDE and save the files.

  • View code for BotHandler.java

    Copied 
    package com.handlers;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.logging.Logger;
    
    import org.json.JSONObject;
    import org.json.JSONArray;
    import org.json.JSONException;
    
    import com.zc.cliq.enums.ACTION_TYPE;
    import com.zc.cliq.enums.BUTTON_TYPE;
    import com.zc.cliq.enums.CHANNEL_OPERATION;
    import com.zc.cliq.enums.SLIDE_TYPE;
    import com.zc.cliq.objects.Action;
    import com.zc.cliq.objects.ActionData;
    import com.zc.cliq.objects.BotDetails;
    import com.zc.cliq.objects.ButtonObject;
    import com.zc.cliq.objects.Confirm;
    import com.zc.cliq.objects.Message;
    import com.zc.cliq.objects.Slide;
    import com.zc.cliq.requests.BotContextHandlerRequest;
    import com.zc.cliq.requests.BotMentionHandlerRequest;
    import com.zc.cliq.requests.BotMenuActionHandlerRequest;
    import com.zc.cliq.requests.BotMessageHandlerRequest;
    import com.zc.cliq.requests.BotParticipationHandlerRequest;
    import com.zc.cliq.requests.BotWebhookHandlerRequest;
    import com.zc.cliq.requests.BotWelcomeHandlerRequest;
    import com.zc.cliq.util.ZCCliqUtil;
    
    public class BotHandler implements com.zc.cliq.interfaces.BotHandler
    {
    	Logger LOGGER = Logger.getLogger(BotHandler.class.getName());
    	
    	@Override
    	public Map<String,Object> messageHandler(BotMessageHandlerRequest req) throws JSONException {
    
    		String message = req.getMessage();
    		Map<String, Object> resp = new HashMap<String, Object>();
    		
    		String text;
    		if(message == null){
    			text = "Please enable 'Message' in bot settings";
    		}
    		else if(message.equalsIgnoreCase("webhooktoken")){
    			
    			Message msg = Message.getInstance("Click on the token generation button below!");
    			ButtonObject btnObj = new ButtonObject();
    			btnObj.setType(BUTTON_TYPE.GREEN_OUTLINE);
    			btnObj.setLabel("Create Webhook");
    			Action action = new Action();
    			action.setType(ACTION_TYPE.INVOKE_FUNCTION);
    			ActionData actionData = new ActionData();
    			actionData.setName("authenticate"); // ** ENTER YOUR BUTTON FUNCTION NAME HERE **
    			action.setData(actionData);
    			Confirm confirm = new Confirm();
    			confirm.setTitle("Generate Webhooks for a GitLab Project");
    			confirm.setDescription("Connect to GitLab Projects from within Cliq");
    			JSONObject input = new JSONObject();
    			input.put("type", "user_webhook_token");
    			confirm.setInput(input);
    			action.setConfirm(confirm);
    			btnObj.setAction(action);
    			
    			msg.addButton(btnObj);
    			return ZCCliqUtil.toMap(msg);
    		}
    		else{
    			text = "Sorry, I'm not programmed yet to do this :sad:";
    		}
    		
    		resp.put("text", text);
    		return resp;
    	}
    	
    	@Override
    	public Map<String, Object> menuActionHandler(BotMenuActionHandlerRequest req) throws Exception {
    		Message msg = Message.getInstance();
    		BotDetails bot = BotDetails.getInstance(GithubConstants.BOT_NAME);
    		msg.setBot(bot);
    		if(req.getActionName().equals("Repos")){
    			JSONArray reposArray = CommandHandler.getRepos();
    			if(reposArray.length() == 0){
    				msg.setText("There aren't are repos created yet.");
    			}
    			else{
    				Slide slide = Slide.getInstance();
    				msg.setText("Here's a list of the *repositories*");
    				slide.setType(SLIDE_TYPE.TABLE);
    				slide.setTitle("Repo details");
    				List<String> headers = new ArrayList<String>();
    				headers.add("Name");
    				headers.add("Private");
    				headers.add("Open Issues");
    				headers.add("Link");
    				JSONObject data = new JSONObject();
    				data.put("headers", headers);
    				
    				JSONArray rows = new JSONArray();
    				for(int i=0; i< reposArray.length(); i++){
    					JSONObject repo = reposArray.optJSONObject(i);
    					JSONObject row = new JSONObject();
    					row.put("Name", repo.optString("name"));
    					row.put("Private", repo.optBoolean("private") ? "Yes" : "No");
    					row.put("Open Issues", repo.optString("open_issues_count"));
    					row.put("Link", "[Click here](" + repo.optString("html_url") + ")");
    					
    					rows.put(row);
    				}
    				data.put("rows", rows);
    				slide.setData(data);
    				msg.addSlide(slide);
    			}
    		}				
    		else{
    			msg.setText("Menu action triggered :fist:");
    		}
    		return ZCCliqUtil.toMap(msg);
    	}
    
    	@Override
    	public Map<String, Object> webhookHandler(BotWebhookHandlerRequest req) throws Exception
    	{	
    		JSONObject reqBody = req.getBody();
    		LOGGER.info(reqBody.toString());
    		JSONObject commitJson = reqBody.optJSONArray("commits").optJSONObject(0);
    		
    		Message msg = Message.getInstance("A commit has been pushed !");
    		msg.setBot(BotDetails.getInstance(GithubConstants.BOT_NAME));
    		
    		Slide commitMsg = Slide.getInstance();
    		commitMsg.setType(SLIDE_TYPE.TEXT);
    		commitMsg.setTitle("Commit message");
    		commitMsg.setData(commitJson.optString("message"));
    		msg.addSlide(commitMsg);
    		
    		Slide details = Slide.getInstance();
    		details.setType(SLIDE_TYPE.LABEL);
    		details.setTitle("Details");
    		JSONArray dataArray = new JSONArray();
    		JSONObject committer = new JSONObject();
    		committer.put("Committer", commitJson.optJSONObject("author").optString("username"));
    		dataArray.put(committer);
    		JSONObject repoName = new JSONObject();
    		repoName.put("Repo Name", reqBody.optJSONObject("repository").optString("name"));
    		dataArray.put(repoName);
    		JSONObject timestamp = new JSONObject();
    		timestamp.put("Timestamp", commitJson.optString("timestamp"));
    		dataArray.put(timestamp);
    		JSONObject compare = new JSONObject();
    		compare.put("Compare", "[Click here](" + reqBody.optString("compare") +  ")");
    		dataArray.put(compare);
    		details.setData(dataArray);
    		msg.addSlide(details);
    		
    		return ZCCliqUtil.toMap(msg);
    	}
    
    	@Override
    	public Map<String, Object> participationHandler(BotParticipationHandlerRequest req) throws Exception
    	{
    		String text;
    		if(req.getOperation().equals(CHANNEL_OPERATION.ADDED)){
    			text = "Hi. Thanks for adding me to the channel :smile:";
    		}
    		else if(req.getOperation().equals(CHANNEL_OPERATION.REMOVED)){
    			text = "Bye-Bye :bye-bye:";
    		}
    		else{
    			text = "I'm too a participant of this chat :wink:";
    		}
    		Message msg = Message.getInstance(text);
    		return ZCCliqUtil.toMap(msg);
    	}
    	
    	@Override
    	public Map<String,Object> welcomeHandler(BotWelcomeHandlerRequest req) {
    		String uName = req.getUser() != null ? req.getUser().getFirstName() : "user";
    		String text = "Hello " + uName + ". Thank you for subscribing :smile:";
    		Message msg = Message.getInstance(text);
    		return ZCCliqUtil.toMap(msg);
    	}
    	
    	@Override
    	public Map<String, Object> contextHandler(BotContextHandlerRequest req) {
    		
    		Map<String, Object> resp = new HashMap<String, Object>();
    		if(req.getContextId().equals("personal_details")){
    			Map<String, String> answers = req.getAnswers();
    			StringBuilder str = new StringBuilder();
    			str.append("Name: ").append(answers.get("name")).append("\n");
    			str.append("Department: ").append(answers.get("dept")).append("\n");
    			
    			resp.put("text", "Nice ! I have collected your info: \n" + str.toString());
    		}
    		return resp;
    	}
    
    	@Override
    	public Map<String, Object> mentionHandler(BotMentionHandlerRequest req)
    	{
    		String text = "Hey *" + req.getUser().getFirstName() + "*, thanks for mentioning me here. I'm from Catalyst city";
    		Map<String, Object> resp = new HashMap<String, Object>();
    		resp.put("text", text);
    		return resp;
    	}
    }
    
  • View code for CommandHandler.java

    Copied 
    package com.handlers;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.json.JSONArray;
    import org.json.JSONObject;
    
    import com.zc.api.APIConstants.RequestMethod;
    import com.zc.api.APIRequest;
    import com.zc.cliq.enums.SLIDE_TYPE;
    import com.zc.cliq.objects.BotDetails;
    import com.zc.cliq.objects.CommandSuggestion;
    import com.zc.cliq.objects.Message;
    import com.zc.cliq.objects.Slide;
    import com.zc.cliq.requests.CommandHandlerRequest;
    import com.zc.cliq.util.ZCCliqUtil;
    
    import okhttp3.Response;
    
    public class CommandHandler implements com.zc.cliq.interfaces.CommandHandler
    {
    	@Override
    	public Map<String, Object> executionHandler(CommandHandlerRequest req) throws Exception{
    		
    		Message msg = Message.getInstance();
    		
    		BotDetails bot = BotDetails.getInstance(GithubConstants.BOT_NAME);
    		msg.setBot(bot);
    		String commandName = req.getName();
    		
    		if(commandName.equals("commits")){
    			List<CommandSuggestion> repoSuggestions = req.getSelections();
    			if(repoSuggestions == null || repoSuggestions.isEmpty()){
    				msg.setText("Please select a repo from the suggestions.");
    			}else{
    				String repoName = repoSuggestions.get(0).getTitle();
    				JSONArray commitsArray = getCommits(repoName);
    				if(commitsArray.length() == 0){
    					msg.setText("There aren't are commits made yet.");
    				}
    				else{
    					Slide slide = Slide.getInstance();
    					msg.setText("Here's a list of the latest " + GithubConstants.PER_PAGE + " commits made to the repository *" + repoName + "*.");
    					slide.setType(SLIDE_TYPE.TABLE);
    					slide.setTitle("Commit details");
    					List<String> headers = new ArrayList<String>();
    					headers.add("Date");
    					headers.add("Commit message");
    					headers.add("Committed by");
    					headers.add("Link");
    					JSONObject data = new JSONObject();
    					data.put("headers", headers);
    					JSONArray rows = new JSONArray();
    					
    					for(int i=0; i<commitsArray.length(); i++){
    						JSONObject obj = commitsArray.optJSONObject(i);
    						JSONObject commit = obj.optJSONObject("commit");
    						JSONObject author = commit.optJSONObject("author");
    						
    						JSONObject row = new JSONObject();
    						row.put("Date", author.optString("date"));
    						row.put("Commit message", commit.optString("message"));
    						row.put("Committed by", author.optString("name"));
    						row.put("Link", "[Click here](" + obj.optString("html_url") + ")");
    						
    						rows.put(row);
    					}
    					data.put("rows", rows);
    					slide.setData(data);
    					msg.addSlide(slide);
    				}
    				
    			}
    		}
    		else if(commandName.equals("issues")){
    			List<CommandSuggestion> repoSuggestions = req.getSelections();
    			if(repoSuggestions == null || repoSuggestions.isEmpty()){
    				msg.setText("Please select a repo from the suggestions.");
    			}else{
    				String repoName = repoSuggestions.get(0).getTitle();
    				JSONArray issuesArray = getIssues(repoName);
    				if(issuesArray.length() == 0){
    					msg.setText("There aren't are issues raised yet.");
    				}
    				else{
    					Slide slide = Slide.getInstance();
    					msg.setText("Here's a list of the latest " + GithubConstants.PER_PAGE + " issues raised to the repository *" + repoName + "*");
    					slide.setType(SLIDE_TYPE.TABLE);
    					slide.setTitle("Issue details");
    					List<String> headers = new ArrayList<String>();
    					headers.add("Created At");
    					headers.add("Title");
    					headers.add("Created By");
    					headers.add("Link");
    					JSONObject data = new JSONObject();
    					data.put("headers", headers);
    					JSONArray rows = new JSONArray();
    					
    					for(int i=0; i<issuesArray.length(); i++){
    						JSONObject issueObj = issuesArray.optJSONObject(i);
    						
    						JSONObject row = new JSONObject();
    						row.put("Created At", issueObj.optString("created_at"));
    						row.put("Title", issueObj.optString("title"));
    						row.put("Created By", issueObj.optJSONObject("user").optString("login"));
    						row.put("Link", "[Click here](" + issueObj.optString("html_url") + ")");
    						
    						rows.put(row);
    					}
    					data.put("rows", rows);
    					slide.setData(data);
    					msg.addSlide(slide);
    				}
    				
    			}
    		}
    		else{
    			msg.setText("Slash command executed");
    		}
    		
    		return ZCCliqUtil.toMap(msg);
    	}
    
    	@Override
    	public List<CommandSuggestion> suggestionHandler(CommandHandlerRequest req) throws Exception {
    		List<CommandSuggestion> suggestionList = new ArrayList<CommandSuggestion>();
    		JSONArray reposArray = getRepos();
    		List<String> repoNames = new ArrayList<>();
    		for(int i=0; i< reposArray.length(); i++){
    			JSONObject repo = reposArray.optJSONObject(i);
    			repoNames.add(repo.optString("name"));
    		}
    		if(req.getName().equals("commits") || req.getName().equals("issues")){
    			repoNames.forEach((name) -> {
    				CommandSuggestion sugg = CommandSuggestion.getInstance();
    				sugg.setTitle(name);
    				suggestionList.add(sugg);
    			});
    		}
    		return suggestionList;
    	}
    
    	public static JSONArray getRepos() throws Exception{
    		APIRequest req = getRequestObj("https://api.github.com/user/repos");
    		req.executeRequest();
    		Response resp = req.getHttpResponse();
    		JSONArray reposArray = new JSONArray(resp.body().string());
    		
    		return reposArray;
    	}
    	
    	private String getUsername() throws Exception{
    		APIRequest req = getRequestObj("https://api.github.com/user");
    		JSONObject respJson = new JSONObject(req.getResponse().getResponseJSON().get(0).toString());
    		return respJson.get("login").toString();
    	}
    	
    	private JSONArray getCommits(String repoName) throws Exception{
    		APIRequest req = getRequestObj("https://api.github.com/repos/" + getUsername() + "/" + repoName + "/commits?per_page=" + GithubConstants.PER_PAGE);
    		req.executeRequest();
    		Response resp = req.getHttpResponse();
    		JSONArray commitsArray = new JSONArray(resp.body().string());
    		return commitsArray;
    	}
    	
    	private JSONArray getIssues(String repoName) throws Exception{
    		APIRequest req = getRequestObj("https://api.github.com/repos/" + getUsername() + "/" + repoName + "/issues?per_page=" + GithubConstants.PER_PAGE);
    		req.executeRequest();
    		Response resp = req.getHttpResponse();
    		JSONArray issuesArray = new JSONArray(resp.body().string());
    		return issuesArray;
    	}
    	
    	private static APIRequest getRequestObj(String url)
    	{
    		APIRequest req = new APIRequest();
    		req.setUrl(url);
    		req.setRequestMethod(RequestMethod.GET);
    		req.setAuthNeeded(false);
    		HashMap<String, String> headers = new HashMap<>();
    		headers.put("Authorization", "Token " + GithubConstants.PERSONAL_ACCESS_TOKEN);
    		req.setHeaders(headers);
    		return req;
    	}
    }
    
  • View code for FunctionHandler.java

    Copied 
    package com.handlers;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import com.zc.cliq.objects.FormChangeResponse;
    import com.zc.cliq.objects.FormDynamicFieldResponse;
    import com.zc.cliq.objects.Message;
    import com.zc.cliq.objects.WidgetSection;
    import com.zc.cliq.requests.ButtonFunctionRequest;
    import com.zc.cliq.requests.FormFunctionRequest;
    import com.zc.cliq.requests.WidgetFunctionRequest;
    import com.zc.cliq.util.ZCCliqUtil;
    
    public class FunctionHandler implements com.zc.cliq.interfaces.FunctionHandler
    {
    
    	@SuppressWarnings("unchecked")
    	@Override
    	public Map<String, Object> buttonFunctionHandler(ButtonFunctionRequest req) throws Exception
    	{
    		String text;
    		if(req.getName().equals("authenticate")){
    			text = ((HashMap<String,Object>)req.getArguments().get("input")).get("token").toString();
    		}
    		else{
    			text = "Button function executed";
    		}
    		Message msg = Message.getInstance(text);
    		return ZCCliqUtil.toMap(msg);
    	}
    	
    	@Override
    	public Map<String, Object> formSubmitHandler(FormFunctionRequest req) throws Exception 
    	{
    		return new HashMap<>();
    	}
    
    	@Override
    	public FormChangeResponse formChangeHandler(FormFunctionRequest req) throws Exception
    	{
    		FormChangeResponse resp = FormChangeResponse.getInstance();
    		return resp;
    	}
    
    	@Override
    	public FormDynamicFieldResponse formDynamicFieldHandler(FormFunctionRequest req) throws Exception
    	{
    		FormDynamicFieldResponse resp = FormDynamicFieldResponse.getInstance();
    		return resp;
    	}
    
    	@Override
    	public Map<String, Object> widgetButtonHandler(WidgetFunctionRequest req) throws Exception
    	{
    		return new HashMap<>();
    	}
    
    	private WidgetSection getButtonsSection()
    	{
    		WidgetSection buttonSection = WidgetSection.getInstance();
    		return buttonSection;
    	}
    }
    

Next, create a new Java class file in the handlers/directory and name it GithubConstants.java. Add the code given below to the file.

  • View code for GithubConstants.java

    Copied 
    //$Id$
    package com.handlers;
    
    public class GithubConstants
    {
    	public static final String PERSONAL_ACCESS_TOKEN = "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";
    	public static final Integer PER_PAGE = 10;
    	public static final String BOT_NAME = "github-bot";
    }
    
Note: After you copy and paste this code, paste the personal access token that you obtained from GitHub in Step 2 on line 6 in this code.

The functions directory is now configured.

Let's quickly go over the code of the handler classes. We will discuss the components specified here in detail while configuring the extension.

  • The BotHandler.java class contains the business logic of the GitHub bot component that we will configure in the Cliq extension. After you enable the chat bot, you can generate a webhook token to authenticate the connection to GitHub, list the details of your Git repositories, and obtain the details of the latest commits and issues in a repository. These actions are defined in this class.
  • The CommandHandler.java class handles the actions to be performed for the issues and commits commands. It also defines the suggestion handler that lists the repos of the Git account. The user must select a repo from the suggestion after entering the command, to execute the command for. CommandHandler.java also defines the APIs for fetching other information from the Git account.
  • The FunctionHandler.java class contains the business logic of the button function that authenticates the webhook token for GitHub.
  • The GitHubConstants.java class file stores the constant values of the function, such as the personal access token and the bot name.