├── .gitignore ├── .npmignore ├── .prettierrc ├── babel.config.js ├── src ├── utils.js └── index.js ├── dist ├── utils.js └── index.js ├── .eslintrc ├── LICENSE ├── package.json ├── README.md └── types └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | src -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | plugins: [ 13 | ['@babel/plugin-proposal-private-methods', { loose: true }], 14 | ['@babel/plugin-proposal-class-properties', { loose: true }], 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { parseString } from 'xml2js'; 2 | 3 | const getCookieValueAndHeader = (cookieHeader, cookieName) => { 4 | const match = cookieHeader.match(new RegExp(`${cookieName}=(?.*?);`)); 5 | return { cookieValue: match[1], cookieHeader: match[0] }; 6 | }; 7 | 8 | const parseXml = xml => { 9 | return new Promise((resolve, reject) => { 10 | parseString( 11 | xml, 12 | { explicitArray: false, explicitRoot: false }, 13 | (err, result) => { 14 | if (err) return reject(err); 15 | return resolve(result); 16 | }, 17 | ); 18 | }); 19 | }; 20 | 21 | export { getCookieValueAndHeader, parseXml }; 22 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.parseXml = exports.getCookieValueAndHeader = void 0; 7 | 8 | var _xml2js = require("xml2js"); 9 | 10 | const getCookieValueAndHeader = (cookieHeader, cookieName) => { 11 | const match = cookieHeader.match(new RegExp(`${cookieName}=(?.*?);`)); 12 | return { 13 | cookieValue: match[1], 14 | cookieHeader: match[0] 15 | }; 16 | }; 17 | 18 | exports.getCookieValueAndHeader = getCookieValueAndHeader; 19 | 20 | const parseXml = xml => { 21 | return new Promise((resolve, reject) => { 22 | (0, _xml2js.parseString)(xml, { 23 | explicitArray: false, 24 | explicitRoot: false 25 | }, (err, result) => { 26 | if (err) return reject(err); 27 | return resolve(result); 28 | }); 29 | }); 30 | }; 31 | 32 | exports.parseXml = parseXml; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 3 | "parser": "babel-eslint", 4 | "env": { 5 | "node": true, 6 | "jest/globals": true 7 | }, 8 | "plugins": ["jest"], 9 | "settings": { 10 | "import/resolver": { 11 | "node": { 12 | "paths": ["./"] 13 | }, 14 | "babel-module": {} 15 | } 16 | }, 17 | "rules": { 18 | "prettier/prettier": "error", 19 | "no-param-reassign": 0, 20 | "no-use-before-define": 0, 21 | "no-console": 0, 22 | "no-shadow": 0, 23 | "camelcase": 0, 24 | "comma-dangle": ["error", "always-multiline"], 25 | "class-methods-use-this": 0, 26 | "jest/no-disabled-tests": "warn", 27 | "jest/no-focused-tests": "error", 28 | "jest/no-identical-title": 0, 29 | "jest/prefer-to-have-length": "warn", 30 | "jest/valid-expect": "error", 31 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 32 | "import/prefer-default-export": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Omer Dangoor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "otter.ai-api", 3 | "version": "1.0.1", 4 | "description": "Unofficial API for Otter.ai", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "babel src --out-dir dist", 8 | "watch": "babel src --watch --out-dir dist --verbose", 9 | "test": "jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/omerdn1/otter-api.git" 14 | }, 15 | "author": "Omer Dangoor", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/omerdn1/otter-api/issues" 19 | }, 20 | "homepage": "https://github.com/omerdn1/otter-api#readme", 21 | "dependencies": { 22 | "axios": "^0.19.0", 23 | "request": "^2.88.0", 24 | "request-promise": "^4.2.5", 25 | "xml2js": "^0.4.22" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.7.5", 29 | "@babel/core": "^7.7.5", 30 | "@babel/node": "^7.7.4", 31 | "@babel/plugin-proposal-class-properties": "^7.7.4", 32 | "@babel/plugin-proposal-private-methods": "^7.7.4", 33 | "@babel/preset-env": "^7.7.6", 34 | "babel-eslint": "^10.0.3", 35 | "eslint": "^6.7.2", 36 | "eslint-config-airbnb-base": "^14.0.0", 37 | "eslint-config-prettier": "^6.7.0", 38 | "eslint-plugin-import": "^2.19.1", 39 | "eslint-plugin-jest": "^23.1.1", 40 | "eslint-plugin-prettier": "^3.1.1", 41 | "jest": "^24.9.0", 42 | "nodemon": "^2.0.2", 43 | "prettier": "^1.19.1" 44 | }, 45 | "types": "types/index.d.ts" 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # otter.ai-api 2 | 3 | This is an unofficial API for [otter.ai](http://otter.ai)'s speech-to-text service. 4 | 5 | ## Contents 6 | 7 | - [Installation](#installation) 8 | - [Setup](#setup) 9 | - [Methods](#methods) 10 | - [Get Speeches](#get-speeches) 11 | - [Get Speech](#get-speech) 12 | - [Speech Search](#speech-search) 13 | - [TODO: add all methods](#todo--add-all-methods) 14 | - [License](#license) 15 | 16 | ## Installation 17 | 18 | `npm install --save otter.ai-api` 19 | 20 | ## Setup 21 | 22 | ```jsonc 23 | import OtterApi from 'otter.ai-api'; 24 | 25 | const otterApi = new OtterApi({ 26 | email: 'email@example.com', // Your otter.ai email 27 | password: 'abc123!#', // Your otter.ai password 28 | }); 29 | ``` 30 | 31 | ## Methods 32 | 33 | ### Get Speeches 34 | 35 | Receive an array of all user speeches 36 | 37 | **Method**: 38 | 39 | ```js 40 | otterApi.getSpeeches(); 41 | ``` 42 | 43 | **Response**: 44 | 45 | ```jsonc 46 | [ 47 | ... 48 | { 49 | "speech_id": "77NXWSPLSSXQ56JU", // Speech ID 50 | "start_time": 1174447006, // Start date in Unix Epoch 51 | "end_time": 0, 52 | "modified_time": 1174447006, // Last modified date in Unix Epoch 53 | "deleted": false, 54 | "duration": 250, // Duration of the speech in seconds 55 | "title": "Example Title", // Title 56 | "summary": "example, test, sample, showcase", // Keywords 57 | "from_shared": false, 58 | "shared_with": [], 59 | "unshared": false, 60 | "shared_by": null, 61 | "owner": {}, // Owner info 62 | "shared_groups": [], 63 | "can_edit": true, 64 | "can_comment": true, 65 | "is_read": false, 66 | "process_finished": true, 67 | "upload_finished": true, 68 | "hasPhotos": 0, 69 | "download_url": "https://downloadurl.com", // Download URL 70 | "transcript_updated_at": 1174447006, // Last transcript update date in Unix Epoch 71 | "images": [], 72 | "speakers": [], // Array of speakers 73 | "word_clouds": [], // Word cloud 74 | "live_status": "none", 75 | "live_status_message": "", 76 | "public_share_url": null, 77 | "folder": null, 78 | "created_at": 1174447006 // Creation date in Unix Epoch 79 | }, 80 | ... 81 | ] 82 | ``` 83 | 84 | ### Get Speech 85 | 86 | Receive an object of a particular speech 87 | 88 | **Method**: 89 | 90 | ```js 91 | otterApi.getSpeech(speechId); 92 | ``` 93 | 94 | **Parameters**: 95 | 96 | - `speechId` - **required**, an ID of the speech. 97 | 98 | **Response**: 99 | 100 | ```jsonc 101 | { 102 | ...getSpeeches response, 103 | "transcripts": [ 104 | ... 105 | { 106 | "start_offset": 132400, // Transcript start offset in milliseconds 107 | "end_offset": 191240, // Transcript end offset in milliseconds 108 | "speaker_model_label": null, 109 | "transcript": "This is a sample transcript.", // Transcript 110 | "id": 9928892812, // Transcript ID 111 | "alignment": [], // Word timestamps 112 | "speaker_id": null, // Speaker ID 113 | "uuid": "l12322lx-21b4-4623-a51f-lkdlsd2132", // Transcript UUID 114 | "speaker_edited_at": null, 115 | "created_at": "2019-12-19 14:29:21", // Trasncript creation date 116 | "label": null, // Transcript label 117 | "sig": "", 118 | "speech_id": "77NXWSPLSSXQ56JU" // Speech ID 119 | } 120 | ... 121 | ] 122 | } 123 | ``` 124 | 125 | ### Speech Search 126 | 127 | Receive an array of search results given a particular query 128 | 129 | **Method**: 130 | 131 | ```js 132 | otterApi.speechSearch(query); 133 | ``` 134 | 135 | **Parameters**: 136 | 137 | - `query` - **required**, a search query 138 | 139 | **Response**: 140 | 141 | ```jsonc 142 | [ 143 | ... 144 | { 145 | "user_id": 1111117, // User ID 146 | "title": "Example Title", // Speech title 147 | "start_time": 1174447006, // Start date in Unix Epoch 148 | "matched_transcripts": [ 149 | ... 150 | { 151 | "transcript_id": 9928892812, // Transcript ID 152 | "matched_transcript": "This is a sample transcript.", // Transcript 153 | "highlight_spans": [], // Highlight spans over transcript 154 | }, 155 | ... 156 | "groups": [], 157 | "appid": null, 158 | "duration": 250, // Duration of the speech in seconds 159 | "speech_id": "77NXWSPLSSXQ56JU" // Speech ID 160 | ] 161 | ... 162 | } 163 | ] 164 | ``` 165 | 166 | ### TODO: add all methods 167 | 168 | ## License 169 | 170 | MIT 171 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import rp from 'request-promise' 3 | import {getCookieValueAndHeader, parseXml} from './utils' 4 | 5 | const API_BASE_URL = 'https://otter.ai/forward/api/v1'; 6 | const AWS_S3_URL = 'https://s3.us-west-2.amazonaws.com'; 7 | const CSRF_COOKIE_NAME = 'csrftoken'; 8 | 9 | export class OtterApi { 10 | constructor(options = {}) { 11 | this.options = options; 12 | this.user = {}; 13 | this.csrfToken = ''; 14 | } 15 | 16 | login = async () => { 17 | const { email, password } = this.options; 18 | 19 | if (!email || !password) { 20 | throw new Error( 21 | "Email and/or password were not given. Can't perform authentication to otter.ai", 22 | ); 23 | } 24 | const csrfResponse = await axios({ 25 | method: 'GET', 26 | url: `${API_BASE_URL}/login_csrf`, 27 | }); 28 | const { 29 | cookieValue: csrfToken, 30 | cookieHeader: csrfCookie, 31 | } = getCookieValueAndHeader( 32 | csrfResponse.headers['set-cookie'][0], 33 | CSRF_COOKIE_NAME, 34 | ); 35 | 36 | const response = await axios({ 37 | method: 'GET', 38 | url: `${API_BASE_URL}/login`, 39 | params: { 40 | username: email, 41 | }, 42 | headers: { 43 | authorization: `Basic ${Buffer.from(`${email}:${password}`).toString( 44 | 'base64', 45 | )}`, 46 | 'x-csrftoken': csrfToken, 47 | cookie: csrfCookie, 48 | }, 49 | withCredentials: true, 50 | }); 51 | 52 | const cookieHeader = `${response.headers['set-cookie'][0]}; ${response.headers['set-cookie'][1]}`; 53 | ({ cookieValue: this.csrfToken } = getCookieValueAndHeader( 54 | response.headers['set-cookie'][0], 55 | CSRF_COOKIE_NAME, 56 | )); 57 | 58 | this.user = response.data.user; 59 | 60 | axios.defaults.headers.common.cookie = cookieHeader; 61 | 62 | //todo: interferes with alfred :( ` 63 | // console.log('Successfuly logged in to Otter.ai'); 64 | 65 | return response; 66 | }; 67 | 68 | getSpeeches = async (params = 69 | { 70 | pageSize: undefined, 71 | lastLoadTs: undefined, 72 | modifiedAfter: undefined, 73 | }, 74 | ) => { 75 | // Example query from the browser: 76 | // https://otter.ai/forward/api/v1/speeches?userid=1368930&folder=0&page_size=12&last_load_ts=1633203526&modified_after=1633696948 77 | 78 | const {data} = await axios({ 79 | method: 'GET', 80 | url: `${API_BASE_URL}/speeches`, 81 | params: { 82 | userid: this.user.id, 83 | page_size: params.pageSize, 84 | last_load_ts: params.lastLoadTs, 85 | modified_after: params.modifiedAfter, 86 | }, 87 | }) 88 | 89 | return data.speeches 90 | } 91 | 92 | getSpeech = async speech_id => { 93 | const { data } = await axios({ 94 | method: 'GET', 95 | url: `${API_BASE_URL}/speech`, 96 | params: { 97 | speech_id, 98 | userid: this.user.id, 99 | }, 100 | }); 101 | 102 | return data.speech; 103 | }; 104 | 105 | speechSearch = async query => { 106 | const { data } = await axios({ 107 | method: 'GET', 108 | url: `${API_BASE_URL}/speech_search`, 109 | params: { 110 | query, 111 | userid: this.user.id, 112 | }, 113 | }); 114 | 115 | return data.hits; 116 | }; 117 | 118 | validateUploadService = () => 119 | axios({ 120 | method: 'OPTIONS', 121 | url: `${AWS_S3_URL}/speech-upload-prod`, 122 | headers: { 123 | Accept: '*/*', 124 | 'Access-Control-Request-Method': 'POST', 125 | Origin: 'https://otter.ai', 126 | Referer: 'https://otter.ai/', 127 | }, 128 | }); 129 | 130 | uploadSpeech = async file => { 131 | const uploadOptionsResponse = await axios({ 132 | method: 'GET', 133 | url: `${API_BASE_URL}/speech_upload_params`, 134 | params: { 135 | userid: this.user.id, 136 | }, 137 | headers: { 138 | Accept: '*/*', 139 | Connection: 'keep-alive', 140 | Origin: 'https://otter.ai', 141 | Referer: 'https://otter.ai/', 142 | }, 143 | }); 144 | 145 | delete uploadOptionsResponse.data.data.form_action; 146 | 147 | const xmlResponse = await rp({ 148 | method: 'POST', 149 | uri: `${AWS_S3_URL}/speech-upload-prod`, 150 | formData: { ...uploadOptionsResponse.data.data, file }, 151 | }); 152 | 153 | const { Bucket, Key } = await parseXml(xmlResponse); 154 | 155 | const finishResponse = await axios({ 156 | method: 'POST', 157 | url: `${API_BASE_URL}/finish_speech_upload`, 158 | params: { 159 | bucket: Bucket, 160 | key: Key, 161 | language: 'en', 162 | country: 'us', 163 | userid: this.user.id, 164 | }, 165 | headers: { 166 | 'x-csrftoken': this.csrfToken, 167 | }, 168 | }); 169 | 170 | return finishResponse.data; 171 | }; 172 | } 173 | 174 | /** 175 | * @param options 176 | * @returns {Promise} 177 | */ 178 | export const createClient = async (options) => { 179 | const client = new OtterApi(options) 180 | await client.login() 181 | return client 182 | } 183 | 184 | export default OtterApi; 185 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'otter.ai-api' { 2 | interface ClientOptions { 3 | email: string, 4 | password: string, 5 | } 6 | 7 | export interface Owner { 8 | id: number 9 | name: string 10 | email: string 11 | first_name: string 12 | last_name: string 13 | avatar_url: string 14 | } 15 | 16 | export interface WordCloud { 17 | variants: string[] 18 | score: string 19 | word: string 20 | } 21 | 22 | export interface SessionInfo { 23 | live_status: string 24 | live_status_message: string 25 | id?: any 26 | title: string 27 | offset: number 28 | } 29 | 30 | export interface Alignment { 31 | end: number 32 | endOffset: number 33 | start: number 34 | startOffset: number 35 | word: string 36 | } 37 | 38 | export interface Transcript { 39 | start_offset: number 40 | end_offset: number 41 | speaker_model_label: string 42 | transcript: string 43 | id: number 44 | alignment: Alignment[] 45 | speaker_id?: any 46 | uuid: string 47 | speaker_edited_at?: any 48 | created_at: string 49 | label: string 50 | sig: string 51 | speech_id: string 52 | } 53 | 54 | export interface MatchedTranscript { 55 | transcript_id: number 56 | matched_transcript: string 57 | uuid: string 58 | start_offset: number 59 | highlight_spans: HighlightSpan[] 60 | } 61 | 62 | export interface Speech { 63 | speech_id: string 64 | start_time: number 65 | end_time: number 66 | modified_time: number 67 | deleted: boolean 68 | duration: number 69 | title?: any 70 | summary: string 71 | from_shared: boolean 72 | shared_with: any[] 73 | unshared: boolean 74 | shared_by?: any 75 | owner: Owner 76 | shared_groups: any[] 77 | can_edit: boolean 78 | can_comment: boolean 79 | is_read: boolean 80 | process_finished: boolean 81 | upload_finished: boolean 82 | hasPhotos: number 83 | download_url: string 84 | transcript_updated_at: number 85 | images: any[] 86 | speakers: any[] 87 | word_clouds: WordCloud[] 88 | live_status: string 89 | live_status_message: string 90 | public_share_url: string 91 | folder?: any 92 | created_at: number 93 | access_seconds: number 94 | appid: string 95 | can_highlight: boolean 96 | create_method: string 97 | otid: string 98 | can_export: boolean 99 | timecode_offset?: any 100 | timezone?: any 101 | access_request?: any 102 | public_view: boolean 103 | has_started: boolean 104 | auto_record: boolean 105 | displayed_start_time: number 106 | session_info: SessionInfo[] 107 | has_hidden_transcript: boolean 108 | transcripts: Transcript[] 109 | realign_finished: boolean 110 | rematch_finished: boolean 111 | diarization_finished: boolean 112 | rematch_cutoff_time?: any 113 | annotations: any[] 114 | audio_url: string 115 | } 116 | 117 | 118 | export interface SpeechSummary { 119 | // todo unify with the Speech? 120 | speech_id: string 121 | start_time: number 122 | end_time: number 123 | modified_time: number 124 | deleted: boolean 125 | duration: number 126 | title: string | null 127 | summary: string 128 | from_shared: boolean 129 | shared_with: any[] 130 | unshared: boolean 131 | shared_by?: any 132 | owner: Owner 133 | shared_groups: any[] 134 | can_edit: boolean 135 | can_comment: boolean 136 | is_read: boolean 137 | process_finished: boolean 138 | upload_finished: boolean 139 | hasPhotos: number 140 | download_url: string 141 | transcript_updated_at: number 142 | images: any[] 143 | speakers: any[] 144 | word_clouds: WordCloud[] 145 | live_status: string 146 | live_status_message: string 147 | public_share_url?: any 148 | folder?: any 149 | created_at: number 150 | access_seconds: number 151 | appid: string 152 | can_highlight: boolean 153 | create_method: string 154 | otid: string 155 | can_export: boolean 156 | timecode_offset?: any 157 | timezone?: any 158 | access_request?: any 159 | public_view: boolean 160 | has_started: boolean 161 | auto_record: boolean 162 | displayed_start_time: number 163 | } 164 | 165 | 166 | export interface HighlightSpan { 167 | transcript_end: number 168 | span_text: string 169 | match_end: number 170 | match_start: number 171 | transcript_start: number 172 | } 173 | 174 | 175 | export interface SearchResult { 176 | matched_transcripts: MatchedTranscript[] 177 | appid: string 178 | user_id: number 179 | folder_id?: any 180 | groups: any[] 181 | title: string | null 182 | duration: number 183 | start_time: number 184 | speech_id: string 185 | speech_otid: string 186 | } 187 | 188 | export interface GetSpeechesParams { 189 | pageSize?: number, 190 | lastLoadTs?: number, 191 | modifiedAfter?: number, 192 | } 193 | 194 | interface OtterClient { 195 | getSpeeches(params?: GetSpeechesParams): Promise> 196 | 197 | getSpeech(id: string): Promise 198 | 199 | speechSearch(query: string): Promise> 200 | } 201 | 202 | function createClient(options: ClientOptions): Promise 203 | } 204 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = exports.createClient = exports.OtterApi = void 0; 7 | 8 | var _axios = _interopRequireDefault(require("axios")); 9 | 10 | var _requestPromise = _interopRequireDefault(require("request-promise")); 11 | 12 | var _utils = require("./utils"); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 15 | 16 | const API_BASE_URL = 'https://otter.ai/forward/api/v1'; 17 | const AWS_S3_URL = 'https://s3.us-west-2.amazonaws.com'; 18 | const CSRF_COOKIE_NAME = 'csrftoken'; 19 | 20 | class OtterApi { 21 | constructor(options = {}) { 22 | this.login = async () => { 23 | const { 24 | email, 25 | password 26 | } = this.options; 27 | 28 | if (!email || !password) { 29 | throw new Error("Email and/or password were not given. Can't perform authentication to otter.ai"); 30 | } 31 | 32 | const csrfResponse = await (0, _axios.default)({ 33 | method: 'GET', 34 | url: `${API_BASE_URL}/login_csrf` 35 | }); 36 | const { 37 | cookieValue: csrfToken, 38 | cookieHeader: csrfCookie 39 | } = (0, _utils.getCookieValueAndHeader)(csrfResponse.headers['set-cookie'][0], CSRF_COOKIE_NAME); 40 | const response = await (0, _axios.default)({ 41 | method: 'GET', 42 | url: `${API_BASE_URL}/login`, 43 | params: { 44 | username: email 45 | }, 46 | headers: { 47 | authorization: `Basic ${Buffer.from(`${email}:${password}`).toString('base64')}`, 48 | 'x-csrftoken': csrfToken, 49 | cookie: csrfCookie 50 | }, 51 | withCredentials: true 52 | }); 53 | const cookieHeader = `${response.headers['set-cookie'][0]}; ${response.headers['set-cookie'][1]}`; 54 | ({ 55 | cookieValue: this.csrfToken 56 | } = (0, _utils.getCookieValueAndHeader)(response.headers['set-cookie'][0], CSRF_COOKIE_NAME)); 57 | this.user = response.data.user; 58 | _axios.default.defaults.headers.common.cookie = cookieHeader; //todo: interferes with alfred :( ` 59 | // console.log('Successfuly logged in to Otter.ai'); 60 | 61 | return response; 62 | }; 63 | 64 | this.getSpeeches = async (params = { 65 | pageSize: undefined, 66 | lastLoadTs: undefined, 67 | modifiedAfter: undefined 68 | }) => { 69 | // Example query from the browser: 70 | // https://otter.ai/forward/api/v1/speeches?userid=1368930&folder=0&page_size=12&last_load_ts=1633203526&modified_after=1633696948 71 | const { 72 | data 73 | } = await (0, _axios.default)({ 74 | method: 'GET', 75 | url: `${API_BASE_URL}/speeches`, 76 | params: { 77 | userid: this.user.id, 78 | page_size: params.pageSize, 79 | last_load_ts: params.lastLoadTs, 80 | modified_after: params.modifiedAfter 81 | } 82 | }); 83 | return data.speeches; 84 | }; 85 | 86 | this.getSpeech = async speech_id => { 87 | const { 88 | data 89 | } = await (0, _axios.default)({ 90 | method: 'GET', 91 | url: `${API_BASE_URL}/speech`, 92 | params: { 93 | speech_id, 94 | userid: this.user.id 95 | } 96 | }); 97 | return data.speech; 98 | }; 99 | 100 | this.speechSearch = async query => { 101 | const { 102 | data 103 | } = await (0, _axios.default)({ 104 | method: 'GET', 105 | url: `${API_BASE_URL}/speech_search`, 106 | params: { 107 | query, 108 | userid: this.user.id 109 | } 110 | }); 111 | return data.hits; 112 | }; 113 | 114 | this.validateUploadService = () => (0, _axios.default)({ 115 | method: 'OPTIONS', 116 | url: `${AWS_S3_URL}/speech-upload-prod`, 117 | headers: { 118 | Accept: '*/*', 119 | 'Access-Control-Request-Method': 'POST', 120 | Origin: 'https://otter.ai', 121 | Referer: 'https://otter.ai/' 122 | } 123 | }); 124 | 125 | this.uploadSpeech = async file => { 126 | const uploadOptionsResponse = await (0, _axios.default)({ 127 | method: 'GET', 128 | url: `${API_BASE_URL}/speech_upload_params`, 129 | params: { 130 | userid: this.user.id 131 | }, 132 | headers: { 133 | Accept: '*/*', 134 | Connection: 'keep-alive', 135 | Origin: 'https://otter.ai', 136 | Referer: 'https://otter.ai/' 137 | } 138 | }); 139 | delete uploadOptionsResponse.data.data.form_action; 140 | const xmlResponse = await (0, _requestPromise.default)({ 141 | method: 'POST', 142 | uri: `${AWS_S3_URL}/speech-upload-prod`, 143 | formData: { ...uploadOptionsResponse.data.data, 144 | file 145 | } 146 | }); 147 | const { 148 | Bucket, 149 | Key 150 | } = await (0, _utils.parseXml)(xmlResponse); 151 | const finishResponse = await (0, _axios.default)({ 152 | method: 'POST', 153 | url: `${API_BASE_URL}/finish_speech_upload`, 154 | params: { 155 | bucket: Bucket, 156 | key: Key, 157 | language: 'en', 158 | country: 'us', 159 | userid: this.user.id 160 | }, 161 | headers: { 162 | 'x-csrftoken': this.csrfToken 163 | } 164 | }); 165 | return finishResponse.data; 166 | }; 167 | 168 | this.options = options; 169 | this.user = {}; 170 | this.csrfToken = ''; 171 | } 172 | 173 | } 174 | /** 175 | * @param options 176 | * @returns {Promise} 177 | */ 178 | 179 | 180 | exports.OtterApi = OtterApi; 181 | 182 | const createClient = async options => { 183 | const client = new OtterApi(options); 184 | await client.login(); 185 | return client; 186 | }; 187 | 188 | exports.createClient = createClient; 189 | var _default = OtterApi; 190 | exports.default = _default; --------------------------------------------------------------------------------