How to integrate SUSI.AI with Google Sign In API

Google Sign-In manages the OAuth 2.0 flow and token lifecycle, simplifying our integration with Google APIs. A user always has the option to revoke access to an application at any time. Users can see a list of all the apps which they have given permission to access their account details and are using the login API. In this blog post I will discuss how to integrate this API with susi server API to enhance our AAA system. More on Google API here.

Continue ReadingHow to integrate SUSI.AI with Google Sign In API

Adding Support for Displaying Images in SUSI iOS

SUSI provided exciting features in the chat screen. The user can ask a different type of queries and SUSI respond according to the user query. If the user wants to get news, the user can just ask news and SUSI respond with the news. Like news, SUSI can respond to so many queries. Even user can ask SUSI for the image of anything. In response, SUSI displays the image that the user asked. Exciting? Let’s see how displaying images in the chat screen of SUSI iOS is implemented.

Getting image URL from server side –

When we ask susi for the image of anything, it returns the URL of image source in response with answer action type. We get the URL of the image in the expression key of actions object as below:

actions:
[
{
language: "en",
type: "answer",
expression:
"https://pixabay.com/get/e835b60f2cf6033ed1584d05fb1d4790e076e7d610ac104496f1c279a0e8b0ba_640.jpg"
}
]

 

Implementation in App –

In the app, we check if the response coming from server is image URL or not by following two methods.

One – Check if the expression is a valid URL:

func isValidURL() -> Bool {
if let url = URL(string: self) {
return UIApplication.shared.canOpenURL(url)
}
return false
}

The method above return boolean value if the expression is valid or not.

Two – Check if the expression contains image source in URL:

func isImage() -> Bool {
let imageFormats = ["jpg", "jpeg", "png", "gif"]

if let ext = self.getExtension() {
return imageFormats.contains(ext)
}

return false
}

The method above returns the boolean value if the URL string is image source of not by checking its extension.

If both the methods return true, the expression is a valid URL and contain image source, then we consider it as an Image and proceed further.

In collectionView of the chat screen, we register ImageCell and present the cell if the response is the image as below.

Registering the Cell –

register(_:forCellWithReuseIdentifier:) method registers a class for use in creating new collection view cells.

collectionView?.register(ImageCell.self, forCellWithReuseIdentifier: ControllerConstants.imageCell)

Presenting the Cell using cellForItemAt method of UICollectionView

The implementation of cellForItemAt method is responsible for creating, configuring, and returning the appropriate cell for the given item. We do this by calling the dequeueReusableCell(withReuseIdentifier:for:) method of the collection view and passing the reuse identifier that corresponds to the cell type we want. That method always returns a valid cell object. Upon receiving the cell, we set any properties that correspond to the data of the corresponding item, perform any additional needed configuration, and return the cell.

if let expression = message.answerData?.expression, expression.isValidURL(), expression.isImage() {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ControllerConstants.imageCell, for: indexPath) as? ImageCell {
cell.message = message
return cell
}
}

In ImageCell, we present a UIImageView that display the image. When cell the message var, it call downloadImage method to catch and display image from server URL using Kingfisher method.

In method below –

  1. Get image URL string and check if it is image URL
  2. Convert image string to image URL
  3. Set image to the imageView
func downloadImage() {
if let imageString = message?.answerData?.expression, imageString.isImage() {
if let url = URL(string: imageString) {
imageView.kf.setImage(with: url, placeholder: ControllerConstants.Images.placeholder, options: nil, progressBlock: nil, completionHandler: nil)
}
}
}

We have added a tap gesture to the imageView so that when the user taps the image, it opens the full image in the browser by using the method below:

