Integrating Redux In Settings

Settings page in SUSI.AI earlier implemented using Flux needed to be migrated to Redux, Redux integration eases handling of global state management. With Redux being integrated with Settings, the code could be split into smaller manageable components. Earlier implementation involved passing data to other Settings Tab and using Settings component as a state source. This resulted in Settings component getting more lines of code(~1300 LOC). After using Redux, only ~280 LOC is present in settings component file, and separate files for each settings tab view. 

Code Integration

Once user login, the App component fetches User Settings in ComponentDidMount.

componentDidMount = () => {
   const { accessToken, actions } = this.props;

   window.addEventListener('offline', this.onUserOffline);
   window.addEventListener('online', this.onUserOnline);

   actions.getApiKeys();
   if (accessToken) {
     actions.getAdmin();
     actions.getUserSettings().catch(e => {
       console.log(e);
     });
   }
 };

src/App.js

The action getUserSettings fetch users settings from apis.getUserSettings and populates the store settings using SETTINGS_GET_USER_SETTINGS reducer function.

Default States:

The store consists of all the settings required for persisting data.

 const defaultState = {
 theme: 'light',
 server: 'https://api.susi.ai',
 enterAsSend: true,
 micInput: true,
 speechOutput: true,
 speechOutputAlways: false,
 speechRate: 1,
 speechPitch: 1,
 ttsLanguage: 'en-US',
 userName: '',
 prefLanguage: 'en-US',
 timeZone: 'UTC-02',
 customThemeValue: {
   header: '#4285f4',
   pane: '#f3f2f4',
   body: '#fff',
   composer: '#f3f2f4',
   textarea: '#fff',
   button: '#4285f4',
 },
 localStorage: true,
 countryCode: 'US',
 countryDialCode: '+1',
 phoneNo: '',
 checked: false,
 serverUrl: 'https://api.susi.ai',
 backgroundImage: '',
 messageBackgroundImage: '',
 avatarType: 'default',
 devices: {},
};

reducers/settings.js

Reducer action:

SETTINGS_GET_USER_SETTINGS sets the settings store from API payload. The customThemeValue is an object, which is populated once the settings are fetched and the string is split into array.

[actionTypes.SETTINGS_GET_USER_SETTINGS](state, { error, payload }) {
     const { settings, devices = {} } = payload;
     if (error || !settings) {
       return state;
     }

     const {
       theme = defaultState.theme,
       server,
       .
       avatarType,
     } = settings;
     let { customThemeValue } = settings;
     const themeArray = customThemeValue
       ? customThemeValue.split(',').map(value => `#${value}`)
       : defaultState.customThemeValue;
     return {
       ...state,
       devices,
       server,
       serverUrl,
       theme,
       enterAsSend: enterAsSend === 'true',
       micInput: micInput === 'true',
       speechOutput: speechOutput === 'true',
       speechOutputAlways: speechOutputAlways === 'true',
       speechRate: Number(speechRate),
       speechPitch: Number(speechPitch),
       ttsLanguage,
       userName,
       prefLanguage,
       timeZone,
       countryCode,
       countryDialCode,
       phoneNo,
       checked: checked === 'true',
       backgroundImage,
       messageBackgroundImage,
       avatarType,
       customThemeValue: {
         header: themeArray[0],
         pane: themeArray[1],
         body: themeArray[2],
         composer: themeArray[3],
         textarea: themeArray[4],
         button: themeArray[5],
       },
     };
   },

reducers/settings.js

Updating Settings

Reduce function:

The payload contains the updated state, the reducer returns a new state with payload appended.

setUserSettings: createAction(
   actionTypes.SETTINGS_SET_USER_SETTINGS,
   returnArgumentsFn,
 )

reducers/settings.js

Action:

The setUserSettings action updates the settings store, once the user presses the Save Button in the Settings Tab.

function mapStateToProps(store) {
 return {
   enterAsSend: store.settings.enterAsSend,
 };
}

actions/settings.js

Redux integration in the component

Redux is used in ChatAppTab as: The local state of ChatAppTab is initialized with redux store state. Using mapStateToProps we can access enterAsSend object from the redux store.

The connect function connects the React component with Redux store and injects props and actions into our component using HOC pattern. It injects data from the store and functions for dispatching actions to store.

Injecting props in local components:

function mapStateToProps(store) {
 return {
   enterAsSend: store.settings.enterAsSend,
 };
}

components/Settings/ChatAppTab.react.js

Once a user makes a change in the tab and saves it, we need to dispatch an action to modify our prop enterAsSend inside the redux store. 

