Class: BBBEvents::Recording

Inherits:
Object
  • Object
show all
Includes:
Events
Defined in:
lib/bbbevents/recording.rb

Constant Summary

Constants included from Events

Events::EMOJI_WHITELIST, Events::POLL_PUBLISHED_STATUS, Events::RAISEHAND, Events::RECORDABLE_EVENTS

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(events_xml) ⇒ Recording

Returns a new instance of Recording.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/bbbevents/recording.rb', line 16

def initialize(events_xml)
  filename = File.basename(events_xml)
  raise "#{filename} is not a file or does not exist." unless File.file?(events_xml)

  # The Hash.from_xml automatically converts keys with dashes '-' to snake_case
  # (i.e canvas-recording-ready-url becomes canvas_recording_ready_url)
  # see https://www.rubydoc.info/github/datamapper/extlib/Hash.from_xml
  raw_recording_data = Hash.from_xml(File.read(events_xml))

  raise "#{filename} is not a valid xml file (unable to parse)." if raw_recording_data.nil?
  raise "#{filename} is missing recording key." unless raw_recording_data.key?("recording")

  recording_data = raw_recording_data["recording"]
  events = recording_data["event"]
  events = [] if events.nil?
  events = [events] unless events.is_a?(Array)

  @metadata   = recording_data["metadata"]
  @meeting_id = recording_data["metadata"]["meetingId"]

  internal_meeting_id = recording_data["meeting"]["id"]

  @timestamp  = extract_timestamp(internal_meeting_id)
  @start = Time.at(@timestamp / 1000)

  if events.length > 0
    @first_event = events.first["timestamp"].to_i
    @last_event  = events.last["timestamp"].to_i
    @finish = Time.at(timestamp_conversion(@last_event))
  else
    @finish = @start
  end
  @duration = (@finish - @start).to_i

  @attendees = {}
  @polls     = {}
  @files     = []

  # Map to look up external user id (for @data[:attendees]) from the
  # internal user id in most recording events
  @externalUserId = {}

  process_events(events)

  @attendees.values.each do |att|
    att.leaves << @finish if att.joins.length > att.leaves.length
    att.duration = total_duration(@finish, att)
  end
end

Instance Attribute Details

#durationObject

Returns the value of attribute duration.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def duration
  @duration
end

#filesObject

Returns the value of attribute files.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def files
  @files
end

#finishObject

Returns the value of attribute finish.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def finish
  @finish
end

#meeting_idObject

Returns the value of attribute meeting_id.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def meeting_id
  @meeting_id
end

#metadataObject

Returns the value of attribute metadata.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def 
  @metadata
end

#startObject

Returns the value of attribute start.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def start
  @start
end

#timestampObject

Returns the value of attribute timestamp.



14
15
16
# File 'lib/bbbevents/recording.rb', line 14

def timestamp
  @timestamp
end

Instance Method Details

#as_jsonObject



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/bbbevents/recording.rb', line 127

def as_json
  {
    metadata: ,
    meeting_id: @meeting_id,
    duration: @duration,
    start: BBBEvents.format_datetime(@start),
    finish: BBBEvents.format_datetime(@finish),
    attendees: attendees.map(&:as_json),
    files: @files,
    polls: polls.map(&:as_json)
  }
end

#attendeesObject

Take only the values since we no longer need to index.



67
68
69
# File 'lib/bbbevents/recording.rb', line 67

def attendees
  @attendees.values
end

#build_join_left_tuples(joins_lefts_arr_sorted) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/bbbevents/recording.rb', line 248

def build_join_left_tuples(joins_lefts_arr_sorted)
  jl_tuples = []
  jl_tuple = {:join => nil, :left => nil}
  loop_state = :find_join

  events_length = joins_lefts_arr_sorted.length - 1
  for i in 0..events_length

    cur_event = joins_lefts_arr_sorted[i]

    if loop_state == :find_join and cur_event[:event] == :join
      jl_tuple[:join] = cur_event
      loop_state = :find_left
    end

    next_event = nil
    if i < events_length
      next_event = joins_lefts_arr_sorted[i + 1]
    end

    if loop_state == :find_left
      if next_event != nil and next_event[:event] == :left
        # skip the current event to get to the next event
      elsif (cur_event[:event] == :left and next_event != nil and next_event[:event] == :join) or (i == events_length)
        jl_tuple[:left] = cur_event
        jl_tuples.append(jl_tuple)
        jl_tuple = {:join => nil, :left => nil}
        loop_state = :find_join
      end
    end
  end

  jl_tuples
end