@objc func openImage() {
if let imageURL = message?.answerData?.expression {
if let url = URL(string: imageURL) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}

 

Final Output –

Resources –

  1. Apple’s Documentations on collectionView(_:cellForItemAt:) method
  2. Apple’s Documentations on register(_:forCellWithReuseIdentifier:) method
  3. SUSI iOS Link: https://github.com/fossasia/susi_iOS
  4. SUSI API Link: http://api.susi.ai/
  5. Kingfisher – A lightweight, pure-Swift library for downloading and caching images from the web
Continue ReadingAdding Support for Displaying Images in SUSI iOS

Demystifying a travis.yml file

In this blog post, we are going to demystify a travis.yml file. For reference, we will be using the travis configuration used in the  SUSI skill CMS project. But before delving into it, I would like to just give a brief introduction about Continuous Integration (CI) and Travis CI.

What is CI and Travis ?

Continuous Integration represents the practice of integrating a piece of work as early as possible instead of later so that one can receive immediate and frequent feedback on things that are wrong.

“ Continuous Integration is the practice of merging all developer working copies to a shared mainline several times a day.

-Wikipedia

Travis CI is a hosted continuous integration platform that is free for all open source projects hosted on Github. With just a file called .travis.yml containing some information about our project, we can trigger automated builds with every change to our code base in the master branch, other branches or even a pull request.

sudo: required
dist: trusty
language: node_js

node_js:
  - 6

script:
  - npm test

after_success:
 - bash ./pr_deploy.sh
 - bash ./deploy.sh

cache:
  directories:
    - node_modules

branches:
  only:
    - master

Travis configuration file of susi_skill_cms

Part-wise explanation of the file

  • When specifying sudo: required, Travis CI runs each build in an isolated Google Compute Engine virtual machine that offer a vanilla build environment for every build. This has the advantage that no state persists between builds, offering a clean slate and making sure that the tests run in an environment built from scratch. Builds have access to a variety of services for data storage and messaging, and can install anything that’s required for them to run.
  • The keyword dist represents the virtualization environment that is being used. trusty here refers to the distribution of the Linux environment used.
  • The keyword language represents the programming language that is be used for the project. The language used for the project is nodeJs.
  • Then follows the version details of node_js that is to be used. The node_js version used is 6.
  • This is the step where Travis runs the test script. Unless otherwise specified, it runs the default for the set language. In the case of node, it does node_js. The script stands for the build script that would be executed during the build process. The default build script for nodeJs is npm test. The result of execution of npm test can be found from the package.json file. It executes the npm run lint && react-scripts test –env=jsdom command, which is responsible for checking the linting issues and runs various unit and snapshot tests. We can add multiple lines of command to be executed.
  • The after_success block runs after the entire script is done. It’s the last step in the normal build process has been executed successfully. It has 2 commands to be executed –
    • bash ./pr_deploy.sh : Responsible for making a surge deployment of each PR
    • bash ./deploy.sh : Responsible for making a deployment to master branch
  • Travis CI can cache content that does not often change, to speed up the build process. The cache setting caches the node_modules directory, without the need to install the dependencies repeatedly.
  • We can specify certain branches to run, either by specifying a white list (using only keyword) or a black list (using except keyword). Here, the configuration mentions to run the build only for PRs to master branch.

Resources

Continue ReadingDemystifying a travis.yml file

Persisting Cookies over SUSI.AI Subdomains

In this blog post, we are going to see, how the cookies are persisted for all the subdomains of SUSI.AI. By this implementation, the session for the user is maintained over all the SUSI.AI websites.

The cookies are persisted over these SUSI.AI websites –

All the web clients are developed using ReactJs framework and for the manipulation of cookies in React, a npm package – universal-cookie is used. Firstly, we will see how the cookies are set/created during login, followed by removal of cookies during logout.

Creating cookies

  • Firstly, we need to import the universal-cookie package into the component and create an instance of it.
import Cookies from 'universal-cookie';
const cookies = new Cookies();

Now, we can set a new cookie using this instance of cookies.

  • The following snippet sets the cookies after the login is done.

// AJAX call for login

let email = this.state.email.trim();
$.ajax({
  url: loginEndpoint,
  dataType: 'jsonp',
  jsonp: 'callback',
  crossDomain: true,
  success: function(response) {
  if (response.accepted) {
    cookies.set('serverUrl', BASE_URL, {
      path: '/',
      domain: '.susi.ai',
    });
    let accessToken = response.access_token;
    let state = this.state;
    let time = response.valid_seconds;
    this.handleOnSubmit(email, accessToken, time);
  }.bind(this),
  error: function(errorThrown) {
    .
    .
    .
  }.bind(this)    
});

handleOnSubmit = (email, loggedIn, showAdmin, time) => {
  let state = this.state;
  if (state.success) {
    cookies.set('loggedIn', loggedIn, {
      path: '/',
      maxAge: time,
      domain: '.susi.ai',
    });
    cookies.set('emailId', this.state.email, {
      path: '/',
      maxAge: time,
      domain: '.susi.ai',
    });
    this.props.history.push('/', { showLogin: false });
    window.location.reload();
  } else {
    this.setState({
      error: true,
      accessToken: '',
      success: false,
    });
  }
};

 

  • The cookies.set is a function provided by the package, that takes in three (3) parameters –
    • Cookie name
    • Cookie vallue
    • Options – an object containing the cookies properties
  • In the above example, say setting the loggedIn cookie, that contains the access token. We set the cookie name as loggedIn, the cookie value equal to the access token value received from the server response.
  • Apart from that, we have set 3 properties of the cookies, by passing an optional options parameter to the set function.
    • path – It indicates a URL path that must exist in the requested URL in order to send the Cookie header.
    • domainIt specifies allowed hosts to receive the cookie. If unspecified, it defaults to the host of the current document location, excluding subdomains.
    • maxAgeIt specifies a time duration after which the cookie gets expired.

Deleting cookies

  • It is mainly used, when a user wants to logout. It is used in the Logout component of the client’s codebase.
  • An approach to delete/remove a cookie is to set the expiry date of the cookie as Thu, 01 Jan 1970 00:00:01 GMT, which results in the removal of the cookie after a page refresh.
  • Following is the code snippet of how the cookies are removed to log-out a user of the website.

.
.
.
let deleteCookie = function(name, options = {}) {
  let cookieString = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
  if (options.domain) {
    cookieString = `${cookieString}domain=${options.domain};`;
  }
  if (options.path) {
    cookieString = `${cookieString}path=${options.path};`;
  }
  document.cookie = cookieString;
};
.
.
.
.
deleteCookie('loggedIn', { domain: '.susi.ai', path: '/' });
deleteCookie('serverUrl', { domain: '.susi.ai', path: '/' });
deleteCookie('emailId', { domain: '.susi.ai', path: '/' });
deleteCookie('showAdmin', { domain: '.susi.ai', path: '/' });
deleteCookie('username', { domain: '.susi.ai', path: '/' });

 

  • The deleteCookie function takes in 2 params –
    • Cookie name
    • options – an object containing the cookies properties
  • The options parameter needs to be passed while deleting the cookie, as it defines the scope for which the cookie has to be deleted.
  • The function creates a string and appends to it the expiry date, path, domain to the cookie name, if provided.
  • Finally, it sets the cookie by assigning the string to the document object.

Resources

Continue ReadingPersisting Cookies over SUSI.AI Subdomains

Using SUSI.AI Accounting Model to store Device information on Server

Just like with Google Home devices and Amazon Alexa devices, SUSI.AI Users should have a way to add and store the information of their devices (Smart Speakers) on SUSI Server, so that it could be displayed to them on various clients. Hence, we needed a Servlet which could add and store the User data on the Server.

Implementation of Servlets in SUSI.AI

All servlets in SUSI extend AbstractAPIHandler class and implement APIHandler. All servlets have 4 methods, which we overwrite depending on what we want the servlet to do. They are as follows :

@Override
    public String getAPIPath() {
        return null;
    }

@Override
    public BaseUserRole getMinimalBaseUserRole() {
        return null;
    }

@Override
    public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
        return null;
    }

@Override
    public ServiceResponse serviceImpl(Query post, HttpServletResponse response, Authorization rights, JsonObjectWithDefault permissions) throws APIException {
        Return null;
    }

 

How these 4 methods work together?

  1. First method is getAPIPath(). It returns the endpoint of the servlet.
  2. The second method is getMinimalBaseUserRole(). It returns the minimum privilege level required to access the endpoint.
  3. The third method is getDefaultPermissions(). It gets the Default Permissions of a UserRole in SUSI Server. Different UserRoles have different permissions defined in SUSI Server.
  4. Whenever the endpoint defined in the getAPIPath() method is called properly, it responds with whatever is defined in the fourth method, which is serviceImpl().

How is User data stored on SUSI Server?

Before we move on to the actual implementation of the API required to store the device information, we should get a brief idea of how exactly User data is stored on SUSI Server.

Every SUSI User has an Accounting Object. It is a JSONObject which stores the Settings of the particular User on the Server. For example, every time you change some setting on the Web Client https://chat.susi.ai, an API call is made to aaa/changeUserSettings.json with appropriate parameters with information of the changed setting, and the User settings are stored to the Server in the Accounting Object of the User.

Implementation of a Servlet to store Device information on Server

The task of this servlet is to store the information of a new device (Smart speaker) whenever it is initially set up using the Android/iOS app.

This is the implementation of the 4 methods of a servlet which is used to store information of connected devices:

    @Override
    public String getAPIPath() {
        return "/aaa/addNewDevice.json";
    }

    @Override
    public UserRole getMinimalUserRole() {
        return UserRole.USER;
    }

    @Override
    public JSONObject getDefaultPermissions(UserRole baseUserRole) {
        return null;
    }

    @Override
   public ServiceResponse serviceImpl(Query query, HttpServletResponse response, Authorization authorization, JsonObjectWithDefault permissions) throws APIException {

           JSONObject value = new JSONObject();
           
           String key = query.get("macid", null);
           String name = query.get("name", null);
           String device = query.get("device", null);
               
           if (key == null || name == null || device == null) {
               throw new APIException(400, "Bad service call, missing arguments");
           } else {
               value.put(name, device);
           }
           
           if (authorization.getIdentity() == null) {
               throw new APIException(400, "Specified user data not found, ensure you are logged in");
           } 
           else {
                Accounting accounting = DAO.getAccounting(authorization.getIdentity());
                if (accounting.getJSON().has("devices")) {
                       accounting.getJSON().getJSONObject("devices").put(key, value);
                   } 
                else {
                       JSONObject jsonObject = new JSONObject();
                       jsonObject.put(key, value);
                       accounting.getJSON().put("devices", jsonObject);
                   }
               
               JSONObject result = new JSONObject(true);
               result.put("accepted", true);
               result.put("message", "You have successfully added the device!");
               return new ServiceResponse(result);
           }
    }

 

As it can be seen from the above code, the endpoint for this servlet is /aaa/addNewDevice.json and it accepts 3 parameters –

  • macid : Mac Address of the device
  • name : Name of the device
  • device : Additional information which you want to send about the device (subject to changes)

As the main task of this servlet is user specific, and should only be accessible to the particular user, hence we returned UserRole as USER in the getMinimalUserRole() method.

In the serviceImpl() method, first we extract the value of the URL query parameters and store them in variables. If any of the parameters is missing, we display an error response code of 400 with error message “Bad service call, missing arguments”. If query parameters are fine, we store the name and device values in a new JSONObject, value in this case.

An if-else statement then checks for whether the User is logged in or not, using the authorization.getIdentity(), which is a function which returns the identity of the User. The implementation of this function is in Accounting.java file, and is as follows :

    public ClientIdentity getIdentity() {
        return identity;
    }

 

If the User is logged in, getAccounting() function of DAO.java file is called which returns an Accounting object according to the following implementation of the function :

    public static Accounting getAccounting(@Nonnull ClientIdentity identity) {
        return new Accounting(identity, accounting);
    }

 

Our device information is then stored in this Accounting object in the following format :

{
  "lastLoginIP": "162.158.166.19",
  "accepted": true,
  "message": "Success: Showing user data",
  "session": {"identity": {
    "type": "email",
    "name": "myemail@gmail.com",
    "anonymous": false
  }},
  "settings": {
    "customThemeValue": "4285f4,f5f4f6,fff,f5f4f6,fff,4285f4,",
    "theme": "light"
  },
  "devices": {
    "MacID2": {"MyDevice": "SmartSpeaker"},
    "MacID1": {"Name of device": "SmartSpeaker"}
  }
}

 

Resources

Continue ReadingUsing SUSI.AI Accounting Model to store Device information on Server

Displaying Skills Feedback on SUSI iOS

SUSI allows the user to rate the SUSI Skills with the five-star rating system. SUSI offer a good feedback system where the user can post feedback to any skill by using iOS, Android, and Web clients. In Skill Detail, there is a skill feedback text field where the user can write feedback about SUSI Skill. We display the users posted feedbacks on Skill Detail screen. In this post, we will see how the displaying skills feedback feature implemented on SUSI iOS.

Implementation –

We are displaying three feedback on Skill Detail screen, to see all feedback, there is a “See All Review” option, by clicking user is directed to a new screen where he/she can see all feedback related to particular skill.

We use the endpoint below for getting skill feedback from server side –

https://api.susi.ai/cms/getSkillFeedback.json

With the following params:

  • Model
  • Group
  • Language
  • Skill Name

The API endpoint above return the all the feedback array related to particular susi skill. We store feedbacks in an array of Feedback object, which holds three value:

    • Feedback String – Feedback string posted by the user
    • Email – Email address of feedback poster user
    • Time Stamp – Time of posting feedback
class Feedback: NSObject {
var feedbackString: String = ""
var email: String = ""
var timeStamp: String = ""
...
}

To display feedbacks, we are using UITableView with two prototype cells, one for feedbacks and one for “See All Review” option.

There can be different cases eg. when the total number of feedback for skill is less than three or three. When the feedback count is three or less than three, there is no need to show “See All Review” option. Also, tableView height is different for different feedback count. For varying tableView height, we have created an outlet for tableView height constraints and vary accordingly.

@IBOutlet weak var feedbackTableHeighConstraint: NSLayoutConstraint!

Now, let’s see how the number of cells, height for cells and different cells presented according to feedback count with UITableViewDelegate and UITableViewDataSource methods.

Handling number of tableView rows –

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let feedbacks = feedbacks, feedbacks.count > 0 else {
feedbackTableHeighConstraint.constant = 0.0
return 0
}
if feedbacks.count < 4 {
feedbackTableHeighConstraint.constant = CGFloat(72 * feedbacks.count)
return feedbacks.count
} else {
feedbackTableHeighConstraint.constant = 260.0
return 4
}
}