function mapDispatchToProps(dispatch) {
 return {
   actions: bindActionCreators({ ...settingActions, ...uiActions }, dispatch),
 };
}

components/Settings/ChatAppTab.react.j

handleSubmit = () => {
   const { actions } = this.props;
   const { enterAsSend } = this.state;
   this.setState({ loading: true });
   setUserSettings({ enterAsSend })
     .then(data => {
       if (data.accepted) {
         actions.openSnackBar({
           snackBarMessage: 'Settings updated',
         });
         actions.setUserSettings({ enterAsSend });
         this.setState({ loading: false });
       } else {
         actions.openSnackBar({
           snackBarMessage: 'Failed to save Settings',
         });
         this.setState({ loading: false });
       }
     })
     .catch(error => {
       actions.openSnackBar({
         snackBarMessage: 'Failed to save Settings',
       });
     });
 };

components/Settings/ChatAppTab.react.js

handleSubmit is invoked when user presses the Save Button, action.setUserSettings is dispatched once API returns accepted: true response.

To conclude, the removal of flux and integration of redux gave the advantage of making the code more modular and separate the logic into smaller files. In the future, more settings can be easily added using the same data flow pattern.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Redux, SUSI Chat, Settings

Continue ReadingIntegrating Redux In Settings

Enhancing User Security With Server-Side Validation

Server-side validation of SUSI.AI is required for increasing the security of user against malicious users who can bypass client-side validations easily and submit dangerous input to SUSI.AI server. SUSI.AI uses both client-side and server-side validation for various authentication purposes.

Client-side validates user input on the browser and provides a better user interaction. If the form is disabled on incorrect fields, it would save a round trip to the server, so the network traffic which will help your server perform better. Using client-side validation, an error message can be immediately shown before the user moves to the next field.

Adding Recaptcha while on new registration, change password, and more than one login attempts help to reduce abusive traffic, spam, brute force attack. For making user accounts safe from getting hacked on SUSI.AI, a strong password is supposed to be at least 8 characters, at least one special character, one number and text characters are must. The username must have 5-51 characters. So as to prevent SUSI.AI admin panel from crashing.

Code Integration

Client side reCaptcha validation:

onCaptchaSuccess = captchaResponse => {
  if (captchaResponse) {
    this.setState({
      showCaptchaErrorMessage: false,
      captchaResponse,
    });
  }
};

Auth/Login/Login.react.js

Once the user verifies captcha on client-side, a callback onCaptchaSuccess fires which receives a parameter captchaResponse. This captchaResponse is sent over to server-side with key g-recaptcha-response

Validation for User Login, ReCaptcha needs to be shown if the user has tried to log in for more than 1 time. For this, sessionStorage needs to be maintained. sessionStorage has an expiration time, which expires once the user closes the tab.

In the constructor of Login, the attempt is initialized with sessionStorage using key loginAttempts. Now, we have the correct value of loginAttempts and can update it further if the user tries to make more attempt.

constructor(props) {
    super(props);
    this.state = {
      ...
      showCaptchaErrorMessage: false,
      attempts: sessionStorage.getItem('loginAttempts') || 0,
      captchaResponse: '',
    };
  }

Auth/Login/Login.react.js

If the user closes the login dialog, we need to store the current attempts state on componentWillUnmount

componentWillUnmount() {
  sessionStorage.setItem('loginAttempts', this.state.attempts);
}

Auth/Login/Login.react.js

If attempts are less than 1, render the reCaptcha component and send g-recaptcha-response parameter to api.susi.ai/aaa/login.json API endpoint.

Server-side reCaptcha Validation for user login

The checkOneOrMoreInvalidLogins function in LoginService checks if the user has attempted to login in more than once. Access the accounting object and get identity and store it in accounting.

private boolean checkOneOrMoreInvalidLogins(Query post, Authorization authorization, JsonObjectWithDefault permissions) throws APIException {
	Accounting accounting = DAO.getAccounting(authorization.getIdentity());
	JSONObject invalidLogins = accouting.getRequests().getRequests(this.getClass().getCanonicalName());
	long period = permissions.getLong("periodSeconds", 600) * 1000; // get time period in which wrong logins are counted (e.g. the last 10 minutes)
	int counter = 0;
	for(String key : invalidLogins.keySet()){
		if(Long.parseLong(key, 10) > System.currentTimeMillis() - period) counter++;
	}
	if(counter > 0){
		return true;
	}
	return false;
}

aaa/LoginService.java

