diff --git a/Gemfile b/Gemfile index 3f9c8256..6fd584d2 100644 --- a/Gemfile +++ b/Gemfile @@ -82,3 +82,9 @@ gem 'slack-notifier' # For landing background image uploading. gem 'paperclip', '~> 4.2' + +# For uploading recordings to Youtube. +gem 'yt', '~> 0.28.0' + +# Simple HTTP client. +gem 'faraday' diff --git a/Gemfile.lock b/Gemfile.lock index 5544207c..4178690f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -229,6 +229,8 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) xml-simple (1.1.5) + yt (0.28.5) + activesupport PLATFORMS ruby @@ -240,6 +242,7 @@ DEPENDENCIES byebug coffee-rails (~> 4.2) dotenv-rails + faraday font-awesome-sass http_accept_language jbuilder (~> 2.5) @@ -267,6 +270,7 @@ DEPENDENCIES tzinfo-data uglifier (>= 1.3.0) web-console + yt (~> 0.28.0) RUBY VERSION ruby 2.3.4p301 diff --git a/app/assets/javascripts/recordings.coffee b/app/assets/javascripts/recordings.coffee index 6d4f1549..ff9fa804 100644 --- a/app/assets/javascripts/recordings.coffee +++ b/app/assets/javascripts/recordings.coffee @@ -37,6 +37,18 @@ class @Recordings COLUMN[c] = i++ constructor: -> + recordingsObject = this + canUpload = {} + + # Determine which recordings can be uploaded to Youtube. + $.ajax({ + method: 'GET', + async: false, + url: recordingsObject.getRecordingsURL() + '/can_upload' + }).success((res_data) -> + canUpload = res_data + ) + # configure the datatable for recordings this.table = $('#recordings').dataTable({ data: [], @@ -131,6 +143,14 @@ class @Recordings trigger = recordingActions.find('.recording-update-trigger') trigger.removeClass(classes.join(' ')) trigger.addClass(cls) + + upload_btn = recordingActions.find('.cloud-upload') + + if canUpload[row.id] + upload_btn.attr('data-popover-body', '.mail_youtube_popover') + else + upload_btn.attr('data-popover-body', '.mail_popover') + return recordingActions.html() return data } @@ -151,6 +171,18 @@ class @Recordings options.title = I18n.play_recording $('#recordings').tooltip(options) + options.selector = '.youtube-tooltip' + options.title = I18n.upload_youtube + $('#recordings').tooltip(options) + + options.selector = '.upload-tooltip' + options.title = I18n.share + $('#recordings').tooltip(options) + + options.selector = '.mail-tooltip' + options.title = I18n.mail_recording + $('#recordings').tooltip(options) + $(document).one "turbolinks:before-cache", => @getTable().api().clear().draw().destroy() @@ -273,12 +305,52 @@ class @Recordings showAlert(I18n.recording_deleted, 4000); ) + @getTable().on 'click', '.upload-button', (event) -> + btn = $(this) + row = table_api.row($(this).closest('tr')).data() + url = recordingsObject.getRecordingsURL() + id = row.id + + title = btn.closest('form').find('#video-title').val() + privacy_status = btn.closest('form').find('input[name=privacy_status]:checked').val() + + if title == '' + title = row.name + + $.ajax({ + method: 'POST', + url: url+'/'+id + data: {video_title: title, privacy_status: privacy_status} + }) + + @getTable().on 'click', '.mail-recording', (event) -> + btn = $(this) + row = table_api.row($(this).closest('tr')).data() + url = recordingsObject.getRecordingsURL() + id = row.id + + # Take the username from the header. + username = $('#title-header').text().replace('Welcome ', '').trim() + + recording_url = row.playbacks[0].url + webcams_url = getHostName(recording_url) + '/presentation/' + id + '/video/webcams.webm' + subject = username + I18n.recording_mail_subject + body = I18n.recording_mail_body + "\n\n" + recording_url + + mailto = "mailto:?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body); + window.open(mailto); + @getTable().on 'draw.dt', (event) -> $('time[data-time-ago]').timeago(); getTable: -> @table + getHostName = (url) -> + parser = document.createElement('a'); + parser.href = url; + parser.origin; + getRecordingsURL: -> if $(".page-wrapper.rooms").data('main-room') base_url = Meeting.buildRootURL()+'/'+$('body').data('resource')+'/'+Meeting.getInstance().getAdminId() diff --git a/app/assets/stylesheets/main/landing.scss b/app/assets/stylesheets/main/landing.scss index b68180d3..cf00e43a 100644 --- a/app/assets/stylesheets/main/landing.scss +++ b/app/assets/stylesheets/main/landing.scss @@ -89,3 +89,15 @@ color: green; } } + +.youtube-red { + color: red; +} + +.cloud-blue { + color: cornflowerblue; +} + +.top-buffer { + margin-top: 8px; +} diff --git a/app/controllers/bbb_controller.rb b/app/controllers/bbb_controller.rb index 6b47975b..69421fcf 100644 --- a/app/controllers/bbb_controller.rb +++ b/app/controllers/bbb_controller.rb @@ -170,6 +170,41 @@ class BbbController < ApplicationController render_bbb_response bbb_res end + # POST /rooms/:room_id/recordings/:record_id + # POST /rooms/:room_id/:id/recordings/:record_id + def youtube_publish + # If we can't get the client, then they don't have a Youtube account. + begin + client = Yt::Account.new(access_token: current_user.token) + video = client.upload_video(get_webcams_url(params[:record_id]), + title: params[:video_title], + description: t('youtube_description'), + privacy_status: params[:privacy_status]) + rescue + # In this case, they don't have a youtube channel connected to their account, so prompt to create one. + redirect_to 'https://m.youtube.com/create_channel' + end + end + + # GET /rooms/:room_id/recordings/can_upload + def can_upload + upload_data = {} + bbb_get_recordings[:recordings].each{ |recording_data| + next if recording_data[:recordID] == "" + # The recording is uploadable if it contains webcam data and they are logged in thorugh Google. + uploadable = Faraday.head(get_webcams_url(recording_data[:recordID])).status == 200 && + Rails.application.config.omniauth_google && + current_user.provider == 'google' + upload_data[recording_data[:recordID]] = uploadable + } + render json: upload_data + end + + def get_webcams_url(recording_id) + uri = URI.parse(ENV['BIGBLUEBUTTON_ENDPOINT']) + uri.scheme + '://' + uri.host + '/presentation/' + recording_id + '/video/webcams.webm' + end + private def load_room! diff --git a/app/lib/bbb_api.rb b/app/lib/bbb_api.rb index ef1a0312..5b37d072 100644 --- a/app/lib/bbb_api.rb +++ b/app/lib/bbb_api.rb @@ -141,6 +141,7 @@ module BbbApi end res[:recordings].each do |recording| + next if recording.key?(:error) pref_preview = {} recording[:length] = recording[:playback][:format].is_a?(Hash) ? recording[:playback][:format][:length] : recording[:playback][:format].first[:length] # create a playbacks attribute on recording for playback formats diff --git a/app/models/user.rb b/app/models/user.rb index ec106233..471205e8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,6 +26,7 @@ class User < ApplicationRecord user.username = self.send("#{auth_hash['provider']}_username", auth_hash) rescue nil user.email = self.send("#{auth_hash['provider']}_email", auth_hash) rescue nil user.name = auth_hash['info']['name'] + user.token = auth_hash['credentials']['token'] user.save! user end diff --git a/app/views/landing/_rooms_center_panel.html.erb b/app/views/landing/_rooms_center_panel.html.erb index 39b6618a..ab475039 100644 --- a/app/views/landing/_rooms_center_panel.html.erb +++ b/app/views/landing/_rooms_center_panel.html.erb @@ -15,7 +15,7 @@ <% content_for :title do %>
-