Where feedbacks is the array of Feedback object which holds the feedbacks we are getting from the server side for a skill.

var feedbacks: [Feedback]?

In the above method, we see that how we are handling the number of cells case. Now let’s see how to handle which cells to be present on basis of the number of cells case –

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let feedbacks = feedbacks, feedbacks.count > 0 else {
feedbackTableHeighConstraint.constant = 0.0
return UITableViewCell()
}
if feedbacks.count < 4 {
if let cell = tableView.dequeueReusableCell(withIdentifier: "feedbackDisplayCell", for: indexPath) as? FeedbackDisplayCell {
cell.feedback = feedbacks[indexPath.row - Int(indexPath.row/2)]
return cell
}
} else if feedbacks.count > 3 {
if indexPath.row == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "allFeedbackCell", for: indexPath)
return cell
} else {
if let cell = tableView.dequeueReusableCell(withIdentifier: "feedbackDisplayCell", for: indexPath) as? FeedbackDisplayCell {
cell.feedback = feedbacks[indexPath.row]
return cell
}
}
}
return UITableViewCell()
}

If the number of feedbacks is greater than three than we provide “See All Review” option to the user to see all the feedback related to skill. We are displaying all feedbacks using UITableViewController. When the user clicks the “See All Review” option, we pass the feedbacks (Array of all the feedback) to new UITableViewController. By passing feedbacks, we are reducing one network call.

