bbbevents 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9f0ea583b187441128084badd6a7671aca2289135b15e11dbe2ca03473f8a89
4
- data.tar.gz: 79f8ade1f3d3b035c66d63578118e97c688c62015cd37c782bb50b6c5880aade
3
+ metadata.gz: ea120800577b78c239ae7c6b0e1007162493fdad13904dcb88e7f9fe8b2762c2
4
+ data.tar.gz: 61671fcd6e70a3b341aee4e07d1b1961f06aeb3149d9f55785f422cb80af5b3b
5
5
  SHA512:
6
- metadata.gz: bb7bdf62e41f2d56101ef656358b33a6a98fe093d6f6abd99e842766906b93c71db103bfdbd7689a1ad4a3414ad1efdddb5fba32e45995bce556d471c681e855
7
- data.tar.gz: 84e933f01582767c3e9f1626bb77671fb21ca4ad8d63b159065616aed78b7d10b744d32875c338f4d493700e9db40ce50bbbdc21c10c9bce1716be0bb203f18d
6
+ metadata.gz: e8d31d953e2c5b43f92467200f71ca4dbf8c68ae5e7aacfb00ee1320daa527bc9031add9ae732931ab10005437a20fa5d4d28a37339b03a05fcc3946fbfdfc1a
7
+ data.tar.gz: df822b1085cbe78a1e293e8081a53d7836adbc606ede9c5d62dfc2e407b997afd099505ce5bc2d763a082f628fb450fcb9e985b23b99d207f1ebf178babedc27
@@ -0,0 +1,20 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ test:
6
+ name: Rake Test
7
+ strategy:
8
+ matrix:
9
+ ruby: ['2.7', '3.0']
10
+ runs-on: ubuntu-20.04
11
+
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+
20
+ - run: bundler exec rake spec
@@ -0,0 +1,32 @@
1
+ name: bbbevents gem release
2
+ on:
3
+ push:
4
+ tags:
5
+ - '*'
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-20.04
10
+ if: startsWith(github.ref, 'refs/tags/')
11
+ steps:
12
+ - uses: actions/checkout@v2
13
+
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: 2.7.0
18
+
19
+ - name: Install dependencies
20
+ run: bundle install
21
+
22
+ - name: Build gem
23
+ run: gem build *.gemspec
24
+
25
+ - name: Push gem
26
+ env:
27
+ GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
28
+ run: |
29
+ pwd
30
+ ls -la
31
+ filename=$(ls *.gem | head -n 1)
32
+ gem push "$filename"
data/.gitignore CHANGED
@@ -6,6 +6,8 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /vendor/
10
+ Gemfile.lock
9
11
 
10
12
  # rspec failure tracking
11
13
  .rspec_status
@@ -14,4 +16,9 @@
14
16
  /spec/testing.csv
