Created
August 7, 2017 03:22
-
-
Save king1600/3cfc31847cca66e1f82131fe897e2c90 to your computer and use it in GitHub Desktop.
Cleverbot API Wrapper server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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