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…

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…

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 maintenanceOn-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 prefixingReuse, Reduce code written for CSSSupports 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…

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 A UI Event like onClick happensThe UI event dispatches an action, mapDispatchToAction gives access to actions defined in action files.Perform synchronous or asynchronous tasks like fetching data from external API.The payload is used by reducer function, which updates the global store based on payload and previous state.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…

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 codeThe device resolution of screen used by a userUser languageThe 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…

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 imageimage_name(compulsory): The image relative folder path on the serverinfo: Any relevant information about the sliderdeleteSlide: 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) {…

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 Servlets Overview: https://www.tutorialspoint.com/servlets/servlets_overview.htmJava Servlet Sharing Information: https://javaee.github.io/tutorial/servlets003.html#BNAFOJava DAO Pattern: https://www.baeldung.com/java-dao-patternJava Servlet Tutorial: https://howtodoinjava.com/servlets/complete-java-servlets-tutorial/ Tags SUSI.AI, FOSSASIA, GSoC`19, API Development, SUSI Server, SUSI Skills

Continue ReadingAdding GetReportedSkill API on SUSI.AI Server