├── .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('
').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i