let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let allFeedbackVC = storyboard.instantiateViewController(withIdentifier: "AllFeedbackController") as? AllFeedbackViewController {
allFeedbackVC.feedbacks = self.feedbacks
let nvc = AppNavigationController(rootViewController: allFeedbackVC)
self.present(nvc, animated: true, completion: nil)
}

On all skill feedback screen, we are displaying the full review. For the different size of text, we are setting the different size of cell size by using the method below:

if let feedbacks = feedbacks {
let estimatedLabelHeight = UILabel().heightForLabel(text: feedbacks[indexPath.row].feedbackString, font: UIFont.systemFont(ofSize: 14.0), width: 250.0)
return 64 + estimatedLabelHeight
} else {
return 80
}

 

Final Output –

Resources –

  1. SUSI API Link: https://api.susi.ai/
  2. SUSI iOS Link: https://github.com/fossasia/susi_iOS
  3. Apple’s Documentation on UITableViewDelegate: https://developer.apple.com/documentation/uikit/uitableviewdelegate?changes=_6
  4. Apple’s Documentation on UITableViewDataSource: https://developer.apple.com/documentation/uikit/uitableviewdatasource
Continue ReadingDisplaying Skills Feedback on SUSI iOS

Fetching Skills using the ID of a user

The Dashboard on the SUSI Skill CMS is the page where all user specific Skill information is being displayed. It also has a section called “My Skills” which displays the Skills created by the user himself. The data for the “My Skills” section has to be fetched from the Skill metadata of each Skill. Hence, this blog post explains how the Skills created by an author are fetched using an API which takes author’s email as the only parameter.