#build_join_lefts_array(last_event_timestamp, user_session) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/bbbevents/recording.rb', line 283

def build_join_lefts_array(last_event_timestamp, user_session)
  joins_leaves_arr = []
  lefts_count = 0
  joins_count = 0

  user_session.each do | userid, joins_lefts |
    lefts_count += joins_lefts[:lefts].length
    joins_count += joins_lefts[:joins].length
    joins_lefts[:joins].each { |j| joins_leaves_arr.append(j)}
    joins_lefts[:lefts].each { |j| joins_leaves_arr.append(j)}
  end

  if joins_count > lefts_count
    last_event = joins_leaves_arr[-1]
    joins_leaves_arr.append({:timestamp => last_event_timestamp, :userid => "    system    ", :ext_userid=> last_event[:ext_userid], :event => :left})
  end

  joins_leaves_arr
end

#build_tuples_of_kept_events(kept_events) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/bbbevents/recording.rb', line 332

def build_tuples_of_kept_events(kept_events)
  odd_events = []
  even_events = []
  for i in 0..kept_events.length - 1
    odd_even = i + 1
    if odd_even.even?
      even_events.append(kept_events[i])
    else
      odd_events.append(kept_events[i])
    end
  end

  tuples = []
  for i in 0..odd_events.length - 1
    tuple = {:start => odd_events[i], :end => even_events[i]}
    tuples.append(tuple)
  end

  tuples
end

#calculate_user_duration(join_events, left_events) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/bbbevents/recording.rb', line 144

def calculate_user_duration(join_events, left_events)
  joins_leaves_arr = []
  join_events.each { |j| joins_leaves_arr.append({:time => j.to_i, :datetime => j, :event => :join})}
  left_events.each { |j| joins_leaves_arr.append({:time => j.to_i, :datetime => j, :event => :left})}

  joins_leaves_arr_sorted = joins_leaves_arr.sort_by { |event| event[:time] }

  partial_duration = 0
  prev_event = nil

  joins_leaves_arr_sorted.each do |cur_event|
    duration = 0
    if prev_event != nil and cur_event[:event] == :join and prev_event[:event] == :left
      # user left and rejoining, don't update duration
      prev_event = cur_event
    elsif prev_event != nil
      duration = cur_event[:time] - prev_event[:time]
      partial_duration += duration
      prev_event = cur_event
    else
      prev_event = cur_event
    end
  end

  return partial_duration
end

#calculate_user_duration_based_on_userid(last_event_ts, sessions) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/bbbevents/recording.rb', line 171

def calculate_user_duration_based_on_userid(last_event_ts, sessions)
  # combine join and left events into an array
  joins_lefts_arr = build_join_lefts_array(last_event_ts, sessions)

  # sort the events
  joins_lefts_arr_sorted = joins_lefts_arr.sort_by { |event| event[:timestamp] }

  combined_tuples = combine_tuples_by_userid(sessions)
  combined_tuples_sorted = fill_missing_left_events(combined_tuples)

  prepare_joins_lefts_for_overlap_checks(joins_lefts_arr_sorted)
  mark_overlapping_events(combined_tuples_sorted, joins_lefts_arr_sorted)
  removed_overlap_events = remove_overlapping_events(joins_lefts_arr_sorted)

  duration_tuples = build_join_left_tuples(removed_overlap_events)

  partial_duration = 0
  duration_tuples.each do |tuple|
    duration = tuple[:left][:timestamp].to_i - tuple[:join][:timestamp].to_i
    partial_duration += duration
  end

  partial_duration
end

#combine_tuples_by_userid(user_sessions) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/bbbevents/recording.rb', line 210

def combine_tuples_by_userid(user_sessions)
  combined_tuples = []

  user_sessions.each do | userid, joins_lefts |
    joins_lefts_arr = []
    joins_lefts[:joins].each { |j| joins_lefts_arr.append(j)}
    joins_lefts[:lefts].each { |j| joins_lefts_arr.append(j)}

    tuples = tuples_by_userid(joins_lefts[:joins], joins_lefts[:lefts])

    tuples.each do |tuple|
      combined_tuples.append(tuple)
    end
  end

  combined_tuples
end

#create_csv(filepath) ⇒ Object

Export recording data to a CSV file.



97
98
99
100
101
102
103
104
# File 'lib/bbbevents/recording.rb', line 97

def create_csv(filepath)
  CSV.open(filepath, "wb") do |csv|
    csv << CSV_HEADER.map(&:capitalize) + (1..polls.length).map { |i| "Poll #{i}" }
    @attendees.each do |id, att|
      csv << att.csv_row + polls.map { |poll| poll.votes[id] || NO_VOTE_SYMBOL }
    end
  end
