How to build a simple social media monitor with NodeJS, GraphQL, and Vue

  • 2019-02-01 01:51 AM
  • 165

How to build a simple social media monitor with NodeJS, GraphQL, and ... If you have troubles setting up a Vue project, you can find out how to ...

Introduction

We’ll build a simple monitor that will track the number of people that followed us on specific dates, using Medium as the use case. This is just a bare prototype and can be done for any other social media or networking platform.

In the end, we should have something like this:

NodeJS, GraphQL, and Vue

To achieve this, we will use thenode-imappackage. The two major protocols for handling emails are IMAP (Internet Messaged Access Protocol) and POP (Post office protocol). IMAP is preferred, because it always syncs with the mail server, hence the changes made on the mail client will appear immediately on the webmail inbox.

Prerequisites

  • NodeJS
  • VueJS
  • A Gmail account

Setup the back-end with node-imap and apollo

Firstly, install the necessary packages.

npm i --save node-imap apollo-server mailparser

Now you can define the types and resolver and then run the apollo server.

const {ApolloServer, gql} = require('apollo-server')
const typeDefs = gql`
    type User {
        email: String!
        password: String!
	}
    type Mutation {
        imapMutation(email: String!, password: String!): User!
    }
`
const resolvers = {
	Mutation:{
		imapMutation: (parent, args) => {}
	}
}
const server = new ApolloServer({typeDefs, resolvers})
server.listen({port: process.env.PORT || 4000}).then(({url})=>{
	console.log(`Server started at ${url}, \n Unai Way ✈️ ✈️ ✈️` )
})

A ‘User’ type that has the email and password string types as its schema has been defined, and a mutation called ‘imapMutation’ which receives the email and password from the user and then returns a response with the User type.

The resolver handles the mutation, and then you can work with the arguments sent from the client.

Now you can run the server.

You can then import the node-imap and mailparser modules. The mailparser module will be used to receive the mail responses as JSON.

const Imap = require('imap')
const simpleParser = require('mailparser').simpleParser

You are going to create a ‘connectImap’ function that will handle our IMAP functionality. From the node-imap documentation, you can get the skeleton of how the module works, and then copy and paste it into the code. It basically works with callbacks and emitters, so we’ll wrap it in a promise.

You should have something like this.

//Create an array to hold the graphPoints
let graphPoints = [];

async function connectToImap(user,password) {
	let resolveGraph = new Promise((resolve,reject)=>{
		const imap = new Imap({
			user: user,
			password: password,
			host: 'imap.gmail.com',
			port: 993,
			tls: true
		})
		function openInbox(cb) {
			imap.openBox('INBOX', true, cb)
		}
		imap.once('ready', function() {
			openInbox(function(err, box) {
				if (err) throw new Error('Invalid Login. Please Try again.')
					f.on('message', function(msg, seqno) {
					   msg.on('body', function(stream, info) {
						let buffer = ''
						stream.on('data', function(chunk) {
						  buffer += chunk.toString('utf8')
						})
					   })	
					})
					f.once('error', function(err) {
						reject(new Error('Fetching results failed'))
					})
					f.once('end', function() {
						console.log('Done fetching all messages!')
						imap.end()
					})
				})
			})
		})
		imap.once('error', function(err) {
			reject(new Error('Failed Imap connection'))
		})
		imap.once('end', function() {
			console.log('Connection ended')
		})
		imap.connect()
	})
	return await resolveGraph
}

When the ‘ready’ event is called, we connect to our mail, and then we can search for messages. So, we’ll search for emails from the medium account that handles followers ([email protected]).

Our ‘ready’ event should look like this.

imap.once('ready', function() {
  //Open Inbox
  openInbox(function(err, box) {
    if (err) throw new Error('Invalid Login. Please Try again.')
    //Search for mails sent from the medium account
    imap.search([['FROM',
	'[email protected]',
     ]], (err,result) =>{
	//Get the headers from the mail 
	var f = imap.fetch(result, {
	    bodies: ['HEADER.FIELDS (FROM SUBJECT DATE)'],
	    struct: true
	})
	f.on('message', function(msg, seqno) {
	  msg.on('body', function(stream, info) {
	   let buffer = ''
	   stream.on('data', function(chunk) {
	     buffer += chunk.toString('utf8')
	     simpleParser(buffer).then(parsed => {
	       //Check if the subject of each mail had a 'started following you' substring
	       if(parsed.subject && parsed.subject.includes('started following you')){
		 //Split the array to get the total number of followers, and remove empty spaces
		 let numberOfFollowers = parsed.subject.split(/,|and/).filter(v=>v!=' ').length
		 //Handle the instance where the mail says 'paschal and 3 others followed you'
		 if(/\d others/.test(parsed.subject)) {
	            let otherNumber = /\d/.exec(parsed.subject)
		    numberOfFollowers += (Number(otherNumber[0]) - 1)
		 }
		 //Create an object with the number of followers and the date
	         let dataPoint = {
		     numberOfFollowers: numberOfFollowers,
		     date: parsed.date
	          }
		 //Push to graphPoints array
		  graphPoints.push(dataPoint)
	        }
	      })
	      .catch(err =>{throw err})	
	     })
	   })
	})
	f.once('error', function(err) {
		reject(new Error('Fetching results failed'))
	})
	f.once('end', function() {
		console.log('Done fetching all messages!')
		imap.end()
	})
    })
 })
})
imap.once('error', function(err) {
   reject(new Error('Failed Imap connection'))
})
  