If user has attempted to login in more than once, get from g-recaptcha-response API parameter. Check if reCaptcha is verified using VerifyRecaptcha utility class.

if(checkOneOrMoreInvalidLogins(post, authorization, permissions) ){
	String gRecaptchaResponse = post.get("g-recaptcha-response", null);
	boolean isRecaptchaVerified = VerifyRecaptcha.verify(gRecaptchaResponse);
	if(!isRecaptchaVerified){
		result.put("message", "Please verify recaptcha");
		result.put("accepted", false);
		return new ServiceResponse(result);
	}
}

aaa/LoginService.java

The VerifyRecaptcha class provides with a verify method, which can be called to check if user response if correct or not. The secretKey is key given for communication between server-side and google.

URL is created and using InputStream, content(res) is directly read from a URL using openStream() function provided. The bufferedReader reads the contents line by line(using cp), and rd.read() returns a string. The result is appended to stringBuilder sb until -1 is read.

res.close() closes the stream and releases the resources for URL reading. JSONObject is created using the string created from URL, and value mapped to success is used to check if ReCaptcha is verified. If the response is correct, it returns true.

public class VerifyRecaptcha {
	public static boolean verify(String response) {
		try {
			String url = "https://www.google.com/recaptcha/api/siteverify?"
					+ "secret=6LfPZGAUAAAAAAULjaq7Rt9-7IGJoLYoz2Di6yVV&response=" + response;
			InputStream res = new URL(url).openStream();
			BufferedReader rd = new BufferedReader(new InputStreamReader(res, Charset.forName("UTF-8")));

			StringBuilder sb = new StringBuilder();
			int cp;
			while ((cp = rd.read()) != -1) {
				sb.append((char) cp);
			}
			String jsonText = sb.toString();
			res.close();

			JSONObject json = new JSONObject(jsonText);
			return json.getBoolean("success");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}
}

tools/VerifyRecaptcha.java

User name validation on server side

Changing regex in config: Regular express for username to have 5-51 characters. Setting regex and tooltip in configs:

users.username.regex=^(.{5,51})$

users.username.regex.tooltip=Enter atleast 5 character, upto 51 character

users.username.regex=^(.{5,51})$
users.username.regex.tooltip=Enter atleast 5 character, upto 51 character

conf/config.properties

If a user tries to add userName more than 51 characters or less than 5 characters, 

The regex is compiled using Pattern.compile and then matched against the value provided by the user in API parameter. If the pattern matches, it means the user provided the correct userName.

if(possibleKeys[i].equals("userName") && value != null){
    String usernamePattern = DAO.getConfig("users.username.regex", "^(.{5,51})");
    String usernamePatternTooltip = DAO.getConfig("users.username.regex.tooltip",
    "Enter atleast 5 character, upto 51 character");
    Pattern pattern = Pattern.compile(usernamePattern);
    if(!pattern.matcher(value).matches()) {
        throw new APIException(400, usernamePatternTooltip);
    }
}

aaa/ChangeUserSettings.java

With server-side validation incorporated, the SUSI.AI users accounts are much safer than only with client-side validation. Validation over both client-side and server-side complement each, server-side validation being much more robust.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, SUSI.AI Server

Continue ReadingEnhancing User Security With Server-Side Validation

Creating Common Loading Component in SUSI.AI

A circular loading component appears whenever an asynchronous event takes time on the front-end, to show the user that content is yet to be fetched and processed.

Creating Common Loading component eased the process of handling circular loading alignment. Using common loader decreased code repetition. There were major cases for loader component, mobile view, and desktop view.

Why Styled-components for SUSI.AI?

  • Painless maintenance
  • On-demand CSS injection: The components being rendered in the page are kept on track and only those component’s styles are injected into the DOM. If we combine react-loadable or some other code splitting, the app becomes really performant.
  • Styled components generate their own class name like sc-g3h4h6 and add it as an attribute to DOM, which avoids class name overlaps and misalignments.
  • Styled components are easier to detect as they can be detected by linter and husky. Whereas, static CSS files are not detected, which leads to dead code in the codebase and make them hard to manage. 
  • Simple dynamic styling: adapting the styling of a component based on its props, without having to manually manage dozens of classes.
  • Automatic vendor prefixing
  • Reuse, Reduce code written for CSS
  • Supports media queries, which makes creating a responsive PWA easier