+

<% if admin? && !@meeting_running %> <%= t('admin_room_title', user: @user.name) %> <% else %> diff --git a/app/views/landing/rooms.html.erb b/app/views/landing/rooms.html.erb index a7f2aeac..f3881873 100644 --- a/app/views/landing/rooms.html.erb +++ b/app/views/landing/rooms.html.erb @@ -55,6 +55,40 @@ <%= t('no') %>

+
+
+
+
+ +
+
+ +
+
+
+ + + + +
+ +
+
+ + +
+
+ +
+ diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 0a6efe2f..b5b3be0a 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -7,5 +7,5 @@ Rails.application.config.omniauth_twitter = ENV['TWITTER_ID'].present? Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ENV['TWITTER_ID'], ENV['TWITTER_SECRET'] provider :google_oauth2, ENV['GOOGLE_OAUTH2_ID'], ENV['GOOGLE_OAUTH2_SECRET'], - scope: ['profile', 'email'], access_type: 'online', name: 'google' + scope: ['profile', 'email', 'youtube', 'youtube.upload'], access_type: 'online', name: 'google' end diff --git a/config/initializers/youtube.rb b/config/initializers/youtube.rb new file mode 100644 index 00000000..8d357882 --- /dev/null +++ b/config/initializers/youtube.rb @@ -0,0 +1,21 @@ +# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +# +# Copyright (c) 2016 BigBlueButton Inc. and by respective authors (see below). +# +# This program is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 3.0 of the License, or (at your option) any later +# version. +# +# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with BigBlueButton; if not, see . + +Yt.configure do |config| + config.log_level = :debug + config.client_id = ENV['GOOGLE_OAUTH2_ID'] + config.client_secret = ENV['GOOGLE_OAUTH2_SECRET'] +end diff --git a/config/locales/en-us.yml b/config/locales/en-us.yml index 4dc9358a..3ec14721 100644 --- a/config/locales/en-us.yml +++ b/config/locales/en-us.yml @@ -46,8 +46,9 @@ en-US: client: are_you_sure: Are you sure? change_visibility: Change visibility - delete_recording: Delete recording + delete_recording: Delete enter_name: Enter a name to join + mail_recording: Mail meeting_ended: Meeting was ended meeting_started: Meeting was started no_recordings: No Recordings @@ -58,11 +59,15 @@ en-US: recording_created: A recording was created recording_deleted: Recording was deleted recording_published: Recording was published + recording_mail_subject: " has invited you to view a recording." + recording_mail_body: "To view the recording, follow the link below:" recording_unlisted: Recording was unlisted recording_unpublished: Recording was unpublished + share: Share unpublish_recording: Hide recording unlisted: Unlisted unpublished: No one + upload_youtube: Youtube user_waiting_body: "%{user} is waiting to join %{meeting}!" user_waiting_title: A user is waiting copied: Copied @@ -158,8 +163,14 @@ en-US: url_copy_explanation: Copy this URL to invite others to the meeting user_person_room: "%{name} personal room" video: Video + video_title: Video title visibility: Visibility wait_for_mod_msg: Looks like you're the first one here... wait_for_mod_explanation: You will automatically join when the meeting starts watch: Watch 'yes': 'Yes' + youtube_description: This recording was recorded with BigBlueButton. For more information check out https://bigbluebutton.org/ + youtube_privacy_options: + public: Public + private: Private + unlisted: Unlisted diff --git a/config/routes.rb b/config/routes.rb index 833311c3..6465fe9a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,8 @@ Rails.application.routes.draw do scope '/:room_id', :constraints => {:room_id => disallow_slash} do # recording routes for updating, deleting and viewing recordings get '/(:id)/recordings', to: 'bbb#recordings', defaults: {id: nil, format: 'json'}, :constraints => {:id => disallow_slash} + get '/(:id)/recordings/can_upload', to: 'bbb#can_upload', defaults: {id: nil, format: 'json'}, :constraints => {:id => disallow_slash} + post '/(:id)/recordings/:record_id', to: 'bbb#youtube_publish', defaults: {id: nil, format: 'json'}, :constraints => {:id => disallow_slash} patch '/(:id)/recordings/:record_id', to: 'bbb#update_recordings', defaults: {id: nil, format: 'json'}, :constraints => {:id => disallow_slash} delete '/(:id)/recordings/:record_id', to: 'bbb#delete_recordings', defaults: {id: nil, format: 'json'}, :constraints => {:id => disallow_slash} diff --git a/db/migrate/20170518190442_add_token_to_users.rb b/db/migrate/20170518190442_add_token_to_users.rb new file mode 100644 index 00000000..5a1313e4 --- /dev/null +++ b/db/migrate/20170518190442_add_token_to_users.rb @@ -0,0 +1,5 @@ +class AddTokenToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 51b91b7d..4aff0bb8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170510175654) do +ActiveRecord::Schema.define(version: 20170518190442) do create_table "users", force: :cascade do |t| t.string "provider", null: false @@ -25,6 +25,7 @@ ActiveRecord::Schema.define(version: 20170510175654) do t.string "background_content_type" t.integer "background_file_size" t.datetime "background_updated_at" + t.string "token" t.index ["email"], name: "index_users_on_email", unique: true t.index ["encrypted_id"], name: "index_users_on_encrypted_id", unique: true t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true