Skip to content

Instantly share code, notes, and snippets.

@king1600
Created August 7, 2017 03:22
Show Gist options
  • Save king1600/3cfc31847cca66e1f82131fe897e2c90 to your computer and use it in GitHub Desktop.
Save king1600/3cfc31847cca66e1f82131fe897e2c90 to your computer and use it in GitHub Desktop.
Cleverbot API Wrapper server
const http = require('http');
const crypto = require('crypto');
const XVIS = 'TEI939AFFIAGAYQZ';
const HOST = 'http://www.cleverbot.com';
const BASE = `${HOST}/webservicemin`;
const UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';
class OrderedParams extends Array {
constructor(...args) {
super(...args);
}
getParam(key) {
for (let param of this)
if (param.key === key)
return param.value;
return undefined;
}
setParam(key, value) {
for (let i in this)
if (this[i].key === key)
return (this[i].value = value);
return this.push({key: key, value: value});
}
};
class CleverbotSession {
constructor() {
this.stimulus;
this.sessionId;
this.prepared = false;
this.conversation = [];
this.params = new OrderedParams();
this.params.setParam('uc', 'UseOfficialCleverbotAPI');
this.defaultHeaders = {
'Origin': HOST,
'Referer': `${HOST}/`,
'User-Agent': UserAgent,
'Cookie': {'XVIS': XVIS},
};
}
async ask(question) {
// url encode question
this.stimulus = encodeURIComponent(question);
// modify parameters if session is prepared
if (this.prepared) {
this.params.setParam('in', this.stimulus);
this.params.setParam('ns',
(parseInt(this.params.getParam('ns')) + 1) + '');
}
// perform request and initialized if needed
let resp = await this._sendResponse();
if (this.params.length < 2) this.prepare(resp);
this.conversation.push(question);
this.params.setParam('out', resp.headers.cboutput);
// parse the response
let parsed = resp.data.split('\r\r\r\r\r\r')
.slice(0, -1).map(line => line.split('\r'));
if (parsed[0][1] === 'DENIED')
throw new Error('Serice Error');
let answer = parsed[0][0];
this.conversation.push(answer);
// update the session information & return retrieved answer
this.sessionId = parsed[0][1];
this.params.setParam('xai', this.defaultHeaders.Cookie.XAI
+ ',' + parsed[0][2]);
this.defaultHeaders.Cookie.CBSTATE =
'&&0&&0&' + this.params.getParam('ns') + '&' +
this.conversation.map(c => encodeURIComponent(c)).join('&');
return answer;
}
/**
* Send HTTP Response with message parameters
*/
async _sendResponse() {
// build conversation string
let convoString = [];
const convo = this.conversation
if (this.conversation.length > 0)
for (let v = 2, i = convo.length - 1; i > -1; i--, v++)
convoString.push(`vText${v}=${encodeURIComponent(convo[i])}`);
convoString = convoString.join('&');
if (convoString.length > 0)
convoString = '&' + convoString;
// built rest of the url
let session = this.sessionId ? '&sessionid=' + this.sessionId : '';
let data = 'stimulus=' + this.stimulus + convoString;
data += session.length > 0 ? '&cb_settings_language=en' : '';
data += '&cb_settings_scripting=no' + session + '&islearning=1';
data += '&icognoid=wsf&icognocheck=';
// append icogcheck-hash & add params
data += crypto.createHash('md5')
.update(data.substring(7, 33)).digest('hex');
let url = BASE + '?' + this.params.map(p => {
let value = p.key === 'xai' ? p.value : encodeURIComponent(p.value);
return p.key + '=' + value;
}).join('&');
// perform http request
const headers = {'Content-Type': 'text/plain;charset=UTF-8'};
return await this.request('POST', url, headers, data);
}
/**
* Load initial data for session
*/
prepare(resp) {
this.prepared = true;
this.sessionId = resp.headers['cbconvid'];
this.defaultHeaders.CBSID = this.sessionId;
this.params.setParam('out', '');
this.params.setParam('in', '');
this.params.setParam('bot', 'c');
this.params.setParam('cbsid', this.sessionId);
this.params.setParam('xai', this.defaultHeaders.Cookie.XAI);
this.params.setParam('ns', '1');
this.params.setParam('al', '');
this.params.setParam('dl', 'en');
this.params.setParam('flag', '');
this.params.setParam('user', '');
this.params.setParam('mode', '1');
this.params.setParam('alt', '0');
this.params.setParam('reac', '');
this.params.setParam('emo', '');
this.params.setParam('sou', 'website');
this.params.setParam('xed', '');
}
/**
* Small Cookiejar implementation that wraps requests
*/
async request(method, url, headers={}, body=null) {
// add default headers
Object.keys(this.defaultHeaders).forEach(key => {
headers[key] = this.defaultHeaders[key];
});
// add session cookies
headers['Cookie'] = Object.keys(headers['Cookie'])
.map(c => `${c}=${headers['Cookie'][c]}`).join('; ');
// perform request
let resp = await CleverbotSession._request(method, url, headers, body);
// set cookies for session
if ('set-cookie' in resp.headers) {
resp.headers['set-cookie'].forEach(cookie => {
let [key, value] = cookie.split(';')[0].split('=');
this.defaultHeaders['Cookie'][key] = value;
});
}
// return http result
return resp;
}
/**
* Promisified wrapper of the _rawRequest function
*/
static async _request(method, url, headers={}, body=null) {
return new Promise((resolve, reject) => {
CleverbotSession._rawRequest(resolve, reject, method, url, headers, body);
});
}
/**
* Perform raw http request using resolve and reject from a promise
*/
static _rawRequest(resolve, reject, method, url, headers={}, body=null) {
if (!url.endsWith('&')) url += '&'; // the client uses this
// create http options
const options = {
method: method,
headers: headers,
path: url.split(HOST)[1],
host: HOST.split('://')[1],
};
// perform http request
let request = http.request(options, resp => {
// handle redirects and errors
if (resp.statusCode >= 400)
return reject(resp.statusMessage);
if (resp.statusCode >= 300)
return CleverbotSession._rawRequest(resolve, reject,
method, resp.headers['location'], headers, body);
// gather response data
let content = Buffer.alloc(0);
resp.on('data', chunk => {
content = Buffer.concat([content, chunk]);
})
resp.on('end', () => {
resolve({headers: resp.headers, data: content.toString()});
});
});
// write request body if any
if (body) request.write(body);
request.end();
}
};
class CleverbotServer {
constructor(port = 8000) {
this.port = port; // the server port
this.sessions = {}; // client sessions
this.maxSessions = 5; // max sessions per ip address
this.expires = 5 * 60 * 1000; // session idle expire time
// create http server
this.server = http.createServer((...args) => {
this.handler(...args);
});
// start the server
this.server.listen(port, err => {
if (err) throw err;
console.log('Server started on http://localhost:' + port);
})
}
/**
* Create a UUID 4 for a session id
*/
createId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
let r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
/**
* Send an http response to a client
* @param {http.IncomingMessage} resp the response to send to
* @param {number} code http status code
* @param {Object} data json data to response do
*/
write(resp, code, data) {
data = JSON.stringify(data);
resp.writeHead(code, {
'Content-Type': 'application/json',
'Content-Length': data.length
});
resp.end(data);
}
/**
* Create a new client session
* @param {http.Request} req to get ip address from
*/
createSession(req) {
// get the ip address
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (ip.includes(':')) ip = ip.split(':').slice(-1)[0];
// check active sessions, if max, dont create a session
let active = 0;
Object.keys(this.sessions).forEach(k => {
if (this.sessions[k].ip === ip) active++;
});
if (active >= this.maxSessions) return null;
// create a new session
let session = {
ip: ip,
id: this.createId(),
bot: new CleverbotSession(),
}
this.sessions[session.id] = session;
// make session expire after time period
session.entry = setTimeout(() => {
if (session.id in this.sessions)
delete this.sessions[session.id];
}, this.expires);
// return newly created session
return session;
}
async handler(req, resp) {
let params = {}; // http parameters
let path = req.url.split('?')[0]; // http path
// route needs path
if (!['bot'].includes(path.split('/')[1]))
return this.write(resp, 404, {'error':"Path not found"});
// extract parameter queries
let query = req.url.split('?')[1];
if (query && query.length > 2)
query.split('&').forEach(q => {
let [key, ...value] = q.split('=');
value = value.join('=');
params[key] = value;
})
// only allow get requests
if (req.method.toUpperCase() === 'GET') {
// extract session id from path
let session = path.split('/').slice(1);
if (session.length < 2)
return this.write(resp, 400, {'error':'No session id found'});
session = session[1];
// create session if not existing
if (!(session in this.sessions)) {
session = this.createSession(req);
if (!session)
return this.write(resp, 400, {'error':'Maximum concurrent sessions reached'});
// use existing session and reset its expire timer
} else {
session = this.sessions[session];
clearTimeout(session.entry);
session.entry = setTimeout(() => {
if (session.id in this.sessions)
delete this.sessions[session.id];
}, this.expires);
this.sessions[session.id] = session;
}
// check if there is as ask paraeter
if (!('ask' in params))
return this.write(resp, 400, {'error':'No ask parameter set'});
// make request and return cleverbot response with session id
let response = await session.bot.ask(decodeURIComponent(params['ask']));
return this.write(resp, 200, {id: session.id, response: response});
// dont allow other type of requests
} else {
return this.write(resp, 400, {'error':'only GET requests are accepted'});
}
}
};
// start the server
const server = new CleverbotServer(8080);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment