├── .gitignore ├── .nvmrc ├── Cakefile ├── LICENSE ├── README.md ├── events ├── app.js ├── make-events-json.rb └── package.json ├── lib ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff └── vendor │ ├── bigbluebutton-api.js │ ├── bootstrap-flatly.min.css │ ├── bootstrap.min.css │ ├── bootstrap.min.js │ ├── jsSHA.js │ ├── mustache.js │ └── underscore-min.js ├── package-lock.json ├── package.json ├── proxy ├── .gitignore ├── README.md ├── img │ ├── api-mate-server.png │ └── proxy-output.png ├── index.js ├── package-lock.json └── package.json └── src ├── css ├── api_mate.scss ├── application.scss └── redis_events.scss ├── js ├── api_mate.coffee ├── application.coffee ├── redis_events.coffee └── templates.coffee ├── pug_options.json └── views ├── _menu.pug ├── _menu_events.pug ├── _tab_config_xml.pug ├── api_mate.pug ├── application.pug └── redis_events.pug /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | **/#*# 3 | *.log 4 | node_modules/ 5 | .sass-cache/ 6 | lib/ 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.4.0 2 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | chokidar = require('chokidar') 2 | fs = require('fs') 3 | {spawn} = require('child_process') 4 | pug = require('pug') 5 | sass = require('node-sass') 6 | 7 | binPath = './node_modules/.bin/' 8 | 9 | # Returns a string with the current time to print out. 10 | timeNow = -> 11 | today = new Date() 12 | today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds() 13 | 14 | # Spawns an application with `options` and calls `onExit` 15 | # when it finishes. 16 | run = (bin, options, onExit) -> 17 | bin = binPath + bin 18 | console.log timeNow() + ' - running: ' + bin + ' ' + (if options? then options.join(' ') else '') 19 | cmd = spawn bin, options 20 | cmd.stdout.on 'data', (data) -> #console.log data.toString() 21 | cmd.stderr.on 'data', (data) -> console.log data.toString() 22 | cmd.on 'exit', (code) -> 23 | console.log timeNow() + ' - done.' 24 | onExit?(code, options) 25 | 26 | compileView = (done) -> 27 | options = ['--pretty', 'src/views/api_mate.pug', '--out', 'lib', '--obj', 'src/pug_options.json'] 28 | run 'pug', options, -> 29 | options = ['--pretty', 'src/views/redis_events.pug', '--out', 'lib', '--obj', 'src/pug_options.json'] 30 | run 'pug', options, -> 31 | done?() 32 | 33 | compileCss = (done) -> 34 | options = ['src/css/api_mate.scss', 'lib/api_mate.css'] 35 | run 'node-sass', options, -> 36 | options = ['src/css/redis_events.scss', 'lib/redis_events.css'] 37 | run 'node-sass', options, -> 38 | done?() 39 | 40 | compileJs = (done) -> 41 | options = [ 42 | '-o', 'lib', 43 | '--join', 'api_mate.js', 44 | '--compile', 'src/js/application.coffee', 'src/js/templates.coffee', 'src/js/api_mate.coffee' 45 | ] 46 | run 'coffee', options, -> 47 | options = [ 48 | '-o', 'lib', 49 | '--join', 'redis_events.js', 50 | '--compile', 'src/js/application.coffee', 'src/js/redis_events.coffee' 51 | ] 52 | run 'coffee', options, -> 53 | done?() 54 | 55 | build = (done) -> 56 | compileView (err) -> 57 | compileCss (err) -> 58 | compileJs (err) -> 59 | done?() 60 | 61 | watch = () -> 62 | watcher = chokidar.watch('src', { ignored: /[\/\\]\./, persistent: true }) 63 | watcher.on 'all', (event, path) -> 64 | console.log timeNow() + ' = detected', event, 'on', path 65 | if path.match(/\.coffee$/) 66 | compileJs() 67 | else if path.match(/\.scss/) 68 | compileCss() 69 | else if path.match(/\.pug/) 70 | compileView() 71 | 72 | task 'build', 'Build everything from src/ into lib/', -> 73 | build() 74 | 75 | task 'watch', 'Watch for changes to compile the sources', -> 76 | watch() 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Mconf (http://mconf.org) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | 21 | This project is developed as part of Mconf (http://mconf.org). 22 | Contact information: 23 | Mconf: A scalable opensource multiconference system for web and mobile devices 24 | PRAV Labs - UFRGS - Porto Alegre - Brazil 25 | http://mconf.org/ 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | API Mate 2 | ======== 3 | 4 | API Mate is a web application (a simple web page) to access the APIs of [BigBlueButton](http://bigbluebutton.org) and [Mconf](http://mconf.org). 5 | 6 | Usage 7 | ----- 8 | 9 | * Use it online at http://mconf.github.io/api-mate; or 10 | * Get the latest version from the branch [`gh-pages`](https://github.com/mconf/api-mate/tree/gh-pages) and 11 | open `index.html` in your browser. 12 | 13 | 14 | ## Passing parameters in the URL 15 | 16 | The API Mate HTML page accepts parameters in the URL to pre-configure all the inputs available in the 17 | menu, that will define the links generated. You can, for instance, generate a link in your application 18 | to redirect to the API Mate and automatically fill the server and the shared secret fields in the 19 | API Mate so that it points to the server you want to use. 20 | 21 | The URL below shows a few of the parameters that can be passed in the URL: 22 | 23 | ``` 24 | api_mate.html#server=http://my-server.com/bigbluebutton/api&sharedSecret=lsk8df74e400365b55e0987&meetingID=meeting-1234567&custom-calls=getMyData 25 | ``` 26 | 27 | The parameters should be passed in the hash part of the URL, so they are not submitted to the server. 28 | This means the application at http://mconf.github.io/api-mate will not receive your server's URL 29 | and shared secret. You can also pass these parameters in the search string part of the URL, but that means the server will have access to your parameters (might be useful if 30 | you're hosting your own API Mate). 31 | 32 | The server address and shared secret are defined in the URL parameters `server` and `sharedSecret` 33 | (you can also use `salt`), respectively. 34 | 35 | All the other parameters are matched by an HTML `data-api-mate-param` attribute that is defined 36 | in all inputs in the API Mate. The input to define the meeting ID, for example, has this attribute 37 | set as `data-api-mate-param='meetingID,recordindID'`, so you can use both `meetingID=something` or 38 | `recordingID=something` in the URL and it will automatically fill the meeting ID input. The input 39 | to define custom API calls has the attribute set as `data-api-mate-param='custom-calls'`, and this 40 | is why in the URL above we used `custom-calls=getMyData`. 41 | 42 | 43 | ## Allow cross-domain requests 44 | 45 | The API Mate runs on your web browser and most of the API methods are accesssed through HTTP GET 46 | calls, so you can simply click on a link in the API Mate and you'll access the API method. 47 | 48 | However, for some other methods (such as API methods accessed via POST) or some more advanced 49 | features, we need to run API calls from the javascript using ajax. This will result in a cross-domain 50 | request, since a web page (the API Mate) is making requests directly to another server (your web 51 | conference server). Since cross-domain requests are by default disabled in the browser, they will 52 | all fail. 53 | 54 | We offer two solutions: 55 | 56 | 1. Change your BigBlueButton/Mconf-Live server to accept cross-domain requests (ok, but only 57 | recommended for development and testing); or 58 | 2. Use a local proxy that will receive the calls and proxy them to your web conference server. 59 | 60 | ### 1. Change your server to accept cross-domain requests 61 | 62 | With this option you will enable cross-origin requests using 63 | [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) on your BigBlueButton/Mconf-Live server. 64 | 65 | #### In BigBlueButton/Mconf-Live with Nginx 66 | 67 | Copy to following block of code to the bottom of the file `/etc/bigbluebutton/nginx/web.nginx`, inside the 68 | block `location /bigbluebutton`: 69 | 70 | ``` 71 | location /bigbluebutton { 72 | ... 73 | 74 | # add this block! 75 | if ($http_origin) { 76 | add_header Access-Control-Allow-Origin *; 77 | add_header Access-Control-Allow-Methods "GET,POST,OPTIONS"; 78 | add_header Access-Control-Allow-Headers Content-Type; 79 | add_header Access-Control-Max-Age 86400; 80 | } 81 | } 82 | ``` 83 | 84 | Notice that it will allow cross-domain requests from **any** host, which is not recommended! Use it only 85 | for test and development. 86 | 87 | Save it and restart Nginx to apply the changes: 88 | 89 | ```bash 90 | $ sudo /etc/init.d/nginx restart 91 | ``` 92 | 93 | If you need a more detailed and controlled example, [try this one](http://enable-cors.org/server_nginx.html). 94 | 95 | #### On [Node.js](http://nodejs.org/) with [Express.js](http://expressjs.com/): 96 | 97 | If you're not accessing your web conference server directly, but through an application written in 98 | Node.js, you can use the following code to enable cross-domain requests: 99 | 100 | ```coffeescript 101 | app.all '*', (req, res, next) -> 102 | res.header("Access-Control-Allow-Origin", "*") 103 | res.header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type") 104 | next() 105 | ``` 106 | 107 | [Source](http://enable-cors.org/server_expressjs.html). 108 | 109 | 110 | ### 2. Use a local proxy 111 | 112 | There's an application that can be used as a local proxy called `api-mate-proxy` available in this 113 | repository, in the folder [proxy](https://github.com/mconf/api-mate/tree/master/proxy). 114 | 115 | It is a very simple Node.js application that you can run locally to receive all requests 116 | from the API Mate and proxy them to your web conference server. 117 | 118 | #### Usage 119 | 120 | See `api-mate-proxy`'s [README file](https://github.com/mconf/api-mate/tree/master/proxy). 121 | 122 | 123 | Development 124 | ----------- 125 | 126 | At first, install [Node.js](http://nodejs.org/) (see `package.json` for the specific version required). 127 | 128 | Install the dependencies with: 129 | 130 | npm install 131 | 132 | Then compile the source files with: 133 | 134 | [./node_modules/.bin/]cake build 135 | 136 | This will compile all files inside `src/` to formats that can be opened in the browser and place them into `/lib`. 137 | 138 | To watch for changes and compile the files automatically, run: 139 | 140 | [./node_modules/.bin/]cake watch 141 | 142 | 143 | License 144 | ------- 145 | 146 | Distributed under The MIT License (MIT), see `LICENSE`. 147 | -------------------------------------------------------------------------------- /events/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var fs = require('fs'); 3 | var subscribe = require('redis-subscribe-sse'); 4 | var bodyParser = require('body-parser'); 5 | var redis = require("redis"); 6 | var corser = require("corser"); 7 | 8 | // Main definitions 9 | var path = ''; 10 | 11 | var app = express(); 12 | var redisClient = redis.createClient(); 13 | 14 | app.use(corser.create()); // for CORS 15 | app.use(bodyParser.json()); // support json encoded bodies 16 | app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies 17 | 18 | app.get(path + '/pull', function(req, res) { 19 | var sse; 20 | 21 | sse = subscribe({ 22 | channels: '*', 23 | retry: 5000, 24 | host: '127.0.0.1', 25 | port: 6379, 26 | patternSubscribe: true 27 | }); 28 | 29 | req.socket.setTimeout(0); 30 | 31 | res.set({ 32 | 'Content-Type': 'text/event-stream', 33 | 'Cache-Control': 'no-cache', 34 | 'Connection': 'keep-alive', 35 | 'Access-Control-Allow-Origin': '*' 36 | }); 37 | 38 | sse.pipe(res).on('close', function() { 39 | sse.close(); 40 | }); 41 | }); 42 | 43 | app.post(path + '/push', function(req, res) { 44 | var data = req.body.data; 45 | var channel = req.body.channel; 46 | 47 | console.log("<== publishing", data, "in channel", channel); 48 | redisClient.publish(channel, JSON.stringify(data)); 49 | 50 | res.set({ 51 | 'Content-Type': 'text/plain', 52 | 'Cache-Control': 'no-cache', 53 | 'Access-Control-Allow-Origin': '*' 54 | }); 55 | res.writeHead(200); 56 | res.end(); 57 | }); 58 | 59 | var server = app.listen(3000, function() { 60 | console.log('Listening on port %d', server.address().port); 61 | }); 62 | -------------------------------------------------------------------------------- /events/make-events-json.rb: -------------------------------------------------------------------------------- 1 | # Creates a json file with all the unique events available in the input. 2 | # The input is a text file with a json event per line. It can be generated by 3 | # using the API Mate to get all events in redis during a meeting and pasting into 4 | # a file. 5 | # Unique events will be filtered from this file and printed as the output of 6 | # this script. 7 | 8 | require 'rubygems' 9 | require 'json' 10 | 11 | unique_messages = [] 12 | events = [] 13 | 14 | file = File.open(ARGV[0]) 15 | lines = file.read 16 | lines.each_line do |line| 17 | begin 18 | json = JSON.parse(line) 19 | rescue 20 | json = nil 21 | end 22 | if json && json["header"] 23 | name = json["header"]["name"] 24 | unless unique_messages.include?(name) 25 | unique_messages << name 26 | events << line.strip 27 | end 28 | end 29 | end 30 | file.close 31 | 32 | # sort alphabetically by event name 33 | events = events.sort { |x,y| 34 | JSON.parse(x)["header"]["name"] <=> JSON.parse(y)["header"]["name"] 35 | } 36 | 37 | # add a header and footer to make it a valid json 38 | output = '{"events":[' 39 | output += events.join(',') 40 | output += ']}' 41 | 42 | # output it prettified 43 | puts JSON.pretty_generate(JSON.parse(output)) 44 | -------------------------------------------------------------------------------- /events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-mate-redis-events", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "body-parser": "^1.14.1", 6 | "corser": "^2.0.0", 7 | "express": "^4.9.5", 8 | "redis": "^2.3.1", 9 | "redis-subscribe-sse": "0.2.0" 10 | }, 11 | "engine": "node >= 0.10.0" 12 | } 13 | -------------------------------------------------------------------------------- /lib/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mconf/api-mate/786ffe64f16031d378e956e793ca023d7a278917/lib/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /lib/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mconf/api-mate/786ffe64f16031d378e956e793ca023d7a278917/lib/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /lib/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mconf/api-mate/786ffe64f16031d378e956e793ca023d7a278917/lib/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /lib/vendor/bigbluebutton-api.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.4.1 2 | (function() { 3 | var BigBlueButtonApi, filterCustomParameters, include, noChecksumMethods, root, 4 | indexOf = [].indexOf; 5 | 6 | root = typeof exports !== "undefined" && exports !== null ? exports : this; 7 | 8 | BigBlueButtonApi = class BigBlueButtonApi { 9 | // `url`: The complete URL to the server's API, e.g. `http://server.com/bigbluebutton/api` 10 | // `salt`: The shared secret of your server. 11 | // `debug`: Turn on debug messages, printed to `console.log`. 12 | // `opts`: Additional options 13 | constructor(url, salt, debug = false, opts = {}) { 14 | var base; 15 | this.url = url; 16 | this.salt = salt; 17 | this.debug = debug; 18 | this.opts = opts; 19 | if ((base = this.opts).shaType == null) { 20 | base.shaType = 'sha1'; 21 | } 22 | } 23 | 24 | // Returna a list with the name of all available API calls. 25 | availableApiCalls() { 26 | return ['/', 'create', 'join', 'isMeetingRunning', 'getMeetingInfo', 'end', 'getMeetings', 'getDefaultConfigXML', 'setConfigXML', 'enter', 'configXML', 'signOut', 'getRecordings', 'publishRecordings', 'deleteRecordings', 'updateRecordings', 'getRecordingTextTracks']; 27 | } 28 | 29 | // Returns a list of supported parameters in the URL for a given API method. 30 | // The return is an array of arrays composed by: 31 | // [0] - RegEx or string with the parameter name 32 | // [1] - true if the parameter is required, false otherwise 33 | urlParamsFor(param) { 34 | switch (param) { 35 | case "create": 36 | return [["meetingID", true], ["name", true], ["attendeePW", false], ["moderatorPW", false], ["welcome", false], ["dialNumber", false], ["voiceBridge", false], ["webVoice", false], ["logoutURL", false], ["maxParticipants", false], ["record", false], ["duration", false], ["moderatorOnlyMessage", false], ["autoStartRecording", false], ["allowStartStopRecording", false], [/meta_\w+/, false]]; 37 | case "join": 38 | return [["fullName", true], ["meetingID", true], ["password", true], ["createTime", false], ["userID", false], ["webVoiceConf", false], ["configToken", false], ["avatarURL", false], ["redirect", false], ["clientURL", false]]; 39 | case "isMeetingRunning": 40 | return [["meetingID", true]]; 41 | case "end": 42 | return [["meetingID", true], ["password", true]]; 43 | case "getMeetingInfo": 44 | return [["meetingID", true], ["password", true]]; 45 | case "getRecordings": 46 | return [["meetingID", false], ["recordID", false], ["state", false], [/meta_\w+/, false]]; 47 | case "publishRecordings": 48 | return [["recordID", true], ["publish", true]]; 49 | case "deleteRecordings": 50 | return [["recordID", true]]; 51 | case "updateRecordings": 52 | return [["recordID", true], [/meta_\w+/, false]]; 53 | case "getRecordingTextTracks": 54 | return [["recordID", true]]; 55 | } 56 | } 57 | 58 | // Filter `params` to allow only parameters that can be passed 59 | // to the method `method`. 60 | // To use custom parameters, name them `custom_parameterName`. 61 | // The `custom_` prefix will be removed when generating the urls. 62 | filterParams(params, method) { 63 | var filters, r; 64 | filters = this.urlParamsFor(method); 65 | if ((filters == null) || filters.length === 0) { 66 | ({}); 67 | } else { 68 | r = include(params, function(key, value) { 69 | var filter, i, len; 70 | for (i = 0, len = filters.length; i < len; i++) { 71 | filter = filters[i]; 72 | if (filter[0] instanceof RegExp) { 73 | if (key.match(filter[0]) || key.match(/^custom_/)) { 74 | return true; 75 | } 76 | } else { 77 | if (key.match(`^${filter[0]}$`) || key.match(/^custom_/)) { 78 | return true; 79 | } 80 | } 81 | } 82 | return false; 83 | }); 84 | } 85 | return filterCustomParameters(r); 86 | } 87 | 88 | // Returns a url for any `method` available in the BigBlueButton API 89 | // using the parameters in `params`. 90 | // Parameters received: 91 | // * `method`: The name of the API method 92 | // * `params`: An object with pairs of `parameter`:`value`. The parameters will be used only in the 93 | // API calls they should be used. If a parameter name starts with `custom_`, it will 94 | // be used in all API calls, removing the `custom_` prefix. 95 | // Parameters to be used as metadata should use the prefix `meta_`. 96 | // * `filter`: Whether the parameters in `params` should be filtered, so that the API 97 | // calls will contain only the parameters they accept. If false, all parameters 98 | // in `params` will be added to the API call. 99 | urlFor(method, params, filter = true) { 100 | var checksum, i, key, keys, len, param, paramList, property, query, sep, url; 101 | if (this.debug) { 102 | console.log("Generating URL for", method); 103 | } 104 | if (filter) { 105 | params = this.filterParams(params, method); 106 | } else { 107 | params = filterCustomParameters(params); 108 | } 109 | url = this.url; 110 | // mounts the string with the list of parameters 111 | paramList = []; 112 | if (params != null) { 113 | // add the parameters in alphabetical order to prevent checksum errors 114 | keys = []; 115 | for (property in params) { 116 | keys.push(property); 117 | } 118 | keys = keys.sort(); 119 | for (i = 0, len = keys.length; i < len; i++) { 120 | key = keys[i]; 121 | if (key != null) { 122 | param = params[key]; 123 | } 124 | if (param != null) { 125 | paramList.push(`${this.encodeForUrl(key)}=${this.encodeForUrl(param)}`); 126 | } 127 | } 128 | if (paramList.length > 0) { 129 | query = paramList.join("&"); 130 | } 131 | } else { 132 | query = ''; 133 | } 134 | // calculate the checksum 135 | checksum = this.checksum(method, query); 136 | // add the missing elements in the query 137 | if (paramList.length > 0) { 138 | query = `${method}?${query}`; 139 | sep = '&'; 140 | } else { 141 | if (method !== '/') { 142 | query = method; 143 | } 144 | sep = '?'; 145 | } 146 | if (indexOf.call(noChecksumMethods(), method) < 0) { 147 | query = `${query}${sep}checksum=${checksum}`; 148 | } 149 | return `${url}/${query}`; 150 | } 151 | 152 | // Calculates the checksum for an API call `method` with 153 | // the params in `query`. 154 | checksum(method, query) { 155 | var c, shaObj, str; 156 | query || (query = ""); 157 | if (this.debug) { 158 | console.log(`- Calculating the checksum using: '${method}', '${query}', '${this.salt}'`); 159 | } 160 | str = method + query + this.salt; 161 | switch (this.opts.shaType) { 162 | case 'sha1': 163 | shaObj = new jsSHA("SHA-1", "TEXT"); 164 | break; 165 | case 'sha256': 166 | shaObj = new jsSHA("SHA-256", "TEXT"); 167 | break; 168 | case 'sha384': 169 | shaObj = new jsSHA("SHA-384", "TEXT"); 170 | break; 171 | case 'sha512': 172 | shaObj = new jsSHA("SHA-512", "TEXT"); 173 | break; 174 | default: 175 | shaObj = new jsSHA("SHA-1", "TEXT"); 176 | } 177 | shaObj.update(str); 178 | c = shaObj.getHash("HEX"); 179 | if (this.debug) { 180 | console.log("- Checksum calculated:", c); 181 | } 182 | return c; 183 | } 184 | 185 | // Encodes a string to set it in the URL. Has to encode it exactly like BigBlueButton does! 186 | // Otherwise the validation of the checksum might fail at some point. 187 | encodeForUrl(value) { 188 | return encodeURIComponent(value).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); // use + instead of %20 for space to match what the Java tools do. // encodeURIComponent doesn't escape !'()* but browsers do, so manually escape them. 189 | } 190 | 191 | // Replaces the protocol for `bigbluebutton://`. 192 | setMobileProtocol(url) { 193 | return url.replace(/http[s]?\:\/\//, "bigbluebutton://"); 194 | } 195 | 196 | }; 197 | 198 | // Ruby-like include() method for Objects 199 | include = function(input, _function) { 200 | var _match, _obj, key, value; 201 | _obj = new Object; 202 | _match = null; 203 | for (key in input) { 204 | value = input[key]; 205 | if (_function.call(input, key, value)) { 206 | _obj[key] = value; 207 | } 208 | } 209 | return _obj; 210 | }; 211 | 212 | root.BigBlueButtonApi = BigBlueButtonApi; 213 | 214 | // creates keys without "custom_" and deletes the ones with it 215 | filterCustomParameters = function(params) { 216 | var key, v; 217 | for (key in params) { 218 | v = params[key]; 219 | if (key.match(/^custom_/)) { 220 | params[key.replace(/^custom_/, "")] = v; 221 | } 222 | } 223 | for (key in params) { 224 | if (key.match(/^custom_/)) { 225 | delete params[key]; 226 | } 227 | } 228 | return params; 229 | }; 230 | 231 | noChecksumMethods = function() { 232 | return ['setConfigXML', '/', 'enter', 'configXML', 'signOut']; 233 | }; 234 | 235 | }).call(this); 236 | -------------------------------------------------------------------------------- /lib/vendor/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.3 (http://getbootstrap.com) 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | 7 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /lib/vendor/jsSHA.js: -------------------------------------------------------------------------------- 1 | /* 2 | A JavaScript implementation of the SHA family of hashes, as 3 | defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding 4 | HMAC implementation as defined in FIPS PUB 198a 5 | 6 | Copyright Brian Turek 2008-2017 7 | Distributed under the BSD License 8 | See http://caligatio.github.com/jsSHA/ for more information 9 | 10 | Several functions taken from Paul Johnston 11 | */ 12 | 'use strict';(function(Y){function C(c,a,b){var e=0,h=[],n=0,g,l,d,f,m,q,u,r,I=!1,v=[],w=[],t,y=!1,z=!1,x=-1;b=b||{};g=b.encoding||"UTF8";t=b.numRounds||1;if(t!==parseInt(t,10)||1>t)throw Error("numRounds must a integer >= 1");if("SHA-1"===c)m=512,q=K,u=Z,f=160,r=function(a){return a.slice()};else if(0===c.lastIndexOf("SHA-",0))if(q=function(a,b){return L(a,b,c)},u=function(a,b,h,e){var k,f;if("SHA-224"===c||"SHA-256"===c)k=(b+65>>>9<<4)+15,f=16;else if("SHA-384"===c||"SHA-512"===c)k=(b+129>>>10<< 13 | 5)+31,f=32;else throw Error("Unexpected error in SHA-2 implementation");for(;a.length<=k;)a.push(0);a[b>>>5]|=128<<24-b%32;b=b+h;a[k]=b&4294967295;a[k-1]=b/4294967296|0;h=a.length;for(b=0;be;e+=1)c[e]=a[e].slice();return c};x=1;if("SHA3-224"=== 15 | c)m=1152,f=224;else if("SHA3-256"===c)m=1088,f=256;else if("SHA3-384"===c)m=832,f=384;else if("SHA3-512"===c)m=576,f=512;else if("SHAKE128"===c)m=1344,f=-1,F=31,z=!0;else if("SHAKE256"===c)m=1088,f=-1,F=31,z=!0;else throw Error("Chosen SHA variant is not supported");u=function(a,c,e,b,h){e=m;var k=F,f,g=[],n=e>>>5,l=0,d=c>>>5;for(f=0;f=e;f+=n)b=D(a.slice(f,f+n),b),c-=e;a=a.slice(f);for(c%=e;a.length>>3;a[f>>2]^=k<=h)break;g.push(a.a);l+=1;0===64*l%e&&D(null,b)}return g}}else throw Error("Chosen SHA variant is not supported");d=M(a,g,x);l=A(c);this.setHMACKey=function(a,b,h){var k;if(!0===I)throw Error("HMAC key already set");if(!0===y)throw Error("Cannot set HMAC key after calling update");if(!0===z)throw Error("SHAKE is not supported for HMAC");g=(h||{}).encoding||"UTF8";b=M(b,g,x)(a);a=b.binLen;b=b.value;k=m>>>3;h=k/4-1;if(ka/8){for(;b.length<=h;)b.push(0);b[h]&=4294967040}for(a=0;a<=h;a+=1)v[a]=b[a]^909522486,w[a]=b[a]^1549556828;l=q(v,l);e=m;I=!0};this.update=function(a){var c,b,k,f=0,g=m>>>5;c=d(a,h,n);a=c.binLen;b=c.value;c=a>>>5;for(k=0;k>>5);n=a%m;y=!0};this.getHash=function(a,b){var k,g,d,m;if(!0===I)throw Error("Cannot call getHash after setting HMAC key");d=N(b);if(!0===z){if(-1===d.shakeLen)throw Error("shakeLen must be specified in options"); 18 | f=d.shakeLen}switch(a){case "HEX":k=function(a){return O(a,f,x,d)};break;case "B64":k=function(a){return P(a,f,x,d)};break;case "BYTES":k=function(a){return Q(a,f,x)};break;case "ARRAYBUFFER":try{g=new ArrayBuffer(0)}catch(p){throw Error("ARRAYBUFFER not supported by this environment");}k=function(a){return R(a,f,x)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");}m=u(h.slice(),n,e,r(l),f);for(g=1;g>>24-f%32),m=u(m,f, 19 | 0,A(c),f);return k(m)};this.getHMAC=function(a,b){var k,g,d,p;if(!1===I)throw Error("Cannot call getHMAC without first setting HMAC key");d=N(b);switch(a){case "HEX":k=function(a){return O(a,f,x,d)};break;case "B64":k=function(a){return P(a,f,x,d)};break;case "BYTES":k=function(a){return Q(a,f,x)};break;case "ARRAYBUFFER":try{k=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}k=function(a){return R(a,f,x)};break;default:throw Error("outputFormat must be HEX, B64, BYTES, or ARRAYBUFFER"); 20 | }g=u(h.slice(),n,e,r(l),f);p=q(w,A(c));p=u(g,f,m,p,f);return k(p)}}function b(c,a){this.a=c;this.b=a}function O(c,a,b,e){var h="";a/=8;var n,g,d;d=-1===b?3:0;for(n=0;n>>2]>>>8*(d+n%4*b),h+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return e.outputUpper?h.toUpperCase():h}function P(c,a,b,e){var h="",n=a/8,g,d,p,f;f=-1===b?3:0;for(g=0;g>>2]:0,p=g+2>>2]:0,p=(c[g>>>2]>>>8*(f+g%4*b)&255)<<16|(d>>>8*(f+(g+1)%4*b)&255)<<8|p>>>8*(f+ 21 | (g+2)%4*b)&255,d=0;4>d;d+=1)8*g+6*d<=a?h+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(p>>>6*(3-d)&63):h+=e.b64Pad;return h}function Q(c,a,b){var e="";a/=8;var h,d,g;g=-1===b?3:0;for(h=0;h>>2]>>>8*(g+h%4*b)&255,e+=String.fromCharCode(d);return e}function R(c,a,b){a/=8;var e,h=new ArrayBuffer(a),d,g;g=new Uint8Array(h);d=-1===b?3:0;for(e=0;e>>2]>>>8*(d+e%4*b)&255;return h}function N(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{}; 22 | a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if(!0===c.hasOwnProperty("shakeLen")){if(0!==c.shakeLen%8)throw Error("shakeLen must be a multiple of 8");a.shakeLen=c.shakeLen}if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function M(c,a,b){switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE"); 23 | }switch(c){case "HEX":c=function(a,c,d){var g=a.length,l,p,f,m,q,u;if(0!==g%2)throw Error("String of HEX type must be in byte increments");c=c||[0];d=d||0;q=d>>>3;u=-1===b?3:0;for(l=0;l>>1)+q;for(f=m>>>2;c.length<=f;)c.push(0);c[f]|=p<<8*(u+m%4*b)}return{value:c,binLen:4*g+d}};break;case "TEXT":c=function(c,h,d){var g,l,p=0,f,m,q,u,r,t;h=h||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(t=-1=== 24 | b?3:0,f=0;fg?l.push(g):2048>g?(l.push(192|g>>>6),l.push(128|g&63)):55296>g||57344<=g?l.push(224|g>>>12,128|g>>>6&63,128|g&63):(f+=1,g=65536+((g&1023)<<10|c.charCodeAt(f)&1023),l.push(240|g>>>18,128|g>>>12&63,128|g>>>6&63,128|g&63)),m=0;m>>2;h.length<=u;)h.push(0);h[u]|=l[m]<<8*(t+r%4*b);p+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(t=-1===b?2:0,l="UTF16LE"===a&&1!==b||"UTF16LE"!==a&&1===b,f=0;f>>8);r=p+q;for(u=r>>>2;h.length<=u;)h.push(0);h[u]|=g<<8*(t+r%4*b);p+=2}return{value:h,binLen:8*p+d}};break;case "B64":c=function(a,c,d){var g=0,l,p,f,m,q,u,r,t;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");p=a.indexOf("=");a=a.replace(/\=/g,"");if(-1!==p&&p"'\/]/g, function (s) { 64 | return entityMap[s]; 65 | }); 66 | } 67 | 68 | // Export the escaping function so that the user may override it. 69 | // See https://github.com/janl/mustache.js/issues/244 70 | exports.escape = escapeHtml; 71 | 72 | function Scanner(string) { 73 | this.string = string; 74 | this.tail = string; 75 | this.pos = 0; 76 | } 77 | 78 | /** 79 | * Returns `true` if the tail is empty (end of string). 80 | */ 81 | Scanner.prototype.eos = function () { 82 | return this.tail === ""; 83 | }; 84 | 85 | /** 86 | * Tries to match the given regular expression at the current position. 87 | * Returns the matched text if it can match, the empty string otherwise. 88 | */ 89 | Scanner.prototype.scan = function (re) { 90 | var match = this.tail.match(re); 91 | 92 | if (match && match.index === 0) { 93 | this.tail = this.tail.substring(match[0].length); 94 | this.pos += match[0].length; 95 | return match[0]; 96 | } 97 | 98 | return ""; 99 | }; 100 | 101 | /** 102 | * Skips all text until the given regular expression can be matched. Returns 103 | * the skipped string, which is the entire tail if no match can be made. 104 | */ 105 | Scanner.prototype.scanUntil = function (re) { 106 | var match, pos = this.tail.search(re); 107 | 108 | switch (pos) { 109 | case -1: 110 | match = this.tail; 111 | this.pos += this.tail.length; 112 | this.tail = ""; 113 | break; 114 | case 0: 115 | match = ""; 116 | break; 117 | default: 118 | match = this.tail.substring(0, pos); 119 | this.tail = this.tail.substring(pos); 120 | this.pos += pos; 121 | } 122 | 123 | return match; 124 | }; 125 | 126 | function Context(view, parent) { 127 | this.view = view; 128 | this.parent = parent; 129 | this.clearCache(); 130 | } 131 | 132 | Context.make = function (view) { 133 | return (view instanceof Context) ? view : new Context(view); 134 | }; 135 | 136 | Context.prototype.clearCache = function () { 137 | this._cache = {}; 138 | }; 139 | 140 | Context.prototype.push = function (view) { 141 | return new Context(view, this); 142 | }; 143 | 144 | Context.prototype.lookup = function (name) { 145 | var value = this._cache[name]; 146 | 147 | if (!value) { 148 | if (name === ".") { 149 | value = this.view; 150 | } else { 151 | var context = this; 152 | 153 | while (context) { 154 | if (name.indexOf(".") > 0) { 155 | var names = name.split("."), i = 0; 156 | 157 | value = context.view; 158 | 159 | while (value && i < names.length) { 160 | value = value[names[i++]]; 161 | } 162 | } else { 163 | value = context.view[name]; 164 | } 165 | 166 | if (value != null) { 167 | break; 168 | } 169 | 170 | context = context.parent; 171 | } 172 | } 173 | 174 | this._cache[name] = value; 175 | } 176 | 177 | if (typeof value === "function") { 178 | value = value.call(this.view); 179 | } 180 | 181 | return value; 182 | }; 183 | 184 | function Writer() { 185 | this.clearCache(); 186 | } 187 | 188 | Writer.prototype.clearCache = function () { 189 | this._cache = {}; 190 | this._partialCache = {}; 191 | }; 192 | 193 | Writer.prototype.compile = function (template, tags) { 194 | var fn = this._cache[template]; 195 | 196 | if (!fn) { 197 | var tokens = exports.parse(template, tags); 198 | fn = this._cache[template] = this.compileTokens(tokens, template); 199 | } 200 | 201 | return fn; 202 | }; 203 | 204 | Writer.prototype.compilePartial = function (name, template, tags) { 205 | var fn = this.compile(template, tags); 206 | this._partialCache[name] = fn; 207 | return fn; 208 | }; 209 | 210 | Writer.prototype.compileTokens = function (tokens, template) { 211 | var fn = compileTokens(tokens); 212 | var self = this; 213 | 214 | return function (view, partials) { 215 | if (partials) { 216 | if (typeof partials === "function") { 217 | self._loadPartial = partials; 218 | } else { 219 | for (var name in partials) { 220 | self.compilePartial(name, partials[name]); 221 | } 222 | } 223 | } 224 | 225 | return fn(self, Context.make(view), template); 226 | }; 227 | }; 228 | 229 | Writer.prototype.render = function (template, view, partials) { 230 | return this.compile(template)(view, partials); 231 | }; 232 | 233 | Writer.prototype._section = function (name, context, text, callback) { 234 | var value = context.lookup(name); 235 | 236 | switch (typeof value) { 237 | case "object": 238 | if (isArray(value)) { 239 | var buffer = ""; 240 | 241 | for (var i = 0, len = value.length; i < len; ++i) { 242 | buffer += callback(this, context.push(value[i])); 243 | } 244 | 245 | return buffer; 246 | } 247 | 248 | return value ? callback(this, context.push(value)) : ""; 249 | case "function": 250 | var self = this; 251 | var scopedRender = function (template) { 252 | return self.render(template, context); 253 | }; 254 | 255 | var result = value.call(context.view, text, scopedRender); 256 | return result != null ? result : ""; 257 | default: 258 | if (value) { 259 | return callback(this, context); 260 | } 261 | } 262 | 263 | return ""; 264 | }; 265 | 266 | Writer.prototype._inverted = function (name, context, callback) { 267 | var value = context.lookup(name); 268 | 269 | // Use JavaScript's definition of falsy. Include empty arrays. 270 | // See https://github.com/janl/mustache.js/issues/186 271 | if (!value || (isArray(value) && value.length === 0)) { 272 | return callback(this, context); 273 | } 274 | 275 | return ""; 276 | }; 277 | 278 | Writer.prototype._partial = function (name, context) { 279 | if (!(name in this._partialCache) && this._loadPartial) { 280 | this.compilePartial(name, this._loadPartial(name)); 281 | } 282 | 283 | var fn = this._partialCache[name]; 284 | 285 | return fn ? fn(context) : ""; 286 | }; 287 | 288 | Writer.prototype._name = function (name, context) { 289 | var value = context.lookup(name); 290 | 291 | if (typeof value === "function") { 292 | value = value.call(context.view); 293 | } 294 | 295 | return (value == null) ? "" : String(value); 296 | }; 297 | 298 | Writer.prototype._escaped = function (name, context) { 299 | return exports.escape(this._name(name, context)); 300 | }; 301 | 302 | /** 303 | * Low-level function that compiles the given `tokens` into a function 304 | * that accepts three arguments: a Writer, a Context, and the template. 305 | */ 306 | function compileTokens(tokens) { 307 | var subRenders = {}; 308 | 309 | function subRender(i, tokens, template) { 310 | if (!subRenders[i]) { 311 | var fn = compileTokens(tokens); 312 | subRenders[i] = function (writer, context) { 313 | return fn(writer, context, template); 314 | }; 315 | } 316 | 317 | return subRenders[i]; 318 | } 319 | 320 | return function (writer, context, template) { 321 | var buffer = ""; 322 | var token, sectionText; 323 | 324 | for (var i = 0, len = tokens.length; i < len; ++i) { 325 | token = tokens[i]; 326 | 327 | switch (token[0]) { 328 | case "#": 329 | sectionText = template.slice(token[3], token[5]); 330 | buffer += writer._section(token[1], context, sectionText, subRender(i, token[4], template)); 331 | break; 332 | case "^": 333 | buffer += writer._inverted(token[1], context, subRender(i, token[4], template)); 334 | break; 335 | case ">": 336 | buffer += writer._partial(token[1], context); 337 | break; 338 | case "&": 339 | buffer += writer._name(token[1], context); 340 | break; 341 | case "name": 342 | buffer += writer._escaped(token[1], context); 343 | break; 344 | case "text": 345 | buffer += token[1]; 346 | break; 347 | } 348 | } 349 | 350 | return buffer; 351 | }; 352 | } 353 | 354 | /** 355 | * Forms the given array of `tokens` into a nested tree structure where 356 | * tokens that represent a section have two additional items: 1) an array of 357 | * all tokens that appear in that section and 2) the index in the original 358 | * template that represents the end of that section. 359 | */ 360 | function nestTokens(tokens) { 361 | var tree = []; 362 | var collector = tree; 363 | var sections = []; 364 | 365 | var token; 366 | for (var i = 0, len = tokens.length; i < len; ++i) { 367 | token = tokens[i]; 368 | switch (token[0]) { 369 | case '#': 370 | case '^': 371 | sections.push(token); 372 | collector.push(token); 373 | collector = token[4] = []; 374 | break; 375 | case '/': 376 | var section = sections.pop(); 377 | section[5] = token[2]; 378 | collector = sections.length > 0 ? sections[sections.length - 1][4] : tree; 379 | break; 380 | default: 381 | collector.push(token); 382 | } 383 | } 384 | 385 | return tree; 386 | } 387 | 388 | /** 389 | * Combines the values of consecutive text tokens in the given `tokens` array 390 | * to a single token. 391 | */ 392 | function squashTokens(tokens) { 393 | var squashedTokens = []; 394 | 395 | var token, lastToken; 396 | for (var i = 0, len = tokens.length; i < len; ++i) { 397 | token = tokens[i]; 398 | if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { 399 | lastToken[1] += token[1]; 400 | lastToken[3] = token[3]; 401 | } else { 402 | lastToken = token; 403 | squashedTokens.push(token); 404 | } 405 | } 406 | 407 | return squashedTokens; 408 | } 409 | 410 | function escapeTags(tags) { 411 | return [ 412 | new RegExp(escapeRe(tags[0]) + "\\s*"), 413 | new RegExp("\\s*" + escapeRe(tags[1])) 414 | ]; 415 | } 416 | 417 | /** 418 | * Breaks up the given `template` string into a tree of token objects. If 419 | * `tags` is given here it must be an array with two string values: the 420 | * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of 421 | * course, the default is to use mustaches (i.e. Mustache.tags). 422 | */ 423 | exports.parse = function (template, tags) { 424 | template = template || ''; 425 | tags = tags || exports.tags; 426 | 427 | if (typeof tags === 'string') tags = tags.split(spaceRe); 428 | if (tags.length !== 2) { 429 | throw new Error('Invalid tags: ' + tags.join(', ')); 430 | } 431 | 432 | var tagRes = escapeTags(tags); 433 | var scanner = new Scanner(template); 434 | 435 | var sections = []; // Stack to hold section tokens 436 | var tokens = []; // Buffer to hold the tokens 437 | var spaces = []; // Indices of whitespace tokens on the current line 438 | var hasTag = false; // Is there a {{tag}} on the current line? 439 | var nonSpace = false; // Is there a non-space char on the current line? 440 | 441 | // Strips all whitespace tokens array for the current line 442 | // if there was a {{#tag}} on it and otherwise only space. 443 | function stripSpace() { 444 | if (hasTag && !nonSpace) { 445 | while (spaces.length) { 446 | tokens.splice(spaces.pop(), 1); 447 | } 448 | } else { 449 | spaces = []; 450 | } 451 | 452 | hasTag = false; 453 | nonSpace = false; 454 | } 455 | 456 | var start, type, value, chr; 457 | while (!scanner.eos()) { 458 | start = scanner.pos; 459 | value = scanner.scanUntil(tagRes[0]); 460 | 461 | if (value) { 462 | for (var i = 0, len = value.length; i < len; ++i) { 463 | chr = value.charAt(i); 464 | 465 | if (isWhitespace(chr)) { 466 | spaces.push(tokens.length); 467 | } else { 468 | nonSpace = true; 469 | } 470 | 471 | tokens.push(["text", chr, start, start + 1]); 472 | start += 1; 473 | 474 | if (chr === "\n") { 475 | stripSpace(); // Check for whitespace on the current line. 476 | } 477 | } 478 | } 479 | 480 | start = scanner.pos; 481 | 482 | // Match the opening tag. 483 | if (!scanner.scan(tagRes[0])) { 484 | break; 485 | } 486 | 487 | hasTag = true; 488 | type = scanner.scan(tagRe) || "name"; 489 | 490 | // Skip any whitespace between tag and value. 491 | scanner.scan(whiteRe); 492 | 493 | // Extract the tag value. 494 | if (type === "=") { 495 | value = scanner.scanUntil(eqRe); 496 | scanner.scan(eqRe); 497 | scanner.scanUntil(tagRes[1]); 498 | } else if (type === "{") { 499 | var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1])); 500 | value = scanner.scanUntil(closeRe); 501 | scanner.scan(curlyRe); 502 | scanner.scanUntil(tagRes[1]); 503 | type = "&"; 504 | } else { 505 | value = scanner.scanUntil(tagRes[1]); 506 | } 507 | 508 | // Match the closing tag. 509 | if (!scanner.scan(tagRes[1])) { 510 | throw new Error('Unclosed tag at ' + scanner.pos); 511 | } 512 | 513 | // Check section nesting. 514 | if (type === '/') { 515 | if (sections.length === 0) { 516 | throw new Error('Unopened section "' + value + '" at ' + start); 517 | } 518 | 519 | var section = sections.pop(); 520 | 521 | if (section[1] !== value) { 522 | throw new Error('Unclosed section "' + section[1] + '" at ' + start); 523 | } 524 | } 525 | 526 | var token = [type, value, start, scanner.pos]; 527 | tokens.push(token); 528 | 529 | if (type === '#' || type === '^') { 530 | sections.push(token); 531 | } else if (type === "name" || type === "{" || type === "&") { 532 | nonSpace = true; 533 | } else if (type === "=") { 534 | // Set the tags for the next time around. 535 | tags = value.split(spaceRe); 536 | 537 | if (tags.length !== 2) { 538 | throw new Error('Invalid tags at ' + start + ': ' + tags.join(', ')); 539 | } 540 | 541 | tagRes = escapeTags(tags); 542 | } 543 | } 544 | 545 | // Make sure there are no open sections when we're done. 546 | var section = sections.pop(); 547 | if (section) { 548 | throw new Error('Unclosed section "' + section[1] + '" at ' + scanner.pos); 549 | } 550 | 551 | return nestTokens(squashTokens(tokens)); 552 | }; 553 | 554 | // The high-level clearCache, compile, compilePartial, and render functions 555 | // use this default writer. 556 | var _writer = new Writer(); 557 | 558 | /** 559 | * Clears all cached templates and partials in the default writer. 560 | */ 561 | exports.clearCache = function () { 562 | return _writer.clearCache(); 563 | }; 564 | 565 | /** 566 | * Compiles the given `template` to a reusable function using the default 567 | * writer. 568 | */ 569 | exports.compile = function (template, tags) { 570 | return _writer.compile(template, tags); 571 | }; 572 | 573 | /** 574 | * Compiles the partial with the given `name` and `template` to a reusable 575 | * function using the default writer. 576 | */ 577 | exports.compilePartial = function (name, template, tags) { 578 | return _writer.compilePartial(name, template, tags); 579 | }; 580 | 581 | /** 582 | * Compiles the given array of tokens (the output of a parse) to a reusable 583 | * function using the default writer. 584 | */ 585 | exports.compileTokens = function (tokens, template) { 586 | return _writer.compileTokens(tokens, template); 587 | }; 588 | 589 | /** 590 | * Renders the `template` with the given `view` and `partials` using the 591 | * default writer. 592 | */ 593 | exports.render = function (template, view, partials) { 594 | return _writer.render(template, view, partials); 595 | }; 596 | 597 | // This is here for backwards compatibility with 0.4.x. 598 | exports.to_html = function (template, view, partials, send) { 599 | var result = exports.render(template, view, partials); 600 | 601 | if (typeof send === "function") { 602 | send(result); 603 | } else { 604 | return result; 605 | } 606 | }; 607 | 608 | return exports; 609 | 610 | }()))); 611 | -------------------------------------------------------------------------------- /lib/vendor/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-mate", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "pug-cli": "^1.0.0-alpha6", 6 | "uglify-js": "^3.17.4" 7 | }, 8 | "devDependencies": { 9 | "pug": "3.0.2", 10 | "node-sass": "7.0.3", 11 | "coffee-script": "1.6.3", 12 | "chokidar": "3.5.3" 13 | }, 14 | "engine": "node >= 12.4.0" 15 | } 16 | -------------------------------------------------------------------------------- /proxy/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nvmrc 3 | .target.json 4 | -------------------------------------------------------------------------------- /proxy/README.md: -------------------------------------------------------------------------------- 1 | # API Mate - HTTP Proxy 2 | 3 | ## Usage 4 | 5 | First, install [Node.js](nodejs.org). See the adequate version in [package.json](https://github.com/mconf/api-mate/blob/http-proxy/proxy/package.json). 6 | 7 | Get the source, set up the proxy and run it for the first time: 8 | 9 | ```bash 10 | git clone https://github.com/mconf/api-mate.git 11 | cd api-mate/proxy 12 | npm install 13 | node index.js 14 | ``` 15 | 16 | You should see: 17 | 18 | ```bash 19 | Found configuration file .target.json, using it 20 | Server started at localhost:8000, proxying to test-install.blindsidenetworks.com:80 21 | ``` 22 | 23 | The first time you run it, it will create a configuration file (`.target.json`) for you pointing to the default test server from Blindside Networks at `test-install.blindsidenetworks.com:80`. If you want to proxy the calls to a different server, set its address on `.target.json`. 24 | 25 | Your proxy server will listen at `localhost:8000`, so all you have to do is point the API Mate to this address (and use the salt of your web conference server!): 26 | 27 | ![Use localhost:8000 as the server in the API Mate](https://raw.github.com/mconf/api-mate/master/proxy/img/api-mate-server.png "Use localhost:8000 as the server in the API Mate") 28 | 29 | As you make requests from the API Mate to your proxy, the proxy will print information of the requests it received. 30 | You will notice that the only request that is not proxied is `join`. In this case the user is redirected directly to 31 | the web conference server. 32 | 33 | ![Example of output given by the proxy](https://raw.github.com/mconf/api-mate/master/proxy/img/proxy-output.png "Example of output given by the proxy") 34 | -------------------------------------------------------------------------------- /proxy/img/api-mate-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mconf/api-mate/786ffe64f16031d378e956e793ca023d7a278917/proxy/img/api-mate-server.png -------------------------------------------------------------------------------- /proxy/img/proxy-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mconf/api-mate/786ffe64f16031d378e956e793ca023d7a278917/proxy/img/proxy-output.png -------------------------------------------------------------------------------- /proxy/index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | httpProxy = require('http-proxy'), 3 | fs = require('fs'), 4 | colors = require('colors'); 5 | 6 | // Read or create the configuration file 7 | var config; 8 | var configFile = '.target.json'; 9 | if (fs.existsSync(configFile)) { 10 | console.log(('Found configuration file ' + configFile + ', using it').blue.bold); 11 | file = fs.readFileSync(configFile, { encoding: 'utf8' }); 12 | config = JSON.parse(file); 13 | } else { 14 | console.log(('Did not find configuration file ' + configFile + ', creating it').blue.bold); 15 | config = { 16 | "host": "test-install.blindsidenetworks.com", 17 | "port": 80 18 | }; 19 | fs.writeFileSync(configFile, JSON.stringify(config, null, 2), { encoding: 'utf8' }); 20 | } 21 | 22 | opts = { 23 | // without this option BigBlueButton/nginx will not redirect the call 24 | // properly to bigbluebutton-web 25 | changeOrigin: true 26 | }; 27 | var proxy = new httpProxy.RoutingProxy(opts); 28 | http.createServer(function (req, res) { 29 | console.log("Request received:".yellow, req.method, req.url); 30 | 31 | if (req.method === 'OPTIONS') { 32 | console.log('It\'s a OPTIONS request, sending a default response'.green); 33 | var headers = {}; 34 | headers["Access-Control-Allow-Origin"] = "*"; 35 | headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS"; 36 | headers["Access-Control-Allow-Credentials"] = false; 37 | headers["Access-Control-Max-Age"] = '86400'; // 24 hours 38 | headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept"; 39 | res.writeHead(200, headers); 40 | res.end(); 41 | } else { 42 | 43 | // we don't proxy join requests, redirect the user directly to the join url 44 | if (req.url.match(/\/join\?/)) { 45 | // TODO: get the protocol used in the request 46 | var destination = 'http://' + config.host + ':' + config.port + req.url; 47 | console.log('It\'s a join, redirecting to:'.green, destination); 48 | res.writeHead(302, { Location: destination }); 49 | res.end(); 50 | 51 | } else { 52 | 53 | // accept cross-domain requests for all requests 54 | res.setHeader('Access-Control-Allow-Origin', '*'); 55 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'); 56 | 57 | // proxy everything to the target server 58 | var buffer = httpProxy.buffer(req); 59 | proxy.proxyRequest(req, res, { 60 | port: config.port, 61 | host: config.host, 62 | buffer: buffer 63 | }); 64 | } 65 | 66 | } 67 | 68 | }).listen(8000); 69 | 70 | proxy.on('end', function () { 71 | console.log("The request was proxied".green); 72 | }); 73 | 74 | console.log(('Server started at localhost:8000, proxying to ' + config.host + ':' + config.port).blue.bold); 75 | -------------------------------------------------------------------------------- /proxy/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-mate-proxy", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "colors": { 8 | "version": "0.6.2", 9 | "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", 10 | "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" 11 | }, 12 | "eventemitter3": { 13 | "version": "4.0.7", 14 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", 15 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 16 | }, 17 | "follow-redirects": { 18 | "version": "1.15.2", 19 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", 20 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" 21 | }, 22 | "http-proxy": { 23 | "version": "1.18.1", 24 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", 25 | "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", 26 | "requires": { 27 | "eventemitter3": "^4.0.0", 28 | "follow-redirects": "^1.0.0", 29 | "requires-port": "^1.0.0" 30 | } 31 | }, 32 | "requires-port": { 33 | "version": "1.0.0", 34 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 35 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-mate-proxy", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "http-proxy": "1.18.1", 7 | "colors": "0.6.2" 8 | }, 9 | "engines": { 10 | "node": ">= 0.10" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/css/api_mate.scss: -------------------------------------------------------------------------------- 1 | @import "application.scss"; 2 | 3 | $font-size: 14px; 4 | 5 | .api-mate-results { 6 | clear: both; 7 | overflow: hidden; 8 | 9 | .api-mate-link-wrapper { 10 | clear: both; 11 | width: 100%; 12 | //border-top: 1px solid #eee; 13 | padding: 4px 0; 14 | } 15 | 16 | .api-mate-link { 17 | padding: 2px; 18 | white-space: nowrap; 19 | line-height: 18px; 20 | font-size: $font-size - 1px; 21 | word-break: normal; 22 | 23 | .label { 24 | font-size: $font-size - 2px; 25 | margin: 2px 5px 0 0; 26 | 27 | &.disabled { 28 | opacity: 0.5; 29 | background: #888; 30 | } 31 | } 32 | 33 | &.expanded { 34 | word-break: break-all; 35 | white-space: normal; 36 | padding: 5px; 37 | .api-mate-method-name { 38 | display: block; 39 | margin-bottom: 5px; 40 | } 41 | } 42 | 43 | i { 44 | display: none; 45 | float: left; 46 | margin-right: 5px; 47 | margin-top: 1px; 48 | font-size: 15px; 49 | } 50 | } 51 | 52 | .api-mate-url-standard { 53 | border-color: #3A87AD; 54 | .label { 55 | background: lighten(#3A87AD, 10); 56 | } 57 | i { color: #3A87AD; } 58 | } 59 | .api-mate-url-recordings { 60 | border-color: #468847; 61 | .label { 62 | background: lighten(#468847, 10); 63 | } 64 | i { color: #468847; } 65 | } 66 | .api-mate-url-from-mobile { 67 | border-color: #f89406; 68 | .label { 69 | background: lighten(#f89406, 10); 70 | } 71 | i { color: #f89406; } 72 | } 73 | .api-mate-url-custom-call { 74 | border-color: #B94A48; 75 | .label { 76 | background: lighten(#B94A48, 10); 77 | } 78 | i { color: #B94A48; } 79 | } 80 | 81 | .api-mate-method-name { 82 | white-space: nowrap; 83 | } 84 | 85 | &.updated { 86 | .api-mate-result-title { 87 | color: #EE5F5B; 88 | } 89 | .api-mate-link { 90 | i { color: #EE5F5B; } 91 | .label { background: #EE5F5B; } 92 | .api-mate-method-name { color: #EE5F5B; } 93 | } 94 | } 95 | 96 | .label-title { 97 | margin-top: 0; 98 | font-size: 10px; 99 | float: right; 100 | margin-top: 10px; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/css/application.scss: -------------------------------------------------------------------------------- 1 | $font-size: 14px; 2 | $link-color: #008db6; 3 | 4 | body { 5 | overflow-x: hidden; 6 | transition: all .2s; 7 | 8 | * { 9 | transition: background .8s; 10 | } 11 | } 12 | 13 | a { 14 | color: $link-color; 15 | } 16 | 17 | #page-header-wrapper { 18 | border: 0; 19 | border-radius: 0; 20 | /* background: #24292e; */ 21 | background: #314b5d; 22 | border-bottom: 1px solid #314b5d; 23 | color: #eee; 24 | margin-bottom: 20px; 25 | transition: all .8s; 26 | } 27 | 28 | #page-header { 29 | padding: 16px; 30 | margin: 0; 31 | 32 | .logo { 33 | h2 { 34 | font-size: 36px; 35 | margin: 0; 36 | font-weight: bold; 37 | display: block; 38 | /* text-shadow: 2px 2px #111; */ 39 | } 40 | 41 | .description { 42 | white-space: nowrap; 43 | display: block; 44 | color: #ddd; 45 | margin-top: 10px; 46 | } 47 | } 48 | } 49 | 50 | #page-header-docs { 51 | font-size: $font-size; 52 | margin: 8px 0 0 0; 53 | color: #bbb; 54 | 55 | a { 56 | margin: 0 0 0 8px; 57 | color: #eee; 58 | text-decoration: underline; 59 | 60 | &:hover { 61 | color: $link-color; 62 | } 63 | } 64 | } 65 | 66 | #theme { 67 | text-align: right; 68 | position: absolute; 69 | right: 5px; 70 | bottom: 5px; 71 | 72 | .theme-option { 73 | display: inline-block; 74 | } 75 | } 76 | 77 | #nav { 78 | padding: 0 20px; 79 | 80 | // make it smaller than originally in bootstrap 81 | > li > a { 82 | padding: 5px 10px; 83 | } 84 | } 85 | 86 | #github-banner, #github-banner * { 87 | z-index: 9999; 88 | img { 89 | width: 130px; 90 | } 91 | } 92 | 93 | #content { 94 | width: auto; 95 | padding: 0; 96 | margin: 0 0 30px 0; 97 | } 98 | 99 | #footer { 100 | border-top: 1px dashed #ccc; 101 | padding: 10px 0; 102 | margin: 0 10px 10px 10px; 103 | 104 | .thanks { 105 | .glyphicon { 106 | margin-left: 5px; 107 | } 108 | } 109 | 110 | .experimental { 111 | text-align: right; 112 | padding-right: 40px; 113 | } 114 | } 115 | @media(max-width:767px) { 116 | #footer { 117 | .thanks { text-align: center; } 118 | .experimental { text-align: center; } 119 | } 120 | } 121 | 122 | #options { 123 | margin-bottom: 15px; 124 | text-align: right; 125 | 126 | #options-actions { 127 | margin-bottom: 5px; 128 | } 129 | 130 | button { 131 | margin-bottom: 3px; 132 | 133 | .glyphicon { 134 | margin-right: 5px; 135 | display: none; 136 | } 137 | &.active .glyphicon { display: inline-block; } 138 | } 139 | } 140 | 141 | #menu { 142 | transition: all .8s; 143 | 144 | #menu-server { 145 | clear: both; 146 | margin-bottom: 10px; 147 | } 148 | 149 | #menu-inputs { 150 | clear: both; 151 | margin-top: 32px; 152 | 153 | .form-group { 154 | display: block; 155 | clear: both; 156 | 157 | .tip { 158 | font-size: $font-size; 159 | color: #888; 160 | margin-top: 3px; 161 | font-style: italic; 162 | 163 | &::before { 164 | content: '↘'; 165 | margin-right: 5px; 166 | } 167 | } 168 | }; 169 | 170 | input[type=checkbox] { 171 | margin: 0 0 0 5px; 172 | width: auto; // otherwise will look gigantic on chrome 173 | } 174 | 175 | textarea { 176 | resize: vertical; 177 | } 178 | 179 | #pre-upload-text { 180 | display: none; 181 | } 182 | } 183 | 184 | .experimental { 185 | color: darkorange; 186 | cursor: pointer; 187 | } 188 | 189 | label { 190 | font-weight: normal; 191 | font-size: 0.9em; 192 | margin-bottom: 2px; 193 | } 194 | 195 | } 196 | 197 | #tab-all { 198 | } 199 | 200 | #tab-config-xml { 201 | 202 | dl.dl-horizontal { 203 | dt { 204 | width: 20px; 205 | } 206 | dd { 207 | margin-left: 25px; 208 | } 209 | } 210 | 211 | .step { 212 | margin-bottom: 20px; 213 | 214 | .step-headline { 215 | margin-top: 2px; 216 | margin-bottom: 10px; 217 | 218 | .label-danger { 219 | margin-right: 5px; 220 | } 221 | 222 | ul.step-options { 223 | padding-left: 20px; 224 | margin-top: 5px; 225 | } 226 | } 227 | 228 | } 229 | 230 | #config-xml, #config-xml-2 { 231 | height: 150px; 232 | } 233 | 234 | } 235 | 236 | .input-clean-title { 237 | border-top: 0; 238 | border-left: 0; 239 | border-right: 0; 240 | border-radius: 0; 241 | border-bottom-width: 1px; 242 | box-shadow: none; 243 | font-size: 13px; 244 | border-left: 3px solid #5CB85C; /* #C7254E; */ 245 | 246 | &:focus { 247 | box-shadow: none; 248 | } 249 | } 250 | 251 | .section-title { 252 | font-size: 13px; 253 | padding: 2px 10px; 254 | margin-bottom: 10px; 255 | border-bottom: 1px solid #ddd; 256 | color: #888; 257 | clear: both; 258 | } 259 | 260 | // media queries for bootstrap 3 261 | /* @media(max-width:767px){} */ 262 | /* @media(min-width:768px){} */ 263 | /* @media(min-width:992px){} */ 264 | /* @media(min-width:1200px){} */ 265 | 266 | .form-control:focus { 267 | border-color: #54a0c6; 268 | } 269 | 270 | body.dark-theme { 271 | background: #111; 272 | color: #ddd; 273 | transition: all .8s; 274 | 275 | /* github gray: 24292e */ 276 | 277 | #page-header-wrapper { 278 | background: #000; 279 | border-bottom: 1px solid #222; 280 | transition: all .8s; 281 | } 282 | 283 | #menu { 284 | transition: all .8s; 285 | 286 | input, textarea { 287 | background: #000; 288 | color: #ccc; 289 | border-color: #000; 290 | 291 | &:focus, 292 | &:active { 293 | box-shadow: none; 294 | border-color: #7b2d29; //#54a0c6; 295 | } 296 | } 297 | 298 | .input-clean-title { 299 | background-color: transparent; 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/css/redis_events.scss: -------------------------------------------------------------------------------- 1 | @import "application.scss"; 2 | 3 | .events-template { 4 | cursor: pointer; 5 | margin: 10px 0; 6 | 7 | code { 8 | font-size: 90%; 9 | padding: 2px 4px; 10 | background-color: #f6f6f6; 11 | white-space: pre-wrap; 12 | border: 0; 13 | word-wrap: break-word; 14 | word-break: break-all; 15 | display: block; 16 | color: #333; 17 | 18 | strong { 19 | // color: #C7254E; 20 | color: #357EBD; 21 | } 22 | 23 | } 24 | 25 | .event-name { 26 | text-align: center; 27 | background-color: #f6f6f6; 28 | font-size: 90%; 29 | border-bottom: 1px solid #ddd; 30 | } 31 | 32 | &.updated { 33 | .event-name, code { 34 | color: #EE5F5B; 35 | /* background: #F2DEDE; */ 36 | } 37 | } 38 | } 39 | 40 | #input-event-out-content { 41 | resize: vertical; 42 | font-size: 90%; 43 | white-space: pre-wrap; 44 | word-wrap: break-word; 45 | word-break: break-all; 46 | 47 | &.updated { 48 | color: #EE5F5B; 49 | /* background: #F2DEDE; */ 50 | } 51 | } 52 | 53 | .event-out-content { 54 | margin-bottom: 5px; 55 | } 56 | 57 | .event-out-submit { 58 | float: right; 59 | margin: 0 0 10px 0; 60 | padding-top: 3px; 61 | } 62 | 63 | .event-out-pretty { 64 | float: left; 65 | margin: 0 0 10px 0; 66 | 67 | label { 68 | display: inline; 69 | font-weight: normal; 70 | } 71 | .form-control { 72 | display: inline; 73 | width: auto; 74 | margin: -2px 3px 0 0; 75 | } 76 | } 77 | 78 | #events-results { 79 | margin: 0; 80 | padding: 0; 81 | 82 | li { 83 | margin: 0; 84 | padding: 0; 85 | } 86 | 87 | .events-result { 88 | padding: 5px 10px; 89 | margin: 0 0 10px 0; 90 | background: none; 91 | border: 0; 92 | border-radius: 0; 93 | 94 | &.received { 95 | } 96 | &.sent { 97 | .glyphicon { 98 | color: #5CB85C; 99 | margin-right: 5px; 100 | cursor: pointer; 101 | } 102 | } 103 | } 104 | } 105 | 106 | #menu-server { 107 | .connected-label { 108 | float: right; 109 | color: green; 110 | } 111 | 112 | .disconnected-label { 113 | float: right; 114 | color: red; 115 | } 116 | 117 | &.disconnected { 118 | .section-title { border-color: red; } 119 | .connected-label { display: none; } 120 | .disconnected-label { display: block; } 121 | } 122 | &.connected { 123 | .section-title { border-color: green; } 124 | .connected-label { display: block; } 125 | .disconnected-label { display: none; } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/js/api_mate.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | placeholders = 3 | results: '#api-mate-results' 4 | modal: '#post-response-modal' 5 | apiMate = new ApiMate(placeholders) 6 | apiMate.start() 7 | $('#api-mate-results').on 'api-mate-urls-added', -> 8 | Application.bindTooltips() 9 | 10 | # A class that does all the logic of the API Mate. It's integrated with the html markup 11 | # via data attributes and a few classes. Can be used with other applications than the 12 | # API Mate to provide similar functionality. 13 | # 14 | # Depends on: 15 | # * jQuery 16 | # * underscore/lodash 17 | # * mustache 18 | # * bigbluebutton-api-js 19 | # * bootstrap (for the modal, especially) 20 | # 21 | window.ApiMate = class ApiMate 22 | 23 | # `placeholders` should be an object with the properties: 24 | # * `results`: a string with the jQuery selector for the element that will contain 25 | # the URLs generated. 26 | # * `modal`: a string with the jQuery selector for an element that will be used as 27 | # a modal window (should follow bootstrap's model for modals). 28 | # 29 | # `templates` should be an object with the properties: 30 | # * `results`: a string with a mustache template to show the list of links generated. 31 | # * `postSuccess`: a string with a mustache template with the internal content of the 32 | # modal when showing a success message for a POST request. 33 | # * `postError`: a string with a mustache template with the internal content of the 34 | # modal when showing an error message for a POST request. 35 | # * `preUpload`: a string with a mustache template to format the body of a the POST 36 | # request to pre-upload files when creating a conference. 37 | constructor: (@placeholders, @templates) -> 38 | @updatedTimer = null 39 | @urls = [] # last set of urls generated 40 | @placeholders ?= {} 41 | @templates ?= {} 42 | @templates['results'] ?= resultsTemplate 43 | @templates['postSuccess'] ?= postSuccessTemplate 44 | @templates['postError'] ?= postErrorTemplate 45 | @templates['preUpload'] ?= preUploadUrl 46 | @debug = false 47 | @urlsLast = null 48 | 49 | start: -> 50 | # set random values in some inputs 51 | @initializeMenu() 52 | 53 | # when the meeting name is changed, change the id also 54 | $("[data-api-mate-param*='meetingID']").on "keyup", -> 55 | $("[data-api-mate-param*='name']").val $(this).val() 56 | 57 | # triggers to generate the links 58 | $("[data-api-mate-param]").on "change keyup", (e) => 59 | @generateUrls() 60 | @addUrlsToPage(@urls) 61 | $("[data-api-mate-server]").on "change keyup", (e) => 62 | @generateUrls() 63 | @addUrlsToPage(@urls) 64 | $("[data-api-mate-special-param]").on "change keyup", (e) => 65 | @generateUrls() 66 | @addUrlsToPage(@urls) 67 | $("[data-api-mate-sha]").on "click", (e) => 68 | $("[data-api-mate-sha]").removeClass('active') 69 | $(e.target).addClass('active') 70 | @generateUrls() 71 | @addUrlsToPage(@urls) 72 | 73 | # expand or collapse links 74 | $("[data-api-mate-expand]").on "click", => 75 | selected = !$("[data-api-mate-expand]").hasClass("active") 76 | @expandLinks(selected) 77 | true 78 | 79 | # button to clear the inputs 80 | $("[data-api-mate-clear]").on "click", (e) => 81 | @clearAllFields() 82 | @generateUrls() 83 | @addUrlsToPage(@urls) 84 | 85 | # button to re-randomize menu 86 | $("[data-api-mate-randomize]").on "click", (e) => 87 | @initializeMenu() 88 | @generateUrls() 89 | @addUrlsToPage(@urls) 90 | 91 | # set our debug flag 92 | $("[data-api-mate-debug]").on "click", => 93 | selected = !$("[data-api-mate-debug]").hasClass("active") 94 | @debug = selected 95 | true 96 | 97 | # generate the links already on setup 98 | @generateUrls() 99 | @addUrlsToPage(@urls) 100 | 101 | # binding elements 102 | @bindPostRequests() 103 | 104 | # search 105 | @bindSearch() 106 | 107 | initializeMenu: -> 108 | vbridge = "7" + pad(Math.floor(Math.random() * 10000 - 1).toString(), 4) 109 | $("[data-api-mate-param*='voiceBridge']").val(vbridge) 110 | name = "random-" + Math.floor(Math.random() * 10000000).toString() 111 | $("[data-api-mate-param*='name']").val(name) 112 | $("[data-api-mate-param*='meetingID']").val(name) 113 | $("[data-api-mate-param*='recordID']").val(name) 114 | user = "User " + Math.floor(Math.random() * 10000000).toString() 115 | $("[data-api-mate-param*='fullName']").val(user) 116 | 117 | @setMenuValuesFromURL() 118 | 119 | # Add a div with all links and a close button to the global 120 | # results container 121 | addUrlsToPage: (urls) -> 122 | # don't do it again unless something changed 123 | isEqual = urls? and @urlsLast? and (JSON.stringify(urls) == JSON.stringify(@urlsLast)) 124 | return if isEqual 125 | @urlsLast = _.map(urls, _.clone) 126 | 127 | placeholder = $(@placeholders['results']) 128 | for item in urls 129 | desc = item.description 130 | if desc.match(/recording/i) 131 | item.urlClass = "api-mate-url-recordings" 132 | else if desc.match(/mobile/i) 133 | item.urlClass = "api-mate-url-from-mobile" 134 | else if desc.match(/custom call/i) 135 | item.urlClass = "api-mate-url-custom-call" 136 | else 137 | item.urlClass = "api-mate-url-standard" 138 | opts = 139 | title: new Date().toTimeString() 140 | urls: urls 141 | html = Mustache.to_html(@templates['results'], opts) 142 | $('.results-tooltip').remove() 143 | $(placeholder).html(html) 144 | @expandLinks($("[data-api-mate-expand]").hasClass("active")) 145 | 146 | # mark the items as updated 147 | $('.api-mate-results', @placeholders['results']).addClass("updated") 148 | clearTimeout(@updatedTimer) 149 | @updatedTimer = setTimeout( => 150 | $('.api-mate-results', @placeholders['results']).removeClass("updated") 151 | , 300) 152 | 153 | $(@placeholders['results']).trigger('api-mate-urls-added') 154 | 155 | # Returns a BigBlueButtonApi configured with the server set by the user in the inputs. 156 | getApi: -> 157 | server = {} 158 | server.url = $("[data-api-mate-server='url']").val() 159 | server.salt = $("[data-api-mate-server='salt']").val() 160 | 161 | # Do some cleanups on the server URL to that pasted URLs in various formats work better 162 | # Remove trailing /, and add /api on the end if missing. 163 | server.url = server.url.replace(/(\/api)?\/?$/, '/api') 164 | server.name = server.url 165 | 166 | opts = {} 167 | shaLevels = [ 168 | 'sha1', 169 | 'sha256', 170 | 'sha384', 171 | 'sha512', 172 | ] 173 | # Find the active SHA level, could have only one active so we can break out of the loop 174 | for level in shaLevels 175 | if $("[data-api-mate-sha='#{level}']").hasClass("active") 176 | opts.shaType = level 177 | 178 | new BigBlueButtonApi(server.url, server.salt, @debug, opts) 179 | 180 | # Generate urls for all API calls and store them internally in `@urls`. 181 | generateUrls: () -> 182 | params = {} 183 | customParams = {} 184 | 185 | $('[data-api-mate-param]').each -> 186 | $elem = $(this) 187 | attrs = $elem.attr('data-api-mate-param').split(',') 188 | value = inputValue($elem) 189 | if attrs? and value? 190 | for attr in attrs 191 | params[attr] = value 192 | true # don't ever stop 193 | 194 | lines = inputValue("textarea[data-api-mate-special-param='meta']") 195 | if lines? 196 | lines = lines.replace(/\r\n/g, "\n").split("\n") 197 | for line in lines 198 | separator = line.indexOf("=") 199 | if separator >= 0 200 | paramName = line.substring(0, separator) 201 | paramValue = line.substring(separator+1, line.length) 202 | params["meta_" + paramName] = paramValue 203 | 204 | lines = inputValue("textarea[data-api-mate-special-param='custom-params']") 205 | if lines? 206 | lines = lines.replace(/\r\n/g, "\n").split("\n") 207 | for line in lines 208 | separator = line.indexOf("=") 209 | if separator >= 0 210 | paramName = line.substring(0, separator) 211 | paramValue = line.substring(separator+1, line.length) 212 | params["custom_" + paramName] = paramValue 213 | customParams["custom_" + paramName] = paramValue 214 | 215 | lines = inputValue("textarea[data-api-mate-special-param='custom-calls']") 216 | if lines? 217 | lines = lines.replace(/\r\n/g, "\n").split("\n") 218 | customCalls = lines 219 | else 220 | customCalls = null 221 | 222 | # generate the list of links 223 | api = @getApi() 224 | @urls = [] 225 | 226 | # standard API calls 227 | _elem = (name, desc, url) -> 228 | { name: name, description: desc, url: url } 229 | for name in api.availableApiCalls() 230 | if name is 'join' 231 | params['password'] = params['moderatorPW'] 232 | @urls.push _elem(name, "#{name} as moderator", api.urlFor(name, params)) 233 | params['password'] = params['attendeePW'] 234 | @urls.push _elem(name, "#{name} as attendee", api.urlFor(name, params)) 235 | 236 | # so all other calls will use the moderator password 237 | params['password'] = params['moderatorPW'] 238 | else 239 | @urls.push _elem(name, name, api.urlFor(name, params)) 240 | 241 | # custom API calls set by the user 242 | if customCalls? 243 | for name in customCalls 244 | @urls.push _elem(name, "custom call: #{name}", api.urlFor(name, customParams, false)) 245 | 246 | # for mobile 247 | params['password'] = params['moderatorPW'] 248 | @urls.push _elem("join", "mobile call: join as moderator", api.setMobileProtocol(api.urlFor("join", params))) 249 | params['password'] = params['attendeePW'] 250 | @urls.push _elem("join", "mobile call: join as attendee", api.setMobileProtocol(api.urlFor("join", params))) 251 | 252 | # Empty all inputs in the configuration menu 253 | clearAllFields: -> 254 | $("[data-api-mate-param]").each -> 255 | $(this).val("") 256 | $(this).attr("checked", null) 257 | 258 | # Expand (if `selected` is true) or collapse the links. 259 | expandLinks: (selected) -> 260 | if selected 261 | $(".api-mate-link", @placeholders['results']).addClass('expanded') 262 | else 263 | $(".api-mate-link", @placeholders['results']).removeClass('expanded') 264 | 265 | # Logic for when a button to send a request via POST is clicked. 266 | bindPostRequests: -> 267 | _apiMate = this 268 | $(document).on 'click', 'a[data-api-mate-post]', (e) -> 269 | $target = $(this) 270 | href = $target.attr('data-url') 271 | 272 | # get the data to be posted for this method and the content type 273 | method = $target.attr('data-api-mate-post') 274 | data = _apiMate.getPostData(method) 275 | contentType = _apiMate.getPostContentType(method) 276 | 277 | $('[data-api-mate-post]').addClass('disabled') 278 | $.ajax 279 | url: href 280 | type: "POST" 281 | crossDomain: true 282 | contentType: contentType 283 | dataType: "xml" 284 | data: data 285 | complete: (jqxhr, status) -> 286 | # TODO: show the result properly formatted and highlighted in the modal 287 | modal = _apiMate.placeholders['modal'] 288 | postSuccess = _apiMate.templates['postSuccess'] 289 | postError = _apiMate.templates['postError'] 290 | 291 | if jqxhr.status is 200 292 | $('.modal-header', modal).removeClass('alert-danger') 293 | $('.modal-header', modal).addClass('alert-success') 294 | html = Mustache.to_html(postSuccess, { response: jqxhr.responseText }) 295 | $('.modal-body', modal).html(html) 296 | else 297 | $('.modal-header h4', modal).text('Ooops!') 298 | $('.modal-header', modal).addClass('alert-danger') 299 | $('.modal-header', modal).removeClass('alert-success') 300 | opts = 301 | status: jqxhr.status 302 | statusText: jqxhr.statusText 303 | opts['response'] = jqxhr.responseText unless _.isEmpty(jqxhr.responseText) 304 | html = Mustache.to_html(postError, opts) 305 | $('.modal-body', modal).html(html) 306 | 307 | $(modal).modal({ show: true }) 308 | $('[data-api-mate-post]').removeClass('disabled') 309 | 310 | e.preventDefault() 311 | false 312 | 313 | getPostData: (method) -> 314 | if method is 'create' 315 | urls = inputValue("textarea[data-api-mate-param='pre-upload']") 316 | if urls? 317 | urls = urls.replace(/\r\n/g, "\n").split("\n") 318 | urls = _.map(urls, (u) -> { url: u }) 319 | opts = { urls: urls } 320 | Mustache.to_html(@templates['preUpload'], opts) 321 | else if method is 'setConfigXML' 322 | if isFilled("textarea[data-api-mate-param='configXML']") 323 | api = @getApi() 324 | query = "configXML=#{api.encodeForUrl($("#input-config-xml").val())}" 325 | query += "&meetingID=#{api.encodeForUrl($("#input-mid").val())}" 326 | checksum = api.checksum('setConfigXML', query) 327 | query += "&checksum=" + checksum 328 | query 329 | 330 | getPostContentType: (method) -> 331 | if method is 'create' 332 | 'application/xml; charset=utf-8' 333 | else if method is 'setConfigXML' 334 | 'application/x-www-form-urlencoded' 335 | 336 | bindSearch: -> 337 | _apiMate = this 338 | $(document).on 'keyup', '[data-api-mate-search-input]', (e) -> 339 | $target = $(this) 340 | searchTerm = inputValue($target) 341 | 342 | search = -> 343 | $elem = $(this) 344 | if searchTerm? and not _.isEmpty(searchTerm.trim()) 345 | visible = false 346 | searchRe = makeSearchRegexp(searchTerm) 347 | attrs = $elem.attr('data-api-mate-param')?.split(',') or [] 348 | attrs = attrs.concat($elem.attr('data-api-mate-search')?.split(',') or []) 349 | for attr in attrs 350 | visible = true if attr.match(searchRe) 351 | else 352 | visible = true 353 | 354 | if visible 355 | $elem.parents('.form-group').show() 356 | else 357 | $elem.parents('.form-group').hide() 358 | true # don't ever stop 359 | 360 | $('[data-api-mate-param]').each(search) 361 | $('[data-api-mate-special-param]').each(search) 362 | 363 | setMenuValuesFromURL: -> 364 | # set values based on parameters in the URL 365 | # gives priority to params in the hash (e.g. 'api_mate.html#sharedSecret=123') 366 | query = getHashParams() 367 | # but also accept params in the search string for backwards compatibility (e.g. 'api_mate.html?sharedSecret=123') 368 | query2 = parseQueryString(window.location.search.substring(1)) 369 | query = _.extend(query2, query) 370 | if query.server? 371 | $("[data-api-mate-server='url']").val(query.server) 372 | delete query.server 373 | # accept several options for the secret 374 | if query.salt? 375 | $("[data-api-mate-server='salt']").val(query.salt) 376 | delete query.salt 377 | if query.sharedSecret? 378 | $("[data-api-mate-server='salt']").val(query.sharedSecret) 379 | delete query.sharedSecret 380 | if query.secret? 381 | $("[data-api-mate-server='salt']").val(query.secret) 382 | delete query.secret 383 | # all other properties 384 | for prop, value of query 385 | setInputValue($("[data-api-mate-param='#{prop}']"), value) 386 | setInputValue($("[data-api-mate-special-param='#{prop}']"), value) 387 | 388 | 389 | # Returns the value set in an input, if any. For checkboxes, returns the value 390 | # as a boolean. For any other input, return as a string. 391 | # `selector` can be a string with a selector or a jQuery object. 392 | inputValue = (selector) -> 393 | $elem = $(selector) 394 | 395 | type = $elem.attr('type') or $elem.prop('tagName')?.toLowerCase() 396 | switch type 397 | when 'checkbox' 398 | $elem.is(":checked") 399 | else 400 | value = $elem.val() 401 | if value? and not _.isEmpty(value.trim()) 402 | value 403 | else 404 | null 405 | 406 | # Sets `value` as the value of the input. For checkboxes checks the input if the value 407 | # is anything other than [null, undefined, 0]. 408 | # `selector` can be a string with a selector or a jQuery object. 409 | setInputValue = (selector, value) -> 410 | $elem = $(selector) 411 | 412 | type = $elem.attr('type') or $elem.prop('tagName')?.toLowerCase() 413 | switch type 414 | when 'checkbox' 415 | val = value? && value != '0' && value != 0 416 | $elem.prop('checked', val) 417 | else 418 | $elem.val(value) 419 | 420 | # Check if an input text field has a valid value (not empty). 421 | isFilled = (field) -> 422 | value = $(field).val() 423 | value? and not _.isEmpty(value.trim()) 424 | 425 | # Pads a number `num` with zeros up to `size` characters. Returns a string with it. 426 | # Example: 427 | # pad(123, 5) 428 | # > '00123' 429 | pad = (num, size) -> 430 | s = '' 431 | s += '0' for [0..size-1] 432 | s += num 433 | s.substr(s.length-size) 434 | 435 | # Parse the query string into an object 436 | # From http://www.joezimjs.com/javascript/3-ways-to-parse-a-query-string-in-a-url/ 437 | parseQueryString = (queryString) -> 438 | params = {} 439 | 440 | # Split into key/value pairs 441 | if queryString? and not _.isEmpty(queryString) 442 | queries = queryString.split("&") 443 | else 444 | queries = [] 445 | 446 | # Convert the array of strings into an object 447 | i = 0 448 | l = queries.length 449 | while i < l 450 | temp = queries[i].split('=') 451 | params[temp[0]] = temp[1] 452 | i++ 453 | 454 | params 455 | 456 | makeSearchRegexp = (term) -> 457 | terms = term.split(" ") 458 | terms = _.filter(terms, (t) -> not _.isEmpty(t.trim())) 459 | terms = _.map(terms, (t) -> ".*#{t}.*") 460 | terms = terms.join('|') 461 | new RegExp(terms, "i"); 462 | 463 | # Get the parameters from the hash in the URL 464 | # Adapted from: http://stackoverflow.com/questions/4197591/parsing-url-hash-fragment-identifier-with-javascript#answer-4198132 465 | getHashParams = -> 466 | hashParams = {} 467 | a = /\+/g # Regex for replacing addition symbol with a space 468 | r = /([^&;=]+)=?([^&;]*)/g 469 | d = (s) -> decodeURIComponent(s.replace(a, " ")) 470 | q = window.location.hash.substring(1) 471 | hashParams[d(e[1])] = d(e[2]) while e = r.exec(q) 472 | hashParams 473 | -------------------------------------------------------------------------------- /src/js/application.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | Application.bindTooltips() 3 | 4 | window.Application = class Application 5 | @bindTooltips: -> 6 | defaultOptions = 7 | container: 'body' 8 | placement: 'top' 9 | template: '' 10 | $('.tooltipped').tooltip(defaultOptions) 11 | -------------------------------------------------------------------------------- /src/js/redis_events.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | redisEvents = new RedisEvents() 3 | redisEvents.bind() 4 | 5 | $(".events-template").on "click", (e) -> 6 | redisEvents.selectTemplate($(".event-json", $(this)).text()) 7 | 8 | # highlight the template selected 9 | $(this).addClass("updated") 10 | clearTimeout(@selectTemplateTimeout) 11 | @selectTemplateTimeout = setTimeout( => 12 | $(this).removeClass("updated") 13 | , 300) 14 | 15 | window.RedisEvents = class RedisEvents 16 | 17 | constructor: -> 18 | @serverUrl = @getServerUrlFromInput() 19 | @pushPath = '/push' 20 | @pullPath = '/pull' 21 | @publishChannel = null 22 | @source = null 23 | @lastContentSent = null 24 | @searchTimeout = null 25 | 26 | bind: -> 27 | @bindSearch() 28 | 29 | # Button to send and event to the server 30 | $("[data-events-out-submit]").on "click", (e) => 31 | content = $("[data-events-out-content]").val() 32 | content = JSON.parse(content) # TODO: error in case is not valid 33 | channel = $("[data-events-out-channel]").val().trim() 34 | channel = 'to-bbb-apps' if not channel? or channel is "" 35 | @lastContentSent = content 36 | @sendEvent({ channel: channel, data: content }) 37 | 38 | # Button to subscribe to the events from the server 39 | $("[data-events-server-connect]").on "click", (e) => 40 | url = @getServerUrlFromInput() 41 | @connect(url) 42 | 43 | $("[data-events-out-pretty]").on "click", (e) => 44 | @selectTemplate($("[data-events-out-content]").val()) 45 | 46 | bindSearch: -> 47 | timeout = @searchTimeout 48 | $(document).on 'keyup', '[data-events-search-input]', (e) -> 49 | $searchInput = $(this) 50 | 51 | search = -> 52 | searchTerm = $searchInput.val() 53 | showOrHide = -> 54 | $elem = $(this) 55 | if searchTerm? and not _.isEmpty(searchTerm.trim()) 56 | visible = false 57 | searchRe = makeSearchRegexp(searchTerm) 58 | eventText = $("[data-events-template-content]", $elem).text() 59 | visible = true if eventText.match(searchRe) 60 | else 61 | visible = true 62 | if visible 63 | $elem.show() 64 | else 65 | $elem.hide() 66 | true # don't ever stop 67 | $('[data-events-template]').each(showOrHide) 68 | 69 | clearTimeout(timeout) 70 | timeout = setTimeout( -> 71 | search() 72 | , 200) 73 | 74 | getServerUrlFromInput: -> 75 | $("[data-events-server='url']").val() 76 | 77 | onMessageReceived: (e) => 78 | console.log(e) 79 | @setConnected(true) 80 | data = JSON.parse(e.data) 81 | if $("[data-events-out-pretty]").is(":checked") 82 | pretty = JSON.stringify(data, null, 4) 83 | else 84 | pretty = JSON.stringify(data, null, 0) 85 | 86 | return if @excludeEvent(pretty) 87 | 88 | if JSON.stringify(data) is JSON.stringify(@lastContentSent) 89 | @lastContentSent = null 90 | $message = $('
').html(pretty)
 91 |       $icon = $('')
 92 |       $message.prepend($icon)
 93 |       # $label = $('').html('sent')
 94 |       # $message.prepend($label)
 95 |     else
 96 |       $message = $('
').html(pretty)
 97 |       # $label = $('').html('received')
 98 |       # $message.prepend($label)
 99 | 
100 |     $('#events-results').prepend($message)
101 |     Application.bindTooltips()
102 | 
103 |   onMessageError: (e) =>
104 |     console.log "EventSource failed"
105 |     @setConnected(false)
106 | 
107 |   excludeEvent: (str) ->
108 |     patterns = $("[data-events-config='exclude']").val()
109 |     for pattern in patterns.split('\n')
110 |       if str? and pattern? and pattern.trim() != '' and str.match(pattern)
111 |         return true
112 |     false
113 | 
114 |   connect: (url) ->
115 |     try
116 |       @serverUrl = url
117 |       @setConnected(true)
118 |       @source.close() if @source?
119 |       @source = new EventSource("#{@serverUrl}#{@pullPath}", { withCredentials: false })
120 |       @source.onerror = @onMessageError
121 |       @source.onmessage = @onMessageReceived
122 |     catch
123 |       @setConnected(false)
124 | 
125 |   setConnected: (connected) ->
126 |     if connected
127 |       $("#menu-server").removeClass("disconnected")
128 |       $("#menu-server").addClass("connected")
129 |     else
130 |       $("#menu-server").removeClass("connected")
131 |       $("#menu-server").addClass("disconnected")
132 | 
133 |   sendEvent: (content) ->
134 |     url = "#{@serverUrl}#{@pushPath}"
135 |     console.log "Sending the event", content, "to", url
136 |     $.ajax
137 |       url: url
138 |       type: 'POST'
139 |       cache: false
140 |       data: JSON.stringify(content)
141 |       crossdomain: true
142 |       contentType: 'application/json'
143 |       success: (data) ->
144 |         console.log 'Sent the event successfully'
145 |       error: (jqXHR, textStatus, err) ->
146 |         console.log 'Error sending the event:', textStatus, ', err', err
147 | 
148 |   selectTemplate: (text) ->
149 |     content = text
150 |     if $("[data-events-out-pretty]").is(":checked")
151 |       content = JSON.stringify(JSON.parse(content), null, 4)
152 |     else
153 |       content = JSON.stringify(JSON.parse(content), null, 0)
154 |     $("[data-events-out-content]").val(content)
155 | 
156 |     # highlight the elements updated
157 |     $('[data-events-out-content]').addClass("updated")
158 |     clearTimeout(@selectTemplateTimeout2)
159 |     @selectTemplateTimeout2 = setTimeout( =>
160 |       $('[data-events-out-content]').removeClass("updated")
161 |     , 300)
162 | 
163 | makeSearchRegexp = (term) ->
164 |   terms = term.split(" ")
165 |   terms = _.filter(terms, (t) -> not _.isEmpty(t.trim()))
166 |   terms = _.map(terms, (t) -> ".*#{t}.*")
167 |   terms = terms.join('|')
168 |   new RegExp(terms, "i");
169 | 


--------------------------------------------------------------------------------
/src/js/templates.coffee:
--------------------------------------------------------------------------------
 1 | # Mustache.js templates that are used as default by ApiMate if they
 2 | # are not set by the user.
 3 | 
 4 | # Content shown in the middle of the page with the list of links generated
 5 | resultsTemplate =
 6 |   "
7 | 21 |
22 |
Results {{title}}:
23 |
24 |
" 25 | 26 | # Content of the dialog when a POST request succeeds 27 | postSuccessTemplate = "
{{response}}
" 28 | 29 | # Content of the dialog when a POST request fails 30 | postErrorTemplate = 31 | "

Server responded with status: {{status}}: {{statusText}}.

32 | {{#response}} 33 |

Content:

34 |
{{response}}
35 | {{/response}} 36 | {{^response}} 37 |

Content: -- no content --

38 | {{/response}} 39 |

If you don't know the reason for this error, check these possibilities:

40 |
    41 |
  • 42 | Your server does not allow cross-domain requests. 43 | By default BigBlueButton and Mconf-Live do not allow cross-domain 44 | requests, so you have to enable it to test this request via POST. Check our 45 | README 46 | for instructions on how to do it. 47 |
  • 48 |
  • 49 | This API method cannot be accessed via POST. 50 |
  • 51 |
  • 52 | Your server is down or malfunctioning. Log into it and check if everything is OK with 53 | bbb-conf --check. 54 |
  • 55 |
      " 56 | 57 | # Body of a POST request to use pre-upload of slides. 58 | preUploadUrl = 59 | " 60 | 61 | 62 | {{#urls}} 63 | 64 | {{/urls}} 65 | 66 | " 67 | -------------------------------------------------------------------------------- /src/views/_menu.pug: -------------------------------------------------------------------------------- 1 | #options.clearfix 2 | #options-actions 3 | button#clear-all.btn.btn-xs.btn-danger(type='submit', data-api-mate-clear='1') 4 | | Clear inputs 5 | button#randomize-ids.btn.btn-xs.btn-warning(type='submit', data-api-mate-randomize='1') 6 | | Randomize ID's 7 | #options-toggles 8 | div.btn-group(data-toggle="buttons-checkbox") 9 | button.btn.btn-info.btn-xs.tooltipped#view-type-input(type="button", data-api-mate-expand='1', title='Show the entire link generated instead of breaking to a single line') 10 | i.glyphicon.glyphicon-check 11 | | Show full links 12 | div.btn-group(data-toggle="buttons-checkbox") 13 | button.btn.btn-info.btn-xs.tooltipped(type="button", data-api-mate-debug='1', title="Print debug messages to your browser's console") 14 | i.glyphicon.glyphicon-check 15 | | Debug messages 16 | div.btn-group(data-toggle="buttons") 17 | .btn.btn-info.btn-xs.active(data-api-mate-sha='sha1') 18 | input(type="radio", name="sha1") 19 | | SHA1 20 | .btn.btn-info.btn-xs(data-api-mate-sha='sha256') 21 | input(type="radio", name="sha256") 22 | | SHA256 23 | .btn.btn-info.btn-xs(data-api-mate-sha='sha384') 24 | input(type="radio", name="sha384") 25 | | SHA384 26 | .btn.btn-info.btn-xs(data-api-mate-sha='sha512') 27 | input(type="radio", name="sha512") 28 | | SHA512 29 | 30 | 31 | #menu-server 32 | .section-title Server URL and shared secret 33 | #menu-server-panel-1.form-horizontal 34 | .form-group 35 | label.control-label.col-sm-3(for='input-custom-server-url') Server 36 | .col-sm-9 37 | input#input-custom-server-url.form-control.input-sm.input-sm(type='text', value='https://test-install.blindsidenetworks.com/bigbluebutton/api', placeholder='Url (e.g http://192.168.0.100:8080/bigbluebutton/api)', data-api-mate-server='url') 38 | .form-group 39 | label.control-label.col-sm-3(for='input-custom-server-salt') Shared secret 40 | .col-sm-9 41 | input#input-custom-server-salt.form-control.input-sm(type='text', value='8cd8ef52e8e101574e400365b55e11a6', placeholder='Salt', data-api-mate-server='salt') 42 | 43 | #menu-inputs 44 | 45 | .form-group 46 | input#input-filter.form-control.input-sm.input-sm.input-clean-title(type='text', value='', placeholder='Type here to filter parameters (e.g. "token")', autofocus='autofocus', data-api-mate-search-input='1') 47 | 48 | //- common fields 49 | #menu-panel-1.form-horizontal 50 | .form-group 51 | label.control-label.col-sm-3(for='input-mid') meetingID 52 | .col-sm-9 53 | input#input-mid.form-control.input-sm(type='text', data-api-mate-param='meetingID', data-api-mate-search='meeting,meetingID') 54 | .form-group 55 | label.control-label.col-sm-3(for='input-rid') recordID 56 | .col-sm-9 57 | input#input-rid.form-control.input-sm(type='text', data-api-mate-param='recordID', data-api-mate-search='recording,recordID') 58 | .form-group 59 | label.control-label.col-sm-3(for='input-name') name 60 | .col-sm-9 61 | input#input-name.form-control.input-sm(type='text', data-api-mate-param='name') 62 | .form-group 63 | label.control-label.col-sm-3(for='input-attendee-password') attendeePW 64 | .col-sm-9 65 | input#input-attendee-password.form-control.input-sm(type='text', value='ap', data-api-mate-param='attendeePW', data-api-mate-search='password,key,participant') 66 | .form-group 67 | label.control-label.col-sm-3(for='input-moderator-password') moderatorPW 68 | .col-sm-9 69 | input#input-moderator-password.form-control.input-sm(type='text', value='mp', data-api-mate-param='moderatorPW', data-api-mate-search='password,key') 70 | .form-group 71 | label.control-label.col-sm-3(for='input-fullname') fullName 72 | .col-sm-9 73 | input#input-fullname.form-control.input-sm(type='text', data-api-mate-param='fullName') 74 | .form-group 75 | label.control-label.col-sm-3(for='input-welcome') welcome 76 | .col-sm-9 77 | textarea#input-welcome.form-control.input-sm(rows='3', data-api-mate-param='welcome', data-api-mate-search='message,msg') 78 | |
      Welcome to %%CONFNAME%%! 79 | .form-group 80 | label.control-label.col-sm-3(for='input-voice-bridge') voiceBridge 81 | .col-sm-9 82 | input#input-voice-bridge.form-control.input-sm(type='text', data-api-mate-param='voiceBridge') 83 | .form-group 84 | label.control-label.col-sm-3(for='input-record') record 85 | .col-sm-9 86 | input#input-record.form-control.input-sm(type='checkbox', data-api-mate-param='record', data-api-mate-search='recording') 87 | .form-group 88 | label.control-label.col-sm-3(for='input-auto-start-recording') autoStartRecording 89 | .col-sm-9 90 | input#input-auto-start-recording.form-control.input-sm(type='checkbox', data-api-mate-param='autoStartRecording', data-api-mate-search='recording') 91 | .form-group 92 | label.control-label.col-sm-3(for='input-allow-start-stop-recording') allowStartStopRecording 93 | .col-sm-9 94 | input#input-allow-start-stop-recording.form-control.input-sm(type='checkbox', data-api-mate-param='allowStartStopRecording', data-api-mate-search='recording', checked) 95 | 96 | .form-group 97 | label.control-label.col-sm-3(for='input-dial-number') dialNumber 98 | .col-sm-9 99 | input#input-dial-number.form-control.input-sm(type='text', data-api-mate-param='dialNumber', data-api-mate-search='voice') 100 | .form-group 101 | label.control-label.col-sm-3(for='input-web-voice') webVoice 102 | .col-sm-9 103 | input#input-web-voice.form-control.input-sm(type='text', data-api-mate-param='webVoice') 104 | .form-group 105 | label.control-label.col-sm-3(for='input-logout-url') logoutURL 106 | .col-sm-9 107 | input#input-logout-url.form-control.input-sm(type='text', data-api-mate-param='logoutURL') 108 | .form-group 109 | label.control-label.col-sm-3(for='input-max-participants') maxParticipants 110 | .col-sm-9 111 | input#input-max-participants.form-control.input-sm(type='text', data-api-mate-param='maxParticipants') 112 | .form-group 113 | label.control-label.col-sm-3(for='input-duration') duration 114 | .col-sm-9 115 | input#input-duration.form-control.input-sm(type='text', data-api-mate-param='duration') 116 | .form-group 117 | label.control-label.col-sm-3(for='input-user-id') userID 118 | .col-sm-9 119 | input#input-user-id.form-control.input-sm(type='text', data-api-mate-param='userID') 120 | .form-group 121 | label.control-label.col-sm-3(for='input-create-time') createTime 122 | .col-sm-9 123 | input#input-create-time.form-control.input-sm(type='text', data-api-mate-param='createTime') 124 | .form-group 125 | label.control-label.col-sm-3(for='input-web-voice-conf') webVoiceConf 126 | .col-sm-9 127 | input#input-web-voice-conf.form-control.input-sm(type='text', data-api-mate-param='webVoiceConf') 128 | .form-group 129 | label.control-label.col-sm-3(for='input-publish') publish 130 | .col-sm-9 131 | input#input-publish.form-control.input-sm(type='checkbox', data-api-mate-param='publish', data-api-mate-search='recording') 132 | .form-group 133 | label.control-label.col-sm-3(for='input-redirect') redirect 134 | .col-sm-9 135 | input#input-redirect.form-control.input-sm(type='checkbox', data-api-mate-param='redirect', checked) 136 | .form-group 137 | label.control-label.col-sm-3(for='input-client-url') clientURL 138 | .col-sm-9 139 | input#input-client-url.form-control.input-sm(type='text', data-api-mate-param='clientURL') 140 | .form-group 141 | label.control-label.col-sm-3(for='input-config-token') configToken 142 | .col-sm-9 143 | input#input-config-token.form-control.input-sm(type='text', data-api-mate-param='configToken') 144 | .form-group 145 | label.control-label.col-sm-3(for='input-avatar-url') avatarURL 146 | .col-sm-9 147 | input#input-avatar-url.form-control.input-sm(type='text', data-api-mate-param='avatarURL') 148 | .form-group 149 | label.control-label.col-sm-3(for='input-moderator-only-message') moderatorOnlyMessage 150 | .col-sm-9 151 | textarea#input-moderator-only-message.form-control.input-sm(rows='3', data-api-mate-param='moderatorOnlyMessage', data-api-mate-search='message,msg') 152 | 153 | .form-group 154 | label.control-label.col-sm-3(for='input-meta') meta_* 155 | .col-sm-9 156 | textarea#input-meta.form-control.input-sm(rows='3', placeholder='myAttribute=value\nanother_attribute=123', data-api-mate-param='meta', data-api-mate-special-param='meta') 157 | .tip one attribute per line 158 | .form-group 159 | label.control-label.col-sm-3(for='input-state') state 160 | .col-sm-9 161 | input#input-state.form-control.input-sm(type='text', data-api-mate-param='state', placeholder='published,unpublished,deleted,processing,processed,any', data-api-mate-search='record') 162 | .tip comma separated list of values 163 | 164 | .form-group 165 | label.control-label.col-sm-3 166 | | Pre-upload for create 167 | .col-sm-9 168 | textarea.form-control.input-sm#input-pre-upload-url(rows='3', placeholder='http://my-server/sample.pdf\nhttps://other-server/other.pdf', data-api-mate-param='pre-upload', data-api-mate-search='preupload,slides') 169 | .tip one or more URLs, one per line, only works via POST 170 | .form-group 171 | label.control-label.col-sm-3 172 | | XML for setConfigXML 173 | .col-sm-9 174 | textarea.form-control.input-sm#input-config-xml(rows='3', placeholder='All the content of your config.xml', data-api-mate-param='configXML') 175 | .tip only works via POST 176 | 177 | #menu-panel-4.form-horizontal 178 | .form-group 179 | label.control-label.col-sm-3(for='input-custom') Custom parameters: 180 | .col-sm-9 181 | textarea#input-custom.form-control.input-sm(rows='3', placeholder='myCustomAttribute=Testing\nanother_attribute=123', data-api-mate-special-param='custom-params', data-api-mate-search='custom,api,params') 182 | .tip attribute=value (one per line, without spaces) 183 | .form-group 184 | label.control-label.col-sm-3(for='input-custom') Custom API calls: 185 | .col-sm-9 186 | textarea#input-custom-calls.form-control.input-sm(rows='3', placeholder='customApiCall\nanother/custom/call', data-api-mate-special-param='custom-calls', data-api-mate-search='custom,api,calls') 187 | .tip One call name per line 188 | -------------------------------------------------------------------------------- /src/views/_menu_events.pug: -------------------------------------------------------------------------------- 1 | #menu-server.disconnected 2 | .section-title 3 | | Server 4 | .connected-label Connected 5 | .disconnected-label Disconnected 6 | #menu-server-panel 7 | .form-group 8 | .input-group 9 | input#input-server-url.form-control.input-sm(type='text', value='http://localhost:3000', placeholder='Url (e.g http://192.168.0.100:3000)', data-events-server='url') 10 | .input-group-btn 11 | #input-server-connect.btn.btn-sm.btn-primary(data-events-server-connect="") Connect 12 | 13 | #menu-configs 14 | .section-title Configurations 15 | #menu-vars-panel 16 | .form-group 17 | label(for='input-name') 18 | code Exclude patterns 19 | - content = '"name":"BbbPubSubPingMessage"\n"name":"BbbPubSubPongMessage"\n"name":"bbb_apps_is_alive_message"' 20 | textarea#input-config-meeting-id.form-control.input-sm(rows='2', placeholder='assign_presenter_request_message', data-events-config='exclude')= content 21 | //- .form-group 22 | //- input#input-var-meeting-id.form-control.input-sm(type='text', placeholder='meeting ID', data-events-var='meeting_id') 23 | 24 | #menu-templates 25 | .form-group 26 | input#input-filter.form-control.input-sm.input-clean-title(type='text', value='', placeholder='Templates (type to search...)', autofocus='autofocus', data-events-search-input='') 27 | each event in events 28 | .events-template(data-events-template="") 29 | .event-name= event["header"]["name"] 30 | code.event-json(data-events-template-content="") 31 | = JSON.stringify(event) 32 | -------------------------------------------------------------------------------- /src/views/_tab_config_xml.pug: -------------------------------------------------------------------------------- 1 | dl.dl-horizontal 2 | dt 3 | span.label.label-danger= '1' 4 | dd#step-1.step 5 | .step-headline 6 | = 'Create a meeting' 7 | ul.step-options 8 | li 9 | a.btn.btn-xs.btn-primary(href='#') 10 | i.glyphicon.glyphicon-new-window 11 | = ' Click here' 12 | = ' or' 13 | li 14 | a(href='#')= 'http://my-server.com.br/bigbluebutton/api/create?lalalaljaaoisjdoiajsd' 15 | dt 16 | span.label.label-danger= '2' 17 | dd#step-2.step 18 | .step-headline 19 | = 'Get the default config.xml ' 20 | ul.step-options 21 | li 22 | a.btn.btn-xs.btn-primary(href='#') 23 | i.glyphicon.glyphicon-new-window 24 | = ' Click here' 25 | = ' or' 26 | li 27 | a(href='#')= 'http://my-server.com.br/bigbluebutton/api/getDefaultConfigXML?oajsdoiajsodj' 28 | .step-controls 29 | textarea#config-xml.form-control(cols='5', placeholder='Or simply put your original config.xml here') 30 | dt 31 | span.label.label-danger= '3' 32 | dd#step-3.step 33 | .step-headline 34 | = 'Edit with regular expressions' 35 | .step-controls 36 | .form-group 37 | //- /