Why is such an API required?

The task of displaying the “My Skills” section cannot be done entirely on the Client side with the use of existing APIs, as that would result in a lot of AJAX calls to the Server, because for each Skill, there would have to be an AJAX call for each user. Hence, we had to create an endpoint which could directly return the list of Skills created by a user using his ID. This would prevent multiple AJAX calls to the Server.

The endpoint used is:

"https://api.susi.ai/cms/getSkillsByAuthor.json?author_email="+author_email

 

Here, author_email is the email of the author who published the particular Skill, which is fetched from the Skill metadata object which looks like this:

"Antonyms": {
      // skill metadata
      "author_email": akjn11@gmail.com,
      // skill metadata
    }

Working of the API

The code to fetch the Skills created by an author is described in the GetSkillsByAuthor.java file. This API accepts either the author’s name, or the author’s email as the parameter. In our

We get all folders within a folder using the following method:

private void listFoldersForFolder(final File folder, ArrayList<String> fileList) {
        File[] filesInFolder = folder.listFiles();
        if (filesInFolder != null) {
            Arrays.stream(filesInFolder).filter(fileEntry -> fileEntry.isDirectory() && !fileEntry.getName().startsWith(".")).forEach(fileEntry->fileList.add(fileEntry.getName() + ""));
        }
    }

 

We use this method to get list of folders of all the group names for all the different Skills, and store them in a variable ‘folderList’ as per the following code:

File allGroup = new File(String.valueOf(model));
ArrayList<String> folderList = new ArrayList<String>();
listFoldersForFolder(allGroup, folderList);

 

For every folder corresponding to a group name, we traverse inside it and then further traverse inside the language folders to reach the place where all the Skill files are stored.

This is the code for the method to get all files within a folder:

private void listFilesForFolder(final File folder, ArrayList<String> fileList) {
    File[] filesInFolder = folder.listFiles();
    if (filesInFolder != null) {
        Arrays.stream(filesInFolder).filter(fileEntry -> !fileEntry.isDirectory() && !fileEntry.getName().startsWith(".")).forEach(fileEntry->fileList.add(fileEntry.getName() + ""));
    }
}

 

We need to get all Skill files inside the ‘languages’ folder, which itself is in ‘group’ folder. This is the code required to implement the same:

for (String temp_group_name : folderList){
        File group = new File(model, temp_group_name);
        File language = new File(group, language_name);
        ArrayList<String> fileList = new ArrayList<String>();
        listFilesForFolder(language, fileList);

        for (String skill_name : fileList) {
               // get skill metadata of each skill iteratively and put it in result JSONObject accordingly
            }
         }
    }

 