 Let’s have a look at it’s implemented.

Code Integration

const Container = styled.div`
 display: flex;
 align-items: center;
 justify-content: center;
 ${props =>
   props.height
     ? css`
         height: ${props => props.height + 'rem'};
         @media (max-width: 512px) {
           height: ${props =>
             props.height > 20
               ? props.height - 15 + 'rem'
               : props.height + 'rem'};
         }
       `
     : css`
         margin: 19rem 0;
         height: 100%;
         @media (max-width: 512px) {
           margin: 14rem 0;
         }
       `}
`;

const CircularLoader = ({ height = 'auto', color = 'primary', size = 64 }) => {
 return (
   <Container height={height}>
     <CircularProgress color={color} size={size} />
   </Container>
 );
};

components/shared/CircularLoader.js

The CircularLoader component by default should acquire size 64 width and size, and color primary i.e. SUSI.AI blue color.

The container created using styled component, by default should be horizontally and vertically centered. This is used to handle the case where the content takes the whole width and height(100%) of page. 

If we want the container to be of specific height(lesser than the page in which the Loader is being rendered), we pass in height props.

For handling the mobile views, in case of height, 100%, margin-top and margin-bottom are reduced by 5rem.

In case of height passed in through component, check if the height is greater(than 20) so that the mobile view user can see the footer as well, reduce height by 15rem. If the height is lesser than 20rem, the footer is in the viewport.

Before PR, let’s have a look at how the Loader was rendered inside component:

{loading ? (
  <LoadingContainer>
    <CircularProgress size={64} />
  </LoadingContainer>
 ) : (
   ...
 )}

Using Common shared loader,

{loading ? <CircularLoader height={27} /> : ...

Settings/Settings.react.js

Adding styled component across the SUSI.AI web app proved to be of great help, the mobile views were easily made using media queries. Components positioning logic could be changed based on props, and most importantly, styled-components could be reused and were much easier to manage and maintain than static CSS classes or inline styles.

However, for other cases like when single or less number of styles were applied, maintaining everything using styled component proved to be additional overhead. A combination of inline styles and styled-components would be apt depending on the nature of style. 

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, styled-components, SUSI.AI Chat

Continue ReadingCreating Common Loading Component in SUSI.AI

Integrating Redux in SkillCreator and BotBuilder

SkillCreator in SUSI.AI used for creating new skills and Botbuilder for creating new bots had parent-child communication as seen in React components. Many components were using a state, which was lifted up to its parent and passed to its children (Design, Configure, Deploy, SkillCreator) as props. This architecture has many problems, maintainability and readability being the prime one.

For maintaining the state of SkillCreator and Botbuilder we used Redux, which lets us use global state and we don’t have to worry about passing props just for the purpose of sending data deep down. With Redux, the code size reduced and we successfully eliminated a lot of code redundancy. 

Basic Data Flow in Redux

  1. A UI Event like onClick happens
  2. The UI event dispatches an action, mapDispatchToAction gives access to actions defined in action files.
  3. Perform synchronous or asynchronous tasks like fetching data from external API.
  4. The payload is used by reducer function, which updates the global store based on payload and previous state.
  5. The global store changes and using the mapStateToProps present, the function is subscribed to changes over time and gets the updated values of the store.

Code Integration 

The state object consists of 3 nested objects. Each view, .i.e on botWizard, for Build tab we update the skill object. For the Design tab, design object and etc. The objective of storing in Redux store is, we require the data to persist when a user navigates/change to other tabs for example Design Tab, the data of skills tab is mapped from the store so if user switches back to skills tab, no data loss will be there. All such required fields are stored in the store.

Default States: 

const defaultState = {
 skill: {
   name: '',
   file: null,
   category: null,
   language: '',
   image: avatarsIcon,
   imageUrl: '<image_name>',
   code:
     '::name <Bot_name>\n::category <Category>\n::language <Language>\n::author <author_name>\n::author_url <author_url>\n::description <description> \n::dynamic_content <Yes/No>\n::developer_privacy_policy <link>\n::image images/<image_name>\n::terms_of_use <link>\n\n\nUser query1|query2|quer3....\n!example:<The question that should be shown in public skill displays>\n!expect:<The answer expected for the above example>\nAnswer for the user query',
   author: '',
 },
 design: {
   botbuilderBackgroundBody: '#ffffff',
   botbuilderBodyBackgroundImg: '',
   botbuilderUserMessageBackground: '#0077e5',
   botbuilderUserMessageTextColor: '#ffffff',
   botbuilderBotMessageBackground: '#f8f8f8',
   botbuilderBotMessageTextColor: '#455a64',
   botbuilderIconColor: '#000000',
   botbuilderIconImg: botIcon,
   code:
     '::bodyBackground #ffffff\n::bodyBackgroundImage \n::userMessageBoxBackground #0077e5\n::userMessageTextColor #ffffff\n::botMessageBoxBackground #f8f8f8\n::botMessageTextColor #455a64\n::botIconColor #000000\n::botIconImage ',
 },
 configCode:
   "::allow_bot_only_on_own_sites no\n!Write all the domains below separated by commas on which you want to enable your chatbot\n::allowed_sites \n!Choose if you want to enable the default susi skills or not\n::enable_default_skills yes\n!Choose if you want to enable chatbot in your devices or not\n::enable_bot_in_my_devices no\n!Choose if you want to enable chatbot in other user's devices or not\n::enable_bot_for_other_users no",
 view: 'code',
 loading: true,

reducers/create.js

The CREATE_SET_VIEW reducer function updates the view, e.g it can be ‘code’, ‘ui’, ‘tree’. The action setView is dispatched to update the view tab. The action setSkillData is dispatched when we want to update the skill object in our global store.Reducer function for it:

[actionTypes.CREATE_SET_SKILL_DATA](state, { payload }) {
  return {
    ...state,
    skill: {
      ...state.skill,
      ...payload,
    },
  };
},

reducers/create.js

The setSkillData is called whenever the user updates the code of skills.

Updating Design Code

The setDesignData is dispatched whenever we update the code in AceEditor of design tab. generateDesignData function helps in generating design object from the designCode to update the UI view of design tab.

   [actionTypes.CREATE_SET_DESIGN_DATA](state, { payload }) {
     const { code } = payload;
     return {
       ...state,
       design: {
         code,
         ...generateDesignData(code),
     },
   };
 },

reducers/create.js

updateDesignData action is called for changing the design object in global state. In UIView,

handleRemoveUrlBody = () => {
   let {
     actions,
     design: { code },
   } = this.props;
   code = code.replace(
     /^::bodyBackgroundImage\s(.*)$/m,
     '::bodyBackgroundImage ',
   );
   actions.updateDesignData({
     code,
     botbuilderBodyBackgroundImg: '',
     botbuilderBodyBackgroundImgName: '',
   });
 };

reducers/create.js

In reducer, the design state is updated by extracting payload using the spread operator and adding new key-value pairs to design object.

[actionTypes.CREATE_UPDATE_DESIGN_DATA](state, { payload }) {
     return {
       ...state,
       design: {
         ...state.design,
         ...payload,
       },
     };
   },

reducers/create.js

For changing the color, handleChangeColor function in present in UIView to handle color change, which dispatches action setDesignComponentColor

The CREATE_SET_DESIGN_COMPONENT_COLOR updates the design code accordingly to match the UI changes done in the UI tab of the design view.

[actionTypes.CREATE_SET_DESIGN_COMPONENT_COLOR](state, { payload }) {
     let code = '';
     const { component, color } = payload;
     if (component === 'botbuilderBackgroundBody') {
       code = state.design.code.replace(
         /^::bodyBackground\s(.*)$/m,
         `::bodyBackground ${color}`,
       );
     } else if (component === 'botbuilderUserMessageBackground') {
       code = state.design.code.replace(
         /^::userMessageBoxBackground\s(.*)$/m,
         `::userMessageBoxBackground ${color}`,
       );
     } else if (component === 'botbuilderUserMessageTextColor') {
       code = state.design.code.replace(
         /^::userMessageTextColor\s(.*)$/m,
         `::userMessageTextColor ${color}`,
       );
     } else if (component === 'botbuilderBotMessageBackground') {
       code = state.design.code.replace(
         /^::botMessageBoxBackground\s(.*)$/m,
         `::botMessageBoxBackground ${color}`,
       );
     } else if (component === 'botbuilderBotMessageTextColor') {
       code = state.design.code.replace(
         /^::botMessageTextColor\s(.*)$/m,
         `::botMessageTextColor ${color}`,
       );
     } else if (component === 'botbuilderIconColor') {
       code = state.design.code.replace(
         /^::botIconColor\s(.*)$/m,
         `::botIconColor ${color}`,
       );
     }
     return {
       ...state,
       design: {
         ...state.design,
         code,
         [component]: color,
       },
     };
   },

reducers/create.js

The reducer generates the new designCode and also sets the component color for it to be used in UI View. Similar logic is applied for handling the configured state in the reducer.

To conclude, shifting to redux architecture instead of prop drilling proved to be of great advantage. The code became more performant, modular and easy to manage. The state persisted even after component unmounted.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Redux, Skills CMS, SUSI Chat


Continue ReadingIntegrating Redux in SkillCreator and BotBuilder

Adding Google Analytics To SUSI.AI

Google analytics provides SUSI.AI Admins with a way to analyze traffic and get advanced metrics. Google Analytics first collects data, computes the data, and showcases it on console dashboard. It is used for keeping track of user behavior on the website.

How Google Analytics Work

Below shown are fields used by Google Analytics to get user data. A cookie is stored into user browser. _ga stays in the browser for 2 years, _gid for 2 days.

Whenever a user performs an event like a mouse click, page change, open popup, add query strings to URL, information is sent to Google Analytics using an API call describing the user event. Below is the photo describing it:

The above-bordered boxes consist of information sent by google analytics. The information consists of:

  • The user identification code
  • The device resolution of screen used by a user
  • User language
  • The URL of the page user is on

How it processes data

When a user with tracking code lands on SUSI.AI, Google Analytics creates a unique random identity and attaches it to the user cookie.

Each new user is given a unique ID. Whenever a new ID is detected, analytics considers it as a new user. But when an existing ID is detected, it’s considered as a returning user and set over with the hit.

A unique ID code is fetched from every new unique user. Whenever a new user ID is detected in the call, Google Analytics treats the unique ID as a new user. If the ID matches from earlier ID, the user is a returning user and calculates the metrics another way. Each new user gets a unique ID.

However, a new user is detected if the same user clears out the browser cookie or uses another device over the same IP address to view the webpage.

When an existing ID is detected, it’s considered as a returning user and set over with the hit.

Code Integration

Google Analytics must be initialized using initialize(gaTrackingID) function before any of the other tracking functions will record any data. Using react-ga ga(‘create’, …), the values are sent to google analytics

import withTracker from './withTracker';
import GoogleAnalytics from 'react-ga';

..

actions.getApiKeys().then(({ payload }) => {
  const {
    keys: { googleAnalyticsKey = null },
   } = payload;
   googleAnalyticsKey && GoogleAnalytics.initialize(googleAnalyticsKey);
});

..
<Route exact path="/" component={withTracker(BrowseSkill)} />
<Route exact path="/chat" component={withTracker(ChatApp)} />

src/App.js

Higher Order Component for Tracking page activity

A Higher Order Component is a function that returns an enhanced component by adding some more properties or logic and allows reusing component logic.

Using HOC for tracking page helped by not exposing internal component with tracking. The withTracker HOC wraps all the component, which SUSI.AI wants to track and exposes a trackerPage method. 

  • Whenever a component mounts, we track the new pages using componentDidMount. 
  • Whenever the component updates and location of browser url changes, we track the sub pages using componentDidUpdate lifecycle hook.

import React, { Component } from 'react';
import GoogleAnalytics from 'react-ga';
import PropTypes from 'prop-types';

const withTracker = (WrappedComponent, options = { }) => {
  const trackPage = page => {
    GoogleAnalytics.set({
      page,
      ...options,
    });
    GoogleAnalytics.pageview(page);
  };
  const HOC = class extends Component {
    componentDidMount() {
      window.scrollTo(0, 0);
      const page = this.props.location.pathname + this.props.location.search;
      trackPage(page);
    }

    componentDidUpdate(prevProps) {
      const currentPage =
        prevProps.location.pathname + prevProps.location.search;
      const nextPage =
        this.props.location.pathname + this.props.location.search;

      if (currentPage !== nextPage) {
        trackPage(nextPage);
      }
      if (this.props.location.pathname !== prevProps.location.pathname) {
        window.scrollTo(0, 0);
      }
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };

  HOC.propTypes = {
    location: PropTypes.object,
  };

  return HOC;
};

export default withTracker;

src/withTracker.js

The Higher-Order Component pattern turned out to be really useful to achieve D.R.Y (Don’t Repeat Yourself) and keeping component separate from tracking. With React Analytics being added, we can track various metrics, live users on site and see how SUSI.AI traffic is performing over time. 

Resources

Tags

SUSI.AI, FOSSASIA, GSoC19, Google Analytics, Higher Order Components

Continue ReadingAdding Google Analytics To SUSI.AI

Implementing Slideshow Servlet in SUSI.AI Skills

Slideshow shown on SUSI.AI homepage helps SUSI.AI client showcase interesting and new features integrated into the platform. It helps to display the capabilities of SUSI.AI and other interesting areas. The slideshow can be configured from the Admin panel of SUSI.AI. 

For storing slideshow data, images, information, redirect to link on slideshow click, we need to implement a servlet to store data on server-side.

The endpoint is of GET type, and accepts:

  • redirect_link(compulsory): redirect link if a user clicks on the slider image
  • image_name(compulsory): The image relative folder path on the server
  • info: Any relevant information about the slider
  • deleteSlide: True, if the user wants to delete slider

Code Integration

For implementing slideshow service, we need to store the image on the backend using uploadImage service and using the uploaded image file path in the backend to store the full slider details using skillSlideshowService service. 

SkillSlideshowService:

For setting the slideshow, the minimum permission required is ADMIN

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

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

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

cms/SkillSlideshowService.java

Let’s have a look at how it is implemented, the redirect_link and image_name are necessary parameters and if not passed throws exception. If appropriate parameters are present, get the user query data using query.call. Access the data on the server side through DAO.skillSlideshow, if slideshow key is present in JsonTray skillSlideshow, get JSONObject with key “slideshow”.

If deleteKey is false, create a new JSONObject and put the query call data inside it and add to skillSlideshow object with redirectUrl as the key.

If deleteKey is true, remove the object associated with redirect_link and create a new object and add.

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization,
           final JsonObjectWithDefault permissions) throws APIException {
       if (call.get("redirect_link", null) == null || call.get("image_name", null) == null) {
           throw new APIException(400, "Bad Request. No enough parameter present");
       }

       String redirectLink = call.get("redirect_link", null);
       String imageName = call.get("image_name", null);
       String info = call.get("info", null);
       boolean deleteSlide = call.get("deleteSlide", false);
       JsonTray skillSlideshow = DAO.skillSlideshow;
       JSONObject result = new JSONObject();
       JSONObject skillSlideshowObj = new JSONObject();
       if (skillSlideshow.has("slideshow")) {
           skillSlideshowObj = skillSlideshow.getJSONObject("slideshow");
       }
       if (!deleteSlide) {
           try {
               JSONObject slideObj = new JSONObject();
               slideObj.put("image_name", imageName);
               slideObj.put("info", info);
               skillSlideshowObj.put(redirectLink, slideObj);
               skillSlideshow.put("slideshow", skillSlideshowObj, true);
               result.put("accepted", true);
               result.put("message", "Added new Slide " + call.get("redirect_link") + " successfully !");
               return new ServiceResponse(result);
           } catch (Exception e) {
               throw new APIException(500,
                       "Failed : Unable to add slide with path " + call.get("redirect_link") + " !");
           }
       } else {
           try {
               skillSlideshowObj.remove(redirectLink);
               skillSlideshow.put("slideshow", skillSlideshowObj, true);
               result.put("accepted", true);
               result.put("message", "Removed Slide with path " + call.get("redirect_link") + " successfully !");
               return new ServiceResponse(result);
           } catch (Exception e) {
               throw new APIException(501,
                       "Failed to remove Slide: " + call.get("redirect_link") + " doesn't exists!");
           }
       }
   }

cms/SkillSlideshowService.java

GetSkillSlideshow 

For fetching the slideshow data on frontend, GetSkillSlideshow servlet is implemented. The minimum userRole required in ANONYMOUS.

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

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

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

cms/GetSkillSlideshow.java

For fetching the slider data, access DAO.skillSlideshow, and get the JSONObject associated with key slideshow and put it in result response, put accepted key as true and return the response

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization rights,
           final JsonObjectWithDefault permissions) throws APIException {

       JsonTray skillSlideshow = DAO.skillSlideshow;
       JSONObject skillSlideshowObj = skillSlideshow.getJSONObject("slideshow");
       JSONObject result = new JSONObject();

       try {
           result.put("accepted", true);
           result.put("slideshow", skillSlideshowObj);
           result.put("message", "Success : Fetched all Skills Slides!");
           return new ServiceResponse(result);
       } catch (Exception e) {
           throw new APIException(500, "Failed : Unable to fetch Skills Slides!");
       }
   }

cms/GetSkillSlideshow.java

3 types of endpoints are required for achieving the slider slideshow functionality. First, when a user creates or edits a slider, the user first needs to upload the image on the server using uploadImage.json service.

Image Suffix is the suffix of the file name stored in SUSI.AI server, susi_icon is the suffix in the image shown below.

Once the image is uploaded on the server, the API returns the relative path to the server location. The path on the server gets filled in ImagePath field on client-side(disabled to users).

With GetSkillSlideshow and SkillSlideshowService implemented, the admins can now manage and control the slideshow shown on the SUSI.AI home page, directly from the admin panel. The client can now easily discover new, exciting features as well.

Resources

Tags

SUSI.AI, FOSSASIA, GSoC`19, SUSI.AI Server

Continue ReadingImplementing Slideshow Servlet in SUSI.AI Skills

Adding GetReportedSkill API on SUSI.AI Server

The GetReportedSkill API was implemented for Admins to view the reported feedback given by users, admin can then monitor skills, which are working fine and in which users are having problems. This can help in deleting buggy/erroneous skills directly from the reported skills tab in the admin panel.

The endpoint is of GET type, and accept 2 parameters: 

  • access_token(compulsory): It is the access token of the logged-in user. It is of a string data type.
  • search: It is a string param that helps us to fetch a list of feedback related to the search term

The minimal role is set to OPERATOR as Admin section access is required for reported skill list.

API Development

Here is a sample response from api:

{
  “session”: {“identity”: {
    “type”: “host”,
    “name”: “0:0:0:0:0:0:0:1_d9aaded8”,
    “anonymous”: true
  }},
  “accepted”: true,
  “list”: [
    {
      “feedback”: “test”,
      “skill_name”: “GSOC”,
      “email”: “shubhamsuperpro@gmail.com”,
      “timestamp”: “2019-06-15 03:25:29.425”
    },
    {
      “feedback”: “test101”,
      “skill_name”: “GSOC”,
      “email”: “shubham@gmail.com”,
      “timestamp”: “2019-06-15 12:18:33.641”
    }
  ],
  “message”: “Success: Fetched all Reported Skills”
}

The reported skills are stored in DAO under reportedSkills, for fetching the list we need to traverse it’s JSONObject.

JsonTray reportedSkills = DAO.reportedSkills;
JSONObject reportedSkillsListObj = reportedSkills.toJSON();

api/cms/GetReportSkillService.java

Code

For creating a list we need to access each property of JSONObject of reportedSkill, in the following order:

Model → Group → Language → Skill Name → Reported feedback list

for (String key:JSONObject.getNames(reportedSkillsListObj)) {
  modelName = reportedSkillsListObj.getJSONObject(key);
      if (reportedSkillsListObj.has(key)) {
        for (String group_name : JSONObject.getNames(modelName)) {
          groupName = modelName.getJSONObject(group_name);
          if (modelName.has(group_name)) {
            for (String language_name : JSONObject.getNames(groupName)) {
              languageName = groupName.getJSONObject(language_name);
              if (groupName.has(language_name)) {

api/cms/GetReportSkillService.java

If search parameter is passed, check if skillName matches with search parameter, if both strings are equal, create a new reportedObject and append it to reportList, which is a list of reported skills

if (call.get("search", null) != null) {
  String skill_name = call.get("search", null);
    if (languageName.has(skill_name)) {
      skillName = languageName.getJSONObject(skill_name);
      reports = skillName.getJSONArray("reports");
      for (int i = 0; i < reports.length(); i++) {
        JSONObject reportObject = new JSONObject();
        reportObject = reports.getJSONObject(i);
        reportObject.put("skill_name", skill_name);
        reportList.add(reportObject);

api/cms/GetReportSkillService.java

If search parameter is not passed, traversed all reported skills and append it to array(reportList).

getNames returns an array of keys as string stored in JSONObject, we traverse the array and put all the reported skill name, feedback, email and timestamp in reportObject and add it to reportList

} else {
   for (String skill_name : JSONObject.getNames(languageName)) {
      skillName = languageName.getJSONObject(skill_name);
      if (languageName.has(skill_name)) {
        reports = skillName.getJSONArray("reports");
           for (int i = 0; i < reports.length(); i++) {
             JSONObject reportObject = new JSONObject();
   reportObject = reports.getJSONObject(i);
   reportObject.put("skill_name", skill_name);    reportList.add(reportObject);
                 }
            }
       }
  }

api/cms/GetReportSkillService.java

Once we have the list of reported skills reportList return the service response

try {
   result.put("list", reportList);
   result.put("accepted", true);
   result.put("message", "Success: Fetched all Reported  Skills");
   return new ServiceResponse(result);
  } catch (Exception e) {
       throw new APIException(500, "Failed to fetch the requested list!");}

api/cms/GetReportSkillService.java

To conclude, the Admin’s now can take decisions based on reports submitted by the user to delete a skill or ignore the feedback.

Link to PR: https://github.com/fossasia/susi_server/pull/1274

Resources

Tags

SUSI.AI, FOSSASIA, GSoC`19, API Development, SUSI Server, SUSI Skills

Continue ReadingAdding GetReportedSkill API on SUSI.AI Server