imap.once('end', function() {
  console.log('Connection ended')
  resolve(graphPoints)
})
imap.connect()

We search for emails where each subject contains a ‘started following you’ substring. Then, we split the arrays with either a comma or the conjunction ‘and’ to get the number of followers, and then handle cases like ‘Peter and 3 others started following you’. After splitting the string above, we will have an output:

[
  'Peter',
  '3 others started following you'
]

The length of this array is 2, so we have two followers. If the subject contains ‘others’, we take the digit behind it and add to the length of the array, which is 5 and then we subtract 1 to get rid of the ‘3 others started following you’ string. This leaves us with 4.

Then, we resolve the promise when the ‘end’ event is fired (imap.once(‘end’)).

Since we will have to send the array to our apollo client, we will need to define the type of the ‘graphPoints’ array.

Our type definitions should look like this:

const typeDefs = gql`
	scalar Date
    # Make a type user
    type User {
        email: String!
        password: String!
        data: [Graph!]
	}
	
	type Graph {
		numberOfFollowers: Int!
		date: Date!
	}
    type Mutation {
        imapMutation(email: String!, password: String!): User!
    }
`

We added the data key to the ‘User’ type, which will hold the ‘graphPoints’ value, and its type is an array of objects with the ‘Graph’ type.

Finally, we handle the resolver, which will get the email and the password of the user and then return the email and the data (graphPoints).

If we log the user object, our structure should be something like this:

email: String,
data: [ { numberOfFollowers: 1, date: 2017-07-05T07:53:18.000Z },
        { numberOfFollowers: 1, date: 2017-07-07T19:34:57.000Z }
      ]

Setup the front-end with v-charts and apollo client

Now, we want to get the data sent from the server and plot the chart with the v-charts module.

But first, we install our dependencies.

npm install --save vue-chartjs vue-apollo apollo-client apollo-link-http apollo-cache-persist apollo-cache-inmemory graphql graphql-tag moment

I know what you’re thinking — that’s a lot of dependencies. If you have troubles setting up a Vue project, you can find out how to do that here. We should also include vuetify and the vue-router if we want to style the project and create additional routes.

In our ‘src’ folder, we can create a ‘config’ folder. The structure should look like this:

|src
  |config
     -graphql.js
     -index.js
     -LineChart.js
  |pages
      -login.vue
  |router
      -index.js
  App.vue
  main.js

We will have to set up our graphql client in the src/config/index.js file.

import {ApolloClient} from 'apollo-client'
import {HttpLink} from 'apollo-link-http'
import {InMemoryCache} from 'apollo-cache-inmemory'
import {CachePersistor} from 'apollo-cache-persist'

const httpLink = new HttpLink({
    uri:"http://localhost:4000"
})
export const cache = new InMemoryCache()

const apolloClient = new ApolloClient({
    link: httpLink,
    cache: cache
})

export const persistor = new CachePersistor({
    cache,
    storage: window.localStorage
})

export default apolloClient

Make sure the uri is on the same port as your apollo server, by default the apollo server runs on port 4000. Our apollo client is then set up with the httpLink and the cache.

The src/config/graphql.js file should look like this:

import gql from 'graphql-tag'

export const IMAP_MUTATION = gql `mutation imapMutation($email: String!, $password: String!) {
    imapMutation(
        email: $email,
        password: $password
    ){
        email
        data{
            numberOfFollowers
            date
        }
    }
}`

This query will submit the email and the password of the user to the imapMutation and then get the email and the data(graphPoints) from the apollo server.

Then, we create our chart component in the src/config/LineChart.js file. We can use charts ranging from bar charts to histograms. A line chart was used in this example.

import {Line, mixins} from 'vue-chartjs'
const {reactiveProp} = mixins

export default {
    extends: Line,
    mixins: [reactiveProp],
    props:['options'],
    mounted(){
        this.renderChart(this.chartData,this.options)
    }
}

We can import the ‘vue-apollo’ package in our main.js file and include the apolloProvider when initializing the app.

import Vue from 'vue'
import './plugins/vuetify'
import router from './router/index'
import App from './App.vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/dist/vuetify.min.css';

