-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b4c4202
commit 63404ee
Showing
6 changed files
with
228 additions
and
0 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
const axios = require('axios'); | ||
const uuid = require('uuid'); | ||
|
||
const { UnauthorizedError } = require('../../common/error'); | ||
|
||
const TTS_TOKEN_PREFIX = 'tts-token:'; | ||
|
||
module.exports = function TTSController(redisClient) { | ||
/** | ||
* @api {get} /tts/generate Generate a mp3 file from a text | ||
* @apiName generate | ||
* @apiGroup TTS | ||
* | ||
* | ||
* @apiQuery {String} text The text to generate | ||
* @apiQuery {String} token Temporary token to have access to | ||
* | ||
* @apiSuccessExample {binary} Success-Response: | ||
* HTTP/1.1 200 OK | ||
*/ | ||
async function generate(req, res, next) { | ||
const instanceId = await redisClient.get(`${TTS_TOKEN_PREFIX}:${req.query.token}`); | ||
if (!instanceId) { | ||
throw new UnauthorizedError('Invalid TTS token.'); | ||
} | ||
// Streaming response to client | ||
const { data, headers } = await axios({ | ||
url: process.env.TEXT_TO_SPEECH_URL, | ||
method: 'POST', | ||
body: req.body, | ||
headers: { | ||
authorization: `Bearer ${process.env.TEXT_TO_SPEECH_API_KEY}`, | ||
}, | ||
responseType: 'stream', | ||
}); | ||
res.setHeader('content-type', headers['content-type']); | ||
res.setHeader('content-length', headers['content-length']); | ||
data.pipe(res); | ||
} | ||
|
||
/** | ||
* @api {post} /tts/token Get temporary token to access TTS API | ||
* @apiName getToken | ||
* @apiGroup TTS | ||
* | ||
* @apiBody {String} text The text to generate | ||
* | ||
* @apiSuccessExample {binary} Success-Response: | ||
* HTTP/1.1 200 OK | ||
* | ||
* { | ||
* "token": "ac365e90-78f1-482a-8afa-af326d5647a4", | ||
* "url": "https://url_of_the_file" | ||
* } | ||
*/ | ||
async function getTemporaryToken(req, res, next) { | ||
const token = uuid.v4(); | ||
await redisClient.set(`${TTS_TOKEN_PREFIX}:${token}`, req.instance.id, { | ||
EX: 5 * 60, // 5 minutes in seconds | ||
}); | ||
const url = `${process.env.GLADYS_PLUS_BACKEND_URL}/tts/generate?token=${token}&text=${encodeURIComponent( | ||
req.body.text, | ||
)}`; | ||
res.json({ token, url }); | ||
} | ||
|
||
return { | ||
generate, | ||
getTemporaryToken, | ||
}; | ||
}; |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
const { RateLimiterRedis } = require('rate-limiter-flexible'); | ||
|
||
const { TooManyRequestsError } = require('../common/error'); | ||
const asyncMiddleware = require('./asyncMiddleware'); | ||
|
||
const MAX_REQUESTS = parseInt(process.env.TTS_MAX_REQUESTS_PER_MONTH_PER_ACCOUNT, 10); | ||
|
||
module.exports = function TTSRateLimit(logger, redisClient, db) { | ||
const limiter = new RateLimiterRedis({ | ||
storeClient: redisClient, | ||
keyPrefix: 'rate_limit:tts_api', | ||
points: MAX_REQUESTS, // max request per month | ||
duration: 30 * 24 * 60 * 60, // 30 days | ||
}); | ||
return asyncMiddleware(async (req, res, next) => { | ||
const instanceWithAccount = await db.t_account | ||
.join({ | ||
t_instance: { | ||
type: 'INNER', | ||
on: { | ||
account_id: 'id', | ||
}, | ||
}, | ||
}) | ||
.findOne({ | ||
't_instance.id': req.instance.id, | ||
}); | ||
const uniqueIdentifier = instanceWithAccount.id; | ||
// we check if the current account is rate limited | ||
const limiterResult = await limiter.get(uniqueIdentifier); | ||
if (limiterResult && limiterResult.consumedPoints > MAX_REQUESTS) { | ||
logger.warn(`TTS Rate limit: Account ${uniqueIdentifier} has been querying too much this route`); | ||
throw new TooManyRequestsError('Too many requests this month.'); | ||
} | ||
|
||
// We consume one credit | ||
try { | ||
await limiter.consume(uniqueIdentifier); | ||
} catch (e) { | ||
logger.warn(`TTS Rate limit: Account ${uniqueIdentifier} has been querying too much this route`); | ||
logger.warn(e); | ||
throw new TooManyRequestsError('Too many requests this month.'); | ||
} | ||
|
||
next(); | ||
}); | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
const request = require('supertest'); | ||
const nock = require('nock'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const { expect } = require('chai'); | ||
const { RateLimiterRedis } = require('rate-limiter-flexible'); | ||
|
||
const configTest = require('../../../tasks/config'); | ||
|
||
const voiceFile = fs.readFileSync(path.join(__dirname, './voice.mp3')); | ||
|
||
describe('TTS API', () => { | ||
before(() => { | ||
process.env.TEXT_TO_SPEECH_URL = 'https://test-tts.com'; | ||
process.env.TEXT_TO_SPEECH_API_KEY = 'my-token'; | ||
process.env.GLADYS_PLUS_BACKEND_URL = 'http://test-api.com'; | ||
}); | ||
it('should get token + get mp3', async () => { | ||
nock(process.env.TEXT_TO_SPEECH_URL, { encodedQueryParams: true }) | ||
.post('/', (body) => true) | ||
.reply(200, voiceFile, { | ||
'content-type': 'audio/mpeg', | ||
'content-length': 36362, | ||
}); | ||
await TEST_DATABASE_INSTANCE.t_account.update( | ||
{ | ||
id: 'b2d23f66-487d-493f-8acb-9c8adb400def', | ||
}, | ||
{ | ||
status: 'active', | ||
}, | ||
); | ||
const response = await request(TEST_BACKEND_APP) | ||
.post('/tts/token') | ||
.set('Accept', 'application/json') | ||
.set('Authorization', configTest.jwtAccessTokenInstance) | ||
.send({ text: 'Bonjour, je suis Gladys' }) | ||
.expect('Content-Type', /json/) | ||
.expect(200); | ||
expect(response.body).to.have.property('token'); | ||
expect(response.body).to.have.property( | ||
'url', | ||
`http://test-api.com/tts/generate?token=${response.body.token}&text=Bonjour%2C%20je%20suis%20Gladys`, | ||
); | ||
const responseMp3File = await request(TEST_BACKEND_APP) | ||
.get(`/tts/generate?token=${response.body.token}&text=bonjour`) | ||
.set('Accept', 'application/json') | ||
.set('Authorization', configTest.jwtAccessTokenInstance) | ||
.send() | ||
.expect('Content-Type', 'audio/mpeg') | ||
.expect(200); | ||
expect(responseMp3File.text).to.deep.equal(voiceFile.toString()); | ||
}); | ||
it('should return 401', async () => { | ||
const response = await request(TEST_BACKEND_APP) | ||
.get(`/tts/generate?token=toto&text=bonjour`) | ||
.set('Accept', 'application/json') | ||
.set('Authorization', configTest.jwtAccessTokenInstance) | ||
.send() | ||
.expect('Content-Type', /json/) | ||
.expect(401); | ||
expect(response.body).to.deep.equal({ | ||
error_code: 'UNAUTHORIZED', | ||
status: 401, | ||
}); | ||
}); | ||
it('should return 429, too many requests', async () => { | ||
await TEST_DATABASE_INSTANCE.t_account.update( | ||
{ | ||
id: 'b2d23f66-487d-493f-8acb-9c8adb400def', | ||
}, | ||
{ | ||
status: 'active', | ||
}, | ||
); | ||
const limiter = new RateLimiterRedis({ | ||
storeClient: TEST_LEGACY_REDIS_CLIENT, | ||
keyPrefix: 'rate_limit:tts_api', | ||
points: 100, // max request per month | ||
duration: 30 * 24 * 60 * 60, // 30 days | ||
}); | ||
await limiter.consume('b2d23f66-487d-493f-8acb-9c8adb400def', 100); | ||
const response = await request(TEST_BACKEND_APP) | ||
.post('/tts/token') | ||
.set('Accept', 'application/json') | ||
.set('Authorization', configTest.jwtAccessTokenInstance) | ||
.send() | ||
.expect('Content-Type', /json/) | ||
.expect(429); | ||
expect(response.body).to.deep.equal({ | ||
status: 429, | ||
error_code: 'TOO_MANY_REQUESTS', | ||
error_message: 'Too many requests this month.', | ||
}); | ||
}); | ||
}); |
Binary file not shown.