15
17
  testdata/*.xml
16
18
 
17
- /bbbevents-*
19
+ /bbbevents-*
20
+ /pec
21
+
22
+ # result of the example script
23
+ /data.csv
24
+ /data.json
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7.0
data/README.md CHANGED
@@ -15,7 +15,7 @@ bundle install --path vendor/bundle
15
15
  Copy an `events.xml` file into `testdata/` dir.
16
16
 
17
17
  ```
18
- bundle exec ./example.rb spec/fixtures/files/sample-events.xml
18
+ bundle exec ruby example.rb testdata/events.xml
19
19
  ```
20
20
 
21
21
  ## Installation
@@ -102,10 +102,10 @@ poll.published?
102
102
  # Determine when the poll started.
103
103
  poll.start
104
104
 
105
- # Returns an Array contain possible options.
105
+ # Returns an Array containing possible options.
106
106
  poll.options
107
107
 
108
- # Returns a Hash maping user_id's to their poll votes.
108
+ # Returns a Hash mapping user_id's to their poll votes.
109
109
  poll.votes
110
110
  ```
111
111
 
data/bbbevents.gemspec CHANGED
@@ -10,9 +10,9 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["ffdixon@blindsidenetworks.com"]
11
11
 
12
12
  spec.summary = %q{Easily parse data from a BigBlueButton recording's events.xml.}
13
- spec.description = %q{Easily parse data from a BigBlueButton recording's events.xml.}
13
+ spec.description = %q{Ruby gem for easily parse data from a BigBlueButton recording's events.xml.}
14
14
  spec.homepage = "https://www.blindsidenetworks.com"
15
- spec.license = "LGPL 3.0"
15
+ spec.license = "LGPL-3.0"
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
18
  f.match(%r{^(test|spec|features)/})
@@ -21,10 +21,12 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.add_development_dependency "bundler", ">= 1.15"
25
- spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "bundler", "~> 2.0"
25
+ spec.add_development_dependency "rake", "~> 13.0"
26
26
  spec.add_development_dependency "rspec", "~> 3.4"
27
27
 
28
28
  # Gem dependecies.
29
- spec.add_dependency "activesupport"
29
+ spec.add_dependency 'activesupport', '>= 5.0.0.1', '< 8'
30
+ spec.add_dependency 'rexml' # Required for activesupport from_xml
31
+
30
32
  end
data/example.rb CHANGED
@@ -33,12 +33,10 @@ recording.files
33
33
 
34
34
  # Generate a CSV file with the data.
35
35
  recording.create_csv("data.csv")
36
-
36
+
37
37
  puts "Writing the JSON data"
38
38
 
39
39
  # Write JSON data to file.
40
40
  File.open("data.json", 'w') do |f|
41
41
  f.write(recording.to_json)
42
42
  end
43
-
44
-
@@ -1,13 +1,13 @@
1
1
  module BBBEvents
2
2
  class Attendee
3
- attr_accessor :id, :name, :moderator, :joins, :leaves, :duration, :recent_talking_time, :engagement
3
+ attr_accessor :id, :ext_user_id, :name, :moderator, :joins, :leaves, :duration, :recent_talking_time, :engagement, :sessions
4
4
 
5
5
  MODERATOR_ROLE = "MODERATOR"
6
6
  VIEWER_ROLE = "VIEWER"
7
7
 
8
8
  def initialize(join_event)
9
9
  @id = join_event["userId"]
10
- @extUserId = join_event["externalUserId"]
10
+ @ext_user_id = join_event["externalUserId"]
11
11
  @name = join_event["name"]
12
12
  @moderator = (join_event["role"] == MODERATOR_ROLE)
13
13
 
@@ -25,6 +25,10 @@ module BBBEvents
25
25
  poll_votes: 0,
26
26
  talk_time: 0,
27
27
  }
28
+
29
+ # A hash of join and lefts arrays for each internal user id
30
+ # { "w_5lmcgjboagjc" => { :joins => [], :lefts => []}}
31
+ @sessions = Hash.new
28
32
  end
29
33
 
30
34
  def moderator?
@@ -59,15 +63,41 @@ module BBBEvents
59
63
  end
60
64
 
61
65
  def to_h
62
- hash = {}
63
- instance_variables.each { |var| hash[var[1..-1]] = instance_variable_get(var) }
64
- hash
66
+ {
67
+ id: @id,
68
+ ext_user_id: @ext_user_id,
69
+ name: @name,
70
+ moderator: @moderator,
71
+ joins: @joins,
72
+ leaves: @leaves,
73
+ duration: @duration,
74
+ recent_talking_time: @recent_talking_time > 0 ? Time.at(@recent_talking_time) : '',
75
+ engagement: @engagement,
76
+ sessions: @sessions,
77
+ }
78
+ end
79
+
80
+ def as_json
81
+ {
82
+ id: @id,
83
+ ext_user_id: @ext_user_id,
84
+ name: @name,
85
+ moderator: @moderator,
86
+ joins: @joins.map { |join| BBBEvents.format_datetime(join) },
87
+ leaves: @leaves.map { |leave| BBBEvents.format_datetime(leave) },
88
+ duration: @duration,
89
+ recent_talking_time: @recent_talking_time > 0 ? BBBEvents.format_datetime(Time.at(@recent_talking_time)) : '',
90
+ engagement: @engagement,
91
+ sessions: @sessions.map { |key, session| {
92
+ joins: session[:joins].map { |join| join.merge({ timestamp: BBBEvents.format_datetime(join[:timestamp])}) },
93
+ lefts: session[:lefts].map { |leave| leave.merge({ timestamp: BBBEvents.format_datetime(leave[:timestamp])}) }
94
+ }
95
+ }
96
+ }
65
97
  end
66
98
 
67
99
  def to_json
68
- hash = {}
69
- instance_variables.each { |var| hash[var[1..-1]] = instance_variable_get(var) }
70
- hash.to_json
100
+ JSON.generate(as_json)
71
101
  end
72
102
 
73
103
  private
@@ -5,6 +5,10 @@ module BBBEvents
5
5
  DATE_FORMAT = "%m/%d/%Y %H:%M:%S"
6
6
  UNKNOWN_DATE = "??/??/????"
7
7
 
8
+ def self.format_datetime(time)
9
+ time.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
10
+ end
11
+
8
12
  def self.parse(events_xml)
9
13
  Recording.new(events_xml)
10
14
  end
@@ -7,9 +7,11 @@ module BBBEvents
7
7
  "public_chat_event",
8
8
  "participant_status_change_event",
9
9
  "participant_talking_event",
10
+ "participant_muted_event",
10
11
  "poll_started_record_event",
11
12
  "user_responded_to_poll_record_event",
12
13
  "add_shape_event",
14
+ "poll_published_record_event",
13
15
  ]
14
16
 
15
17
  EMOJI_WHITELIST = %w(away neutral confused sad happy applause thumbsUp thumbsDown)
@@ -34,13 +36,23 @@ module BBBEvents
34
36
  @attendees[extUserId] = Attendee.new(e) unless @attendees.key?(extUserId)
35
37
  end
36
38
 
39
+ join_ts = Time.at(timestamp_conversion(e["timestamp"]))
40
+
37
41
  # Handle updates for re-joining users
38
42
  att = @attendees[extUserId]
39
- att.joins << Time.at(timestamp_conversion(e["timestamp"]))
43
+ att.joins << join_ts
40
44
  att.name = e['name']
41
45
  if e['role'] == 'MODERATOR'
42
46
  att.moderator = true
43
47
  end
48
+
49
+ join_2 = {:timestamp => join_ts, :userid => intUserId, :ext_userid => extUserId, :event => :join}
50
+
51
+ unless att.sessions.key?(intUserId)
52
+ att.sessions[intUserId] = { :joins => [], :lefts => []}
53
+ end
54
+
55
+ att.sessions[intUserId][:joins] << join_2
44
56
  end
45
57
 
46
58
  # Log a users leave.
@@ -48,8 +60,18 @@ module BBBEvents
48
60
  intUserId = e['userId']
49
61
  # If the attendee exists, set their leave time.
50
62
  if att = @attendees[@externalUserId[intUserId]]
51
- left = Time.at(timestamp_conversion(e["timestamp"]))
52
- att.leaves << left
63
+ left_ts = Time.at(timestamp_conversion(e["timestamp"]))
64
+ att.leaves << left_ts
65
+
66
+ extUserId = 'missing'
67
+ if @externalUserId.key?(intUserId)
68
+ extUserId = @externalUserId[intUserId]
69
+ end
70
+
71
+ left_2 = {:timestamp => left_ts, :userid => intUserId, :ext_userid => extUserId, :event => :left}
72
+ att.sessions[intUserId][:lefts] << left_2
73
+
74
+ record_stop_talking(att, e["timestamp"])
53
75
  end
54
76
  end
55
77
 
@@ -93,7 +115,24 @@ module BBBEvents
93
115
  attendee.engagement[:talks] += 1
94
116
  attendee.recent_talking_time = timestamp_conversion(e["timestamp"])
95
117
  else
96
- attendee.engagement[:talk_time] += timestamp_conversion(e["timestamp"]) - attendee.recent_talking_time
118
+ record_stop_talking(attendee, e["timestamp"])
119
+ end
120
+ end
121
+
122
+ def record_stop_talking(attendee, timestamp_s)
123
+ return if attendee.recent_talking_time == 0
124
+
125
+ attendee.engagement[:talk_time] += timestamp_conversion(timestamp_s) - attendee.recent_talking_time
126
+ attendee.recent_talking_time = 0
127
+ end
128
+
129
+ def participant_muted_event(e)
130
+ intUserId = e["participant"]
131
+
132
+ return unless attendee = @attendees[@externalUserId[intUserId]]
133
+
134
+ if e["muted"] == "true"
135
+ record_stop_talking(attendee, e["timestamp"])
97
136
  end
98
137
  end
99
138
 
@@ -102,7 +141,7 @@ module BBBEvents
102
141
  id = e["pollId"]
103
142
 
104
143
  @polls[id] = Poll.new(e)
105
- @polls[id].start = timestamp_conversion(e["timestamp"])
144
+ @polls[id].start = Time.at(timestamp_conversion(e["timestamp"]))
106
145
  end
107
146
 
108
147
  # Log user responses to polls.
@@ -113,6 +152,12 @@ module BBBEvents
113
152
  return unless attendee = @attendees[@externalUserId[intUserId]]
114
153
 
115
154
  if poll = @polls[poll_id]
155
+ if poll.type == 'R-'
156
+ poll.votes[@externalUserId[intUserId]] = e["answer"]
157
+
158
+ # We want to store the responses as options.
159
+ poll.options.insert(e["answerId"].to_i, e["answer"])
160
+ end
116
161
  poll.votes[@externalUserId[intUserId]] = poll.options[e["answerId"].to_i]
117
162
  end
118
163
 
@@ -127,5 +172,13 @@ module BBBEvents
127
172
  end
128
173
  end
129
174
  end
175
+
176
+ def poll_published_record_event(e)
177
+ unless e["pollId"].nil?
178
+ if poll = @polls[e["pollId"]]
179
+ poll.published = true
180
+ end
181
+ end
182
+ end
130
183
  end
131
184
  end
@@ -1,9 +1,11 @@
1
1
  module BBBEvents
2
2
  class Poll
3
- attr_accessor :id, :start, :published, :options, :votes
3
+ attr_accessor :id, :type, :question, :start, :published, :options, :votes
4
4
 
5
5
  def initialize(poll_event)
6
6
  @id = poll_event["pollId"]
7
+ @type = poll_event["type"]
8
+ @question = poll_event["question"].nil? ? "" : "#{poll_event['question']}"
7
9
  @published = false
8
10
  @options = JSON.parse(poll_event["answers"]).map { |opt| opt["key"] }
9
11
  @votes = {}
@@ -18,11 +20,22 @@ module BBBEvents
18
20
  instance_variables.each { |var| hash[var[1..-1]] = instance_variable_get(var) }
19
21
  hash
20
22
  end
23
+ alias_method :as_json, :to_h
21
24
 
22
25
  def to_json
23
- hash = {}
24
- instance_variables.each { |var| hash[var[1..-1]] = instance_variable_get(var) }
25
- hash.to_json
26
+ JSON.generate(as_json)
27
+ end
28
+
29
+ def as_json
30
+ {
31
+ id: @id,
32
+ type: @type,
33
+ question: @question,
34
+ published: @published,
35
+ options: @options,
36
+ start: BBBEvents.format_datetime(@start),
37
+ votes: @votes
38
+ }
26
39
  end
27
40
  end
28
41
  end
@@ -1,7 +1,9 @@
1
1
  require 'csv'
2
2
  require 'json'
3
+ require 'active_support'
3
4
  require 'active_support/core_ext/hash'
4
5
 
6
+
5
7
  module BBBEvents
6
8
  CSV_HEADER = %w(name moderator chats talks emojis poll_votes raisehand talk_time join left duration)
7
9
  NO_VOTE_SYMBOL = "-"
@@ -15,6 +17,9 @@ module BBBEvents
15
17
  filename = File.basename(events_xml)
16
18
  raise "#{filename} is not a file or does not exist." unless File.file?(events_xml)
17
19
 
20
+ # The Hash.from_xml automatically converts keys with dashes '-' to snake_case
21
+ # (i.e canvas-recording-ready-url becomes canvas_recording_ready_url)
22
+ # see https://www.rubydoc.info/github/datamapper/extlib/Hash.from_xml
18
23
  raw_recording_data = Hash.from_xml(File.read(events_xml))
19
24
 
20
25
  raise "#{filename} is not a valid xml file (unable to parse)." if raw_recording_data.nil?
@@ -22,17 +27,24 @@ module BBBEvents
22
27
 
23
28
  recording_data = raw_recording_data["recording"]
24
29
  events = recording_data["event"]
30
+ events = [] if events.nil?
25
31
  events = [events] unless events.is_a?(Array)
26
32
 
27
33
  @metadata = recording_data["metadata"]
28
- @meeting_id = recording_data["meeting"]["id"]
29
- @timestamp = extract_timestamp(@meeting_id)
34
+ @meeting_id = recording_data["metadata"]["meetingId"]
35
+
36
+ internal_meeting_id = recording_data["meeting"]["id"]
30
37
 
31
- @first_event = events.first["timestamp"].to_i
32
- @last_event = events.last["timestamp"].to_i
38
+ @timestamp = extract_timestamp(internal_meeting_id)
39
+ @start = Time.at(@timestamp / 1000)
33
40
 
34
- @start = Time.at(@timestamp / 1000)
35
- @finish = Time.at(timestamp_conversion(@last_event))
41
+ if events.length > 0
42
+ @first_event = events.first["timestamp"].to_i
43
+ @last_event = events.last["timestamp"].to_i
44
+ @finish = Time.at(timestamp_conversion(@last_event))
45
+ else
46
+ @finish = @start
47
+ end
36
48
  @duration = (@finish - @start).to_i
37
49
 
38
50
  @attendees = {}
@@ -47,7 +59,7 @@ module BBBEvents
47
59
 
48
60
  @attendees.values.each do |att|
49
61
  att.leaves << @finish if att.joins.length > att.leaves.length
50
- att.duration = total_duration(att)
62
+ att.duration = total_duration(@finish, att)
51
63
  end
52
64
  end
53
65
 
@@ -91,9 +103,17 @@ module BBBEvents
91
103
  end
92
104
  end
93
105
 
106
+ # Transform any CamelCase keys to snake_case
107
+ def transform_metadata
108
+ @metadata.deep_transform_keys do |key|
109
+ k = key.to_s.underscore rescue key
110
+ k.to_sym rescue key
111
+ end
112
+ end
113
+
94
114
  def to_h
95
115
  {
96
- metadata: @metadata,
116
+ metadata: transform_metadata,
97
117
  meeting_id: @meeting_id,
98
118
  duration: @duration,
99
119
  start: @start,
@@ -104,8 +124,230 @@ module BBBEvents
104
124
  }
105
125
  end
106
126
 
127
+ def as_json
128
+ {
129
+ metadata: transform_metadata,
130
+ meeting_id: @meeting_id,
131
+ duration: @duration,
132
+ start: BBBEvents.format_datetime(@start),
133
+ finish: BBBEvents.format_datetime(@finish),
134
+ attendees: attendees.map(&:as_json),
135
+ files: @files,
136
+ polls: polls.map(&:as_json)
137
+ }
138
+ end
139
+
107
140
  def to_json
108
- to_h.to_json
141
+ JSON.generate(as_json)
142
+ end
143
+
144
+ def calculate_user_duration(join_events, left_events)
145
+ joins_leaves_arr = []
146
+ join_events.each { |j| joins_leaves_arr.append({:time => j.to_i, :datetime => j, :event => :join})}
147
+ left_events.each { |j| joins_leaves_arr.append({:time => j.to_i, :datetime => j, :event => :left})}
148
+
149
+ joins_leaves_arr_sorted = joins_leaves_arr.sort_by { |event| event[:time] }
150
+
151
+ partial_duration = 0
152
+ prev_event = nil
153
+
154
+ joins_leaves_arr_sorted.each do |cur_event|
155
+ duration = 0
156
+ if prev_event != nil and cur_event[:event] == :join and prev_event[:event] == :left
157
+ # user left and rejoining, don't update duration
158
+ prev_event = cur_event
159
+ elsif prev_event != nil
160
+ duration = cur_event[:time] - prev_event[:time]
161
+ partial_duration += duration
162
+ prev_event = cur_event
163
+ else
164
+ prev_event = cur_event
165
+ end
166
+ end
167
+
168
+ return partial_duration
169
+ end
170
+
171
+ def calculate_user_duration_based_on_userid(last_event_ts, sessions)
172
+ # combine join and left events into an array
173
+ joins_lefts_arr = build_join_lefts_array(last_event_ts, sessions)
174
+
175
+ # sort the events
176
+ joins_lefts_arr_sorted = joins_lefts_arr.sort_by { |event| event[:timestamp] }
177
+
178
+ combined_tuples = combine_tuples_by_userid(sessions)
179
+ combined_tuples_sorted = fill_missing_left_events(combined_tuples)
180
+
181
+ prepare_joins_lefts_for_overlap_checks(joins_lefts_arr_sorted)
182
+ mark_overlapping_events(combined_tuples_sorted, joins_lefts_arr_sorted)
183
+ removed_overlap_events = remove_overlapping_events(joins_lefts_arr_sorted)
184
+
185
+ duration_tuples = build_join_left_tuples(removed_overlap_events)
186
+
187
+ partial_duration = 0
188
+ duration_tuples.each do |tuple|
189
+ duration = tuple[:left][:timestamp].to_i - tuple[:join][:timestamp].to_i
190
+ partial_duration += duration
191
+ end
192
+
193
+ partial_duration
194
+ end
195
+
196
+ def tuples_by_userid(joins_arr, lefts_arr)
197
+ joins_length = joins_arr.length - 1
198
+ tuples = []
199
+ for i in 0..joins_length
200
+ tuple = {:join => joins_arr[i], :left => nil}
201
+
202
+ if i <= lefts_arr.length - 1
203
+ tuple[:left] = lefts_arr[i]
204
+ end
205
+ tuples.append(tuple)
206
+ end
207
+ tuples
208
+ end
209
+
210
+ def combine_tuples_by_userid(user_sessions)
211
+ combined_tuples = []
212
+
213
+ user_sessions.each do | userid, joins_lefts |
214
+ joins_lefts_arr = []
215
+ joins_lefts[:joins].each { |j| joins_lefts_arr.append(j)}
216
+ joins_lefts[:lefts].each { |j| joins_lefts_arr.append(j)}
217
+
218
+ tuples = tuples_by_userid(joins_lefts[:joins], joins_lefts[:lefts])
219
+
220
+ tuples.each do |tuple|
221
+ combined_tuples.append(tuple)
222
+ end
223
+ end
224
+
225
+ combined_tuples
226
+ end
227
+
228
+ def fill_missing_left_events(combined_tuples)
229
+ joins_lefts_arr_sorted = combined_tuples.sort_by { |event| event[:join][:timestamp]}
230
+
231
+ joins_lefts_arr_sorted_length = joins_lefts_arr_sorted.length - 1
232
+ for i in 0..joins_lefts_arr_sorted_length
233
+ cur_event = joins_lefts_arr_sorted[i]
234
+ if cur_event[:left].nil?
235
+ unless joins_lefts_arr_sorted_length == i
236
+ # Take the next event as the left event for this current event
237
+ next_event = joins_lefts_arr_sorted[i + 1]
238
+ left_event = {:timestamp => next_event[:timestamp], :userid => cur_event[:userid], :event => :left}
239
+
240
+ cur_event[:left] = left_event
241
+ end
242
+ end
243
+ end
244
+
245
+ joins_lefts_arr_sorted
246
+ end
247
+
248
+ def build_join_left_tuples(joins_lefts_arr_sorted)
249
+ jl_tuples = []
250
+ jl_tuple = {:join => nil, :left => nil}
251
+ loop_state = :find_join
252
+
253
+ events_length = joins_lefts_arr_sorted.length - 1
254
+ for i in 0..events_length
255
+
256
+ cur_event = joins_lefts_arr_sorted[i]
257
+
258
+ if loop_state == :find_join and cur_event[:event] == :join
259
+ jl_tuple[:join] = cur_event
260
+ loop_state = :find_left
261
+ end
262
+
263
+ next_event = nil
264
+ if i < events_length
265
+ next_event = joins_lefts_arr_sorted[i + 1]
266
+ end
267
+
268
+ if loop_state == :find_left
269
+ if next_event != nil and next_event[:event] == :left
270
+ # skip the current event to get to the next event
271
+ elsif (cur_event[:event] == :left and next_event != nil and next_event[:event] == :join) or (i == events_length)
272
+ jl_tuple[:left] = cur_event
273
+ jl_tuples.append(jl_tuple)
274
+ jl_tuple = {:join => nil, :left => nil}
275
+ loop_state = :find_join
276
+ end
277
+ end
278
+ end
279
+
280
+ jl_tuples
281
+ end
282
+
283
+ def build_join_lefts_array(last_event_timestamp, user_session)
284
+ joins_leaves_arr = []
285
+ lefts_count = 0
286
+ joins_count = 0
287
+
288
+ user_session.each do | userid, joins_lefts |
289
+ lefts_count += joins_lefts[:lefts].length
290
+ joins_count += joins_lefts[:joins].length
291
+ joins_lefts[:joins].each { |j| joins_leaves_arr.append(j)}
292
+ joins_lefts[:lefts].each { |j| joins_leaves_arr.append(j)}
293
+ end
294
+
295
+ if joins_count > lefts_count
296
+ last_event = joins_leaves_arr[-1]
297
+ joins_leaves_arr.append({:timestamp => last_event_timestamp, :userid => " system ", :ext_userid=> last_event[:ext_userid], :event => :left})
298
+ end
299
+
300
+ joins_leaves_arr
301
+ end
302
+
303
+ def prepare_joins_lefts_for_overlap_checks(joins_leaves_arr_sorted)
304
+ joins_leaves_arr_sorted.each do |event|
305
+ event[:remove] = false
306
+ end
307
+ end
308
+
309
+ def mark_overlapping_events(combined_tuples_sorted, joins_leaves_arr_sorted)
310
+ combined_tuples_sorted.each do |ce|
311
+ joins_leaves_arr_sorted.each do |jl|
312
+ event_ts = jl[:timestamp].to_i
313
+ ce_join = ce[:join][:timestamp].to_i
314
+
315
+ if event_ts > ce_join and not ce[:left].nil? and event_ts < ce[:left][:timestamp].to_i
316
+ jl[:remove] = true
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ def remove_overlapping_events(joins_leaves_arr_sorted)
323
+ keep_events = []
324
+ joins_leaves_arr_sorted.each do |ev|
325
+ if not ev[:remove]
326
+ keep_events.append(ev)
327
+ end
328
+ end
329
+ keep_events
330
+ end
331
+
332
+ def build_tuples_of_kept_events(kept_events)
333
+ odd_events = []
334
+ even_events = []
335
+ for i in 0..kept_events.length - 1
336
+ odd_even = i + 1
337
+ if odd_even.even?
338
+ even_events.append(kept_events[i])
339
+ else
340
+ odd_events.append(kept_events[i])
341
+ end
342
+ end
343
+
344
+ tuples = []
345
+ for i in 0..odd_events.length - 1
346
+ tuple = {:start => odd_events[i], :end => even_events[i]}
347
+ tuples.append(tuple)
348
+ end
349
+
350
+ tuples
109
351
  end
110
352
 
111
353
  private
@@ -129,14 +371,8 @@ module BBBEvents
129
371
  end
130
372
 
131
373
  # Calculates an attendee's duration.
132
- def total_duration(att)
133
- return 0 unless att.joins.length == att.leaves.length
134
- total = 0
135
-
136
- att.joins.length.times do |i|
137
- total += att.leaves[i] - att.joins[i]
138
- end
139
- total
374
+ def total_duration(last_event_ts, att)
375
+ calculate_user_duration_based_on_userid(last_event_ts, att.sessions)
140
376
  end
141
377
  end
142
378
  end
@@ -1,3 +1,3 @@
1
1
  module BBBEvents
2
- VERSION = "1.1.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/testdata/.gitkeep ADDED
File without changes
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bbbevents
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blindside Networks
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-10 00:00:00.000000000 Z
11
+ date: 2023-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.15'
19
+ version: '2.0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.15'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -54,6 +54,26 @@ dependencies:
54
54
  version: '3.4'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 5.0.0.1
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '8'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 5.0.0.1
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '8'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rexml
57
77
  requirement: !ruby/object:Gem::Requirement
58
78
  requirements:
59
79
  - - ">="
@@ -66,18 +86,19 @@ dependencies:
66
86
  - - ">="
67
87
  - !ruby/object:Gem::Version
68
88
  version: '0'
69
- description: Easily parse data from a BigBlueButton recording's events.xml.
89
+ description: Ruby gem for easily parse data from a BigBlueButton recording's events.xml.
70
90
  email:
71
91
  - ffdixon@blindsidenetworks.com
72
92
  executables: []
73
93
  extensions: []
74
94
  extra_rdoc_files: []
75
95
  files:
96
+ - ".github/workflows/ci.yml"
97
+ - ".github/workflows/release.yml"
76
98
  - ".gitignore"
77
99
  - ".rspec"
78
- - ".travis.yml"
100
+ - ".ruby-version"
79
101
  - Gemfile
80
- - Gemfile.lock
81
102
  - LICENSE
82
103
  - README.md
83
104
  - Rakefile
@@ -92,9 +113,10 @@ files:
92
113
  - lib/bbbevents/poll.rb
93
114
  - lib/bbbevents/recording.rb
94
115
  - lib/bbbevents/version.rb
116
+ - testdata/.gitkeep
95
117
  homepage: https://www.blindsidenetworks.com
96
118
  licenses:
97
- - LGPL 3.0
119
+ - LGPL-3.0
98
120
  metadata: {}
99
121
  post_install_message:
100
122
  rdoc_options: []
@@ -111,8 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
133
  - !ruby/object:Gem::Version
112
134
  version: '0'
113
135
  requirements: []
114
- rubyforge_project:
115
- rubygems_version: 2.7.6
136
+ rubygems_version: 3.1.2
116
137
  signing_key:
117
138
  specification_version: 4
118
139
  summary: Easily parse data from a BigBlueButton recording's events.xml.
data/.travis.yml DELETED
@@ -1,14 +0,0 @@
1
- sudo: false
2
-
3
- language: ruby
4
-
5
- rvm:
6
- - 2.5.1
7
-
8
- before_install: gem install bundler -v 1.15.4
9
-
10
- deploy:
11
- provider: rubygems
12
- api_key: ${RUBYGEMS_API_KEY}
13
- on:
14
- tags: true
data/Gemfile.lock DELETED
@@ -1,48 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- bbbevents (1.1.0)
5
- activesupport
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- activesupport (5.2.3)
11
- concurrent-ruby (~> 1.0, >= 1.0.2)
12
- i18n (>= 0.7, < 2)
13
- minitest (~> 5.1)
14
- tzinfo (~> 1.1)
15
- concurrent-ruby (1.1.5)
16
- diff-lcs (1.3)
17
- i18n (1.6.0)
18
- concurrent-ruby (~> 1.0)
19
- minitest (5.11.3)
20
- rake (10.5.0)
21
- rspec (3.8.0)
22
- rspec-core (~> 3.8.0)
23
- rspec-expectations (~> 3.8.0)
24
- rspec-mocks (~> 3.8.0)
25
- rspec-core (3.8.2)
26
- rspec-support (~> 3.8.0)
27
- rspec-expectations (3.8.4)
28
- diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.8.0)
30
- rspec-mocks (3.8.1)
31
- diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.8.0)
33
- rspec-support (3.8.2)
34
- thread_safe (0.3.6)
35
- tzinfo (1.2.5)
36
- thread_safe (~> 0.1)
37
-
38
- PLATFORMS
39
- ruby
40
-
41
- DEPENDENCIES
42
- bbbevents!
43
- bundler (>= 1.15)
44
- rake (~> 10.0)
45
- rspec (~> 3.4)
46
-
47
- BUNDLED WITH
48
- 1.16.3