end

#fill_missing_left_events(combined_tuples) ⇒ Object



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/bbbevents/recording.rb', line 228

def fill_missing_left_events(combined_tuples)
  joins_lefts_arr_sorted = combined_tuples.sort_by { |event| event[:join][:timestamp]}

  joins_lefts_arr_sorted_length = joins_lefts_arr_sorted.length - 1
  for i in 0..joins_lefts_arr_sorted_length
    cur_event = joins_lefts_arr_sorted[i]
    if cur_event[:left].nil?
      unless joins_lefts_arr_sorted_length == i
        # Take the next event as the left event for this current event
        next_event = joins_lefts_arr_sorted[i + 1]
        left_event = {:timestamp => next_event[:timestamp], :userid => cur_event[:userid], :event => :left}

        cur_event[:left] = left_event
      end
    end
  end

  joins_lefts_arr_sorted
end

#mark_overlapping_events(combined_tuples_sorted, joins_leaves_arr_sorted) ⇒ Object



309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/bbbevents/recording.rb', line 309

def mark_overlapping_events(combined_tuples_sorted, joins_leaves_arr_sorted)
  combined_tuples_sorted.each do |ce|
    joins_leaves_arr_sorted.each do |jl|
      event_ts = jl[:timestamp].to_i
      ce_join = ce[:join][:timestamp].to_i

      if event_ts > ce_join and not ce[:left].nil? and event_ts < ce[:left][:timestamp].to_i
        jl[:remove] = true
      end
    end
  end
end

#moderatorsObject

Retrieve a list of all the moderators.



72
73
74
# File 'lib/bbbevents/recording.rb', line 72

def moderators
  attendees.select(&:moderator?)
end

#pollsObject

Take only the values since we no longer need to index.



82
83
84
# File 'lib/bbbevents/recording.rb', line 82

def polls
  @polls.values
end

#prepare_joins_lefts_for_overlap_checks(joins_leaves_arr_sorted) ⇒ Object



303
304
305
306
307
# File 'lib/bbbevents/recording.rb', line 303

def prepare_joins_lefts_for_overlap_checks(joins_leaves_arr_sorted)
  joins_leaves_arr_sorted.each do |event|
    event[:remove] = false
  end
end

#published_pollsObject

Retrieve a list of published polls.



87
88
89
# File 'lib/bbbevents/recording.rb', line 87

def published_polls
  polls.select(&:published?)
end

#remove_overlapping_events(joins_leaves_arr_sorted) ⇒ Object



322
323
324
325
326
327
328
329
330
# File 'lib/bbbevents/recording.rb', line 322

def remove_overlapping_events(joins_leaves_arr_sorted)
  keep_events = []
  joins_leaves_arr_sorted.each do |ev|
    if not ev[:remove]
      keep_events.append(ev)
    end
  end
  keep_events
end

#to_hObject



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/bbbevents/recording.rb', line 114

def to_h
  {
    metadata: ,
    meeting_id: @meeting_id,
    duration: @duration,
    start: @start,
    finish: @finish,
    attendees: attendees.map(&:to_h),
    files: @files,
    polls: polls.map(&:to_h)
  }
end

#to_jsonObject



140
141
142
# File 'lib/bbbevents/recording.rb', line 140

def to_json
  JSON.generate(as_json)
end

#transform_metadataObject

Transform any CamelCase keys to snake_case



107
108
109
110
111
112
# File 'lib/bbbevents/recording.rb', line 107

def 
  @metadata.deep_transform_keys do |key|
    k = key.to_s.underscore rescue key
    k.to_sym rescue key
  end
end

#tuples_by_userid(joins_arr, lefts_arr) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/bbbevents/recording.rb', line 196

def tuples_by_userid(joins_arr, lefts_arr)
  joins_length = joins_arr.length - 1
  tuples = []
  for i in 0..joins_length
    tuple = {:join => joins_arr[i], :left => nil}

    if i <= lefts_arr.length - 1
      tuple[:left] = lefts_arr[i]
    end
    tuples.append(tuple)
  end
  tuples
end

#unpublished_pollsObject

Retrieve a list of unpublished polls.



92
93
94
# File 'lib/bbbevents/recording.rb', line 92

def unpublished_polls
  polls.reject(&:published?)
end

#viewersObject

Retrieve a list of all the viewers.



77
78
79
# File 'lib/bbbevents/recording.rb', line 77

def viewers
  attendees.reject(&:moderator?)
end