This is how the Skill metadata is fetched from the getSkillMetadata() method of SusiSkill class:

skill_name = skill_name.replace(".txt", "");
            JSONObject skillMetadata = SusiSkill.getSkillMetadata(model_name, temp_group_name, language_name, skill_name);

            if(skillMetadata.get("author_email").equals(author_email)) {
                skillMetadata.put("skill_name", skill_name);
                authorSkills.put(skillMetadata);

 

getSkillMetadata() method takes model of the Skill, group of the Skill, language of the Skill, Skill name and also duration (for usage statistical purposes) as the method parameters, and returns the metadata for the Skill in the format as shown below:

   "Antonyms": {
      "model": "general",
      "group": "Knowledge",
      "language": "en",
      "developer_privacy_policy": null,
      "descriptions": "A skill that returns the antonyms for a word",
      "image": "images/antonyms.png",
      "author": "akshat jain",
      "author_url": "https://github.com/Akshat-Jain",
      "author_email": akjn11@gmail.com,
      "skill_name": "Antonyms",
      "protected": false,
      "terms_of_use": null,
      "dynamic_content": true,
      "examples": ["Antonym of happy"],
      "skill_rating": {
        "negative": "0",
        "bookmark_count": 0,
        "positive": "0",
        "stars": {
          "one_star": 0,
          "four_star": 0,
          "five_star": 0,
          "total_star": 0,
          "three_star": 0,
          "avg_star": 0,
          "two_star": 0
        },
        "feedback_count": 0
      },
      "usage_count": 0,
      "skill_tag": "Antonyms",
      "creationTime": "2018-05-06T03:05:22Z",
      "lastAccessTime": "2018-06-23T18:48:36Z",
      "lastModifiedTime": "2018-05-06T03:05:22Z"
    }

 

This is how the Skills created by an author are fetched using this API. The JSON response of this endpoint shows the metadata of all Skills which have ‘author_email’ value the same as that given in the endpoint’s query parameter.

Resources

Continue ReadingFetching Skills using the ID of a user

Implementing API to Fetch all Ratings by a User on Different Skills on SUSI.AI Web Client

SUSI Skill CMS allows the users to rate any Skill on a scale of 1 to 5. The user can also provide a feedback to any Skill. This paves the path to implementing a Dashboard, which has all the analytic data of the user. Hence, an API needed to be implemented which could return the ratings done by a particular user on all different Skills.

How are servlets implemented in SUSI.AI?

All servlets in SUSI extend AbstractAPIHandler class and implement APIHandler. All servlets have 4 methods, which we overwrite depending on what we want the servlet to do. They are as follows :

    @Override
    public String getAPIPath() {
        return null;
    }

    @Override
    public BaseUserRole getMinimalBaseUserRole() {
        return null;
    }

    @Override
    public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
        return null;
    }

    @Override
    public ServiceResponse serviceImpl(Query post, HttpServletResponse response, Authorization rights, JsonObjectWithDefault permissions) throws APIException {
        return null;
    }

 

How these 4 methods work together?

  • First method is getAPIPath(). It returns the endpoint of the servlet.
  • The second method is getMinimalBaseUserRole(). It returns the minimum privilege level required to access the endpoint.
  • The third method is getDefaultPermissions(). It gets the Default Permissions of a UserRole in SUSI Server. Different UserRoles have different permissions defined in SUSI Server.
  • Whenever the endpoint defined in the getAPIPath() method is called properly, it responds with whatever is defined in the fourth method, which is serviceImpl().

Implementing a servlet to fetch all ratings by a user on different Skills

The task of this servlet is to fetch all the ratings done by a user on all the different Skills, so that this fetched data could be used later on for implementation of various user specific features like Dashboard page. This is the implementation of the 4 methods of this servlet:

    @Override
    public UserRole getMinimalUserRole() {
        return UserRole.USER;
    }

    @Override
    public JSONObject getDefaultPermissions(UserRole baseUserRole) {
        return null;
    }

    @Override
    public String getAPIPath() {
        return "/cms/getProfileDetails.json";
    }

    @Override
    public ServiceResp onse serviceImpl(Query query, HttpServletResponse response, Authorization authorization, final JsonObjectWithDefault permissions) throws APIException {

    JsonTray fiveStarSkillRating = DAO.fiveStarSkillRating;
    // JSONObject and JsonArray Declarations

    // Checking if access_token has been given and is valid
    if (authorization.getIdentity() == null) {
        throw new APIException(400, "Specified user data not found, ensure you are logged in");
    } 


    // Fetching email of the user from access token and storing it in a string
    String email = authorization.getIdentity().getName();


    // Iterating over the fiveStarSkillRating JsonTray by extracting keys at every level and accessing their child objects through the extracted keys
    for(String model_name : fiveStarSkillRating.keys())
    {
        // Storing the list of group names and iterating over them
        for(String group_name : groupnameKeysList)
        {
            // Storing the list of language names and iterating over them
            for(String language_name : languagenameKeysList)
            {
                // Storing the list of skill names and iterating over them
                for(String skill_name : skillnamesKeysList)
                {
                    skillnameArray = languageObject.getJSONArray(skill_name);
                    // Iterating over the different Skill JSONObjects
                    for(int i=0; i<skillnameArray.length(); i++) {
                        String jsonEmail = skillnameArray.getJSONObject(i).get("email").toString();
                        if(jsonEmail.equals(email)) {
                        // If the Skill has been rated by the required author, then put the Skill JSONObject into the result array
                        }
                    }
                }
            }
        }
    }

    if(result.length()==0) {
        // Handling case when user hasn’t rated any Skill yet
        result.put("accepted", false);
        result.put("message", "User has not rated any Skills yet.");
        return new ServiceResponse(result);
    }
    result.put("accepted", true);
    result.put("message", "User ratings fetched.");
    return new ServiceResponse(result);
    }

 