//Graphql
import apolloClient from './config/index'
import VueApollo from 'vue-apollo'

Vue.use(VueApollo)
Vue.use(Vuetify)

//apolloProvider
const apolloProvider = new VueApollo({
  defaultClient: apolloClient
})

Vue.config.productionTip = false



new Vue({
  el: "#app",
  template: '<App/>',
  router,
  apolloProvider,
  components: {App}
})

Finally, we will set up the src/pages/login.vue file, which we should have configured as the component for the default home route ‘/’ in the src/router/index.js file.

<template>
    <div>
        <div v-show="!graphReady">
            <v-form
            lazy-validation
            v-model="valid"
            ref="form"
            >
                <v-text-field
                v-model="email"
                label="Email"
                required
                ></v-text-field>
                <v-text-field
                v-model="password"
                type="password"
                label="Password"
                required
                ></v-text-field>
                <v-btn
                :disabled="!valid"
                @click="signup"
                >
                    submit
                </v-btn>
            </v-form>
            </div>
            <div v-show="graphReady">
                <h1>Hey <span v-if="responseMail">{{responseMail.email}},</span></h1>
               
                <line-chart :chart-data="graphPoints" :options="options" />
            </div>
    </div>
</template>

<script>
import {IMAP_MUTATION} from '../config/graphql'
import LineChart from '../config/LineChart.js'
import apolloClient from '../config/index'
import moment from 'moment'
import gql from 'graphql-tag'
export default {
    components: {
        LineChart
    },
    data(){
        return {
            valid: true,
            email: null,
            password: null,
            graphReady: false,
            responseMail: null,
            options:{
                legend:{
                    display: false
                },
                responsive: true,
                maintainAspectRatio: false,
                scales:{
                    xAxes:[{
                        gridLines:{
                            display: true,
                            lineWidth: 1,
                            drawBorder: false
                        }
                    }],
                    yAxes:[{
                        gridLines:{
                            display: true,
                            lineWidth: 3,
                            drawBorder: false
                        }
                    }]
                }
            },
            graphPoints: null

        }
    },
    methods: {
        signup(){
            if (this.$refs.form.validate()) {
                /*Include no-cache policy for this route
                  to avoid caching passwords */
                this.$apollo
                    .mutate({
                        mutation: IMAP_MUTATION,
                        variables:{
                            email: this.email,
                            password: this.password
                        }
                        , fetchPolicy: 'no-cache'
                    })
                    .then(response => {
                        this.snackbar = false;
                       
                        this.responseMail = response.data.imapMutation;
                        this.graphReady = true;
                        return this.responseMail
                    })
                    .then(res =>{ this.plotGraph() })
                    .catch(e =>{ console.log(e) })
            }    
        },
        plotGraph(){
            this.graphPoints = {
                labels:[],
                datasets: [
                    {
                        label: 'Medium followers',
                        borderColor: 'green',
                        fill:false,
                        lintTension: 0,
                        pointBackgroundColor: 'red',
                        data:[]
                    }
                ]
            }
            if(this.responseMail){  
                this.responseMail.data.map(point =>{
                    this.graphPoints.labels.push(this.formatDate(point.date.split('T')[0]))
                    this.graphPoints.datasets[0].data.push(point.numberOfFollowers)
                  
                })
            }
        },
        formatDate(date){
            return moment(date, 'YYYY-MM-DD').format('Do MMM YY')
        }
    }
}
</script>

So we created a basic form that accepts the email and password of the user and then send this data to the server through the signup() method. The plotGraph() method loops through the response(graphPoints) and pushes the dates to the labels array and the number of followers to the data array.

After styling using the ‘options’ object, we should have something like the screenshot shown during the introduction to this project.

Conclusion

You can still do more with your personal project, but the aim of this was to show how to work with the node-imap package, and how apollo works as a server and as a client. If you had any problems with this project, you can leave a comment or send a message on Twitter.

You can try the live version of this, or you could check out the repositories for the client and server applications on GitHub.

If you want to learn new technologies and frameworks, you can do so here and if you learned anything at all, please do well to clap below.

I will like to thank Wes Wagner for his feedback in making this article.

Thanks!

Learn More

The Complete Node.js Developer Course (2nd Edition)

Learn and Understand NodeJS

Node JS: Advanced Concepts

GraphQL: Learning GraphQL with Node.Js

Angular (Angular 2+) & NodeJS - The MEAN Stack Guide

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

Nuxt.js - Vue.js on Steroids

Vue.js Fast Crash Course

The Complete JavaScript Course 2018: Build Real Projects!

GraphQL with React: The Complete Developers Guide

GraphQL with Angular & Apollo - The Full-stack Guide

GraphQL: Learning GraphQL with Node.Js

Complete guide to building a GraphQL API

Original source: https://medium.freecodecamp.org

Suggest