As it can be seen from the above code, the endpoint for this servlet is /cms/getProfileDetails.json and it requires 1 parameter – the access token of the user.

As the main task of this servlet is user specific, and should only be accessible to the particular user, hence we returned UserRole as USER in the getMinimalUserRole() method.

In the serviceImpl() method, we iterate over the fiveStarSkillRating.json JsonTray and keep on extracting keys and accessing the corresponding JSONObjects until we reach the lowermost layer, where all the Skill names are listed. Iterating over the JSONObjects corresponding to each Skill, we check the email in the Skill JSONObject to identify the user. If the email present in the Skill JSONObject matches with the email corresponding to the access token provided as a query parameter, we extract rating and the timestamp for the Skill and put it in a JSONArray. Then, finally we put the JSONArray into our resulting JSONObject.

This is how we can fetch all the ratings by a user on all the different Skills.

Resources

 

Continue ReadingImplementing API to Fetch all Ratings by a User on Different Skills on SUSI.AI Web Client

Allowing user to submit ratings for skills in SUSI iOS

Rating is a great way to get feedback from the user. Generally, the 5-Star rating system used to get feedback. The Five-Star Quality Rating System was developed as an easy-to-understand rating system for users.

In SUSI iOS we are using the Star Rating field to get feedback of SUSI skills. We enable the user to rate the skills from one to five star. In the rating submission, we get the number of stars user picked.

The stars help show if you would recommend the skill to others. The values start at 1 (being the lowest) and go to 5 (being the highest), as seen below –

Server-Side Implementation –

fiveStarRatings API is using to submit users rating. Whenever the user taps the star fiveStarRatings being called:

func submitRating(_ params: [String: AnyObject], _ completion: @escaping(_ updatedRatings: Ratings?, _ success: Bool, _ error: String?) -> Void) {
let url = getApiUrl(UserDefaults.standard.object(forKey: ControllerConstants.UserDefaultsKeys.ipAddress) as! String, Methods.fiveStarRateSkill)
_ = makeRequest(url, .get, [:], parameters: params, completion: { (results, message) in
// handle request
....
})
}

The following params we send in the request:

  • Model
  • Group
  • Language
  • Skill
  • Stars
  • Access Token

After successfully rating submission we get updated ratings for each star of the particular skill. The following response we get from the server after successfully submitted rating:

{
"ratings": {
"one_star": 0,
"four_star": 1,
"five_star": 0,
"total_star": 1,
"three_star": 0,
"avg_star": 4,
"two_star": 0
},
"session": {"identity": {
"type": "email",
"name": "abc@a.com",
"anonymous": false
}},
"accepted": true,
"message": "Skill ratings updated"
}

We use ratings object from fiveStarRatings API to update the ratings displayed on charts and labels and also, we use ratings object to update Skill model which we passed from SkillListingController to SkillDetailController so the user can see updated rating chart when clicking back to skill.

func updateSkill(with ratings: Ratings) {..}

If the user already submitted ratings for a skill, we are using getRatingByUser API with same params as in fiveStarRatings except ratings to get already submitted user rating and we display that ratings as initial ratings in UI.

Implementation of Submit Rating UI –

RatingView is behind the submit rating UI. FloatRatingViewDelegate protocol is implemented to get ratings is updating or get updated.

@objc public protocol FloatRatingViewDelegate {
/// Returns the rating value when touch events end
@objc optional func floatRatingView(_ ratingView: RatingView, didUpdate rating: Double)

/// Returns the rating value as the user pans
@objc optional func floatRatingView(_ ratingView: RatingView, isUpdating rating: Double)
}

Rating updated on rating display chart:

Now see, how we handle the case when Skill is not rated before and the user first time rate the skill.

There is a label that shows the “Skill not rated yet” when a skill is not rated. When the user rates the skill, the label is hidden and chart bar and the label is shown.

if self.ratingsBackViewHeightConstraint.constant == 0 {
self.ratingsBackViewHeightConstraint.constant = 128.0
self.contentType.topAnchor.constraint(equalTo: self.ratingBackView.bottomAnchor, constant: 16).isActive = true
self.ratingsBackStackView.isHidden = false
self.topAvgRatingStackView.isHidden = false
self.notRatedLabel.isHidden = true
}

 

Resources –

  1. Swift Protocol: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html
  2. SUSI Skills: https://skills.susi.ai/
  3. SUSI Server Link: https://github.com/fossasia/susi_server
  4. SUSI iOS Link: https://github.com/fossasia/susi_iOS
Continue ReadingAllowing user to submit ratings for skills in SUSI iOS

Implementing API to facilitate changing review status of a Skill

As any registered user can make Skills in SUSI Skill CMS, sometimes the Skill made is not up to the mark to be displayed on the CMS site. There needs to be a feature implemented where the Admin and higher user roles can review a Skill and approve it accordingly. Then only the approved Skills should be displayed on the Skill CMS. This feature required implementation of an API to allow Admin and higher user roles to change the review status of a Skill. This blog post explains how such an API has been implemented.

Implementing a servlet to allow changing review status of a Skill

The basic task of the servlet is to allow Admin and higher user roles to allow changing the review status of a Skill. The review status of a Skill can either be ‘true’, indicating that the Skill has been approved by the Admin, or ‘false’, which would indicate that the Skill still needs some improvements before it is actually displayed on the SUSI Skill CMS.

Here is the implementation of the API:

  1. The API should be usable to only the users who have a user role Admin or higher. Only those with minimum Admin rights should be allowed to control what Skills are displayed on the CMS site. This is implemented as follows:
   @Override
    public UserRole getMinimalUserRole() {
        return UserRole.ADMIN;
    }

 

  1. The endpoint for the API is ‘/cms/changeSkillStatus.json’. This is implemented as follows:
   @Override
    public String getAPIPath() {
        return "/cms/changeSkillStatus.json";
    }

 

  1. The main method of the servlet is the serviceImpl() method. This is where the actual code goes which will be executed each time the API is called. This is implemented as follows:

    @Override
    public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization, JsonObjectWithDefault permissions) throws APIException {

       // Store the query parameters in variables

        if(skill_name == null || !(reviewed.equals("true") || reviewed.equals("false"))) {
            throw new APIException(400, "Bad service call, missing arguments.");
        }

        JSONObject result = new JSONObject();
        JsonTray skillStatus = DAO.skillStatus;
        JSONObject modelName = new JSONObject();
        JSONObject groupName = new JSONObject();
        JSONObject languageName = new JSONObject();
        JSONObject skillName = new JSONObject();

        if(reviewed.equals("true")) {
            JSONObject reviewStatus = new JSONObject();
            reviewStatus.put("reviewed", true);
                // traverse through the skillStatus.json file down to the Skill name and put review status ‘true’ in it
                skillName.put("reviewed", true);
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }


            // deleting skill name from skillStatus.json file if it’s review status is being changed to ‘false’
            else {
                if (skillStatus.has(model_name)) {
                    modelName = skillStatus.getJSONObject(model_name);
                    if (modelName.has(group_name)) {
                        groupName = modelName.getJSONObject(group_name);
                        if (groupName.has(language_name)) {
                          languageName = groupName.getJSONObject(language_name);
                            if (languageName.has(skill_name)) {
                                languageName.remove(skill_name);
                                    skillStatus.remove(model_name);
                                }
                                skillStatus.commit();
                            }
                        }
                    }
                }
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }
        }

 

The list of reviewed Skills is being stored in the ‘skillStatus.json’ file. So we need to traverse through that file and store the review status of the Skill as required while making the API call.

The API takes 5 parameters:

  • Model of the Skill
  • Group of the Skill
  • Language of the Skill
  • Skill name
  • Review status to be set for the Skill – true or false

On making the API call, if the value of the query parameter ‘reviewed’ is ‘true’, then the Skill name along with its review status is being appended in the ‘skillStatus.json’ file. This is done by traversing through the file using the specified model, group and language info of the Skill.

However, if the value of the query parameter ‘reviewed’ is ‘false’, then we need to check if the Skill is already there in the ‘skillStatus.json’ file. If it is already there, then we remove its entry from the file. If it isn’t in the file already, then it’s review status is already ‘false’. We also need to commit this change to the JsonTray for it to get reflected in the Server.

This is how an API has been implemented which would allow Admin and higher user roles to change the review status of any Skill, which would then facilitate showing only the approved Skills on the CMS site.

  1. Resources

     

Continue ReadingImplementing API to facilitate changing review status of a Skill