bbbevents 1.2.0 → 2.0.3

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: 8b7c540fdb454cc4389952b69531500bfaf6d2997b92c1578117904cd92a4558
4
- data.tar.gz: 953c41dd900c59e78630b5067d6d5b2be5599dce4d154f6744ce183b6d24b889
3
+ metadata.gz: e895c0a369286e3300296ab1b892a3b2c87ac069e5dda6524f29dfd5019f85af
4
+ data.tar.gz: b3c418cd7e21d054f2cf6530bd420ab1578b2d3e0f255a58db6352f49bf38f67
5
5
  SHA512:
6
- metadata.gz: a8a525a2317ef15232cfc1ec5ee18b0dfccdc2d5fadf6e5579f6aa241fbf8b0bb3b7935acb3cf1bea66f2cb8197eb6620541404ca7b361913e7b1ef7b7360724
7
- data.tar.gz: 6ecbc66f261eaa9bffdd8948c763dfe1f794cbec99254ab2e844915cd57b088cbef929b3fa7a05dda66e26df406f84c1da1ed6451f83efa988c2d540f7487939
6
+ metadata.gz: ecadaaf62ac76075d2205667bda14c1f10b2f37c241046d71f01d883758fdfeab85e084f67ac5c5c2b9ddc5ca90a1078eb0ecaaf6279ff5b0621111b72381ebc
7
+ data.tar.gz: c27c012df8e3beddad6a3226c5688e2861e8bfe5ff2e046ed7baaf8066f8ed107cf32d9d3bf6d1254c189fa539ba819361f8d4d0aa81fe89ae555b4cee1773a8
@@ -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-24.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-24.04
10
+ if: startsWith(github.ref, 'refs/tags/')
11
+ steps:
12
+ - uses: actions/checkout@v4
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
@@ -21,11 +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', '~> 5.0', '>= 5.0.0.1'
30
-
29
+ spec.add_dependency 'activesupport', '>= 5.0.0.1', '< 8'
30
+ spec.add_dependency 'rexml' # Required for activesupport from_xml
31
+ spec.add_dependency 'csv'
31
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,6 +1,6 @@
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"
@@ -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,26 +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
- # Convert recent_talking_time to human readable time
65
- if hash["recent_talking_time"] > 0
66
- hash["recent_talking_time"] = Time.at(hash["recent_talking_time"])
67
- else
68
- hash["recent_talking_time"] = ""
69
- end
70
- 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
+ }
71
97
  end
72
98
 
73
99
  def to_json
74
- hash = {}
75
- instance_variables.each { |var| hash[var[1..-1]] = instance_variable_get(var) }
76
- if hash["recent_talking_time"] > 0
77
- hash["recent_talking_time"] = Time.at(hash["recent_talking_time"])
78
- else
79
- hash["recent_talking_time"] = ""
80
- end
81
- hash.to_json
100
+ JSON.generate(as_json)
82
101
  end
83
102
 
84
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
 
@@ -72,12 +94,21 @@ module BBBEvents
72
94
  intUserId = e['userId']
73
95
 
74
96
  return unless attendee = @attendees[@externalUserId[intUserId]]
75
- status = e["value"]
97
+ status = e["status"]
98
+ status_value = e['value']
76
99
 
77
100
  if attendee
78
- if status == RAISEHAND
101
+ # Support new event format
102
+ if status == RAISEHAND && status_value == 'true'
103
+ # Count only raise hand event and not lower hand
104
+ attendee.engagement[:raisehand] += 1
105
+ elsif status == 'reactionEmoji' && status_value != 'none'
106
+ attendee.engagement[:emojis] += 1
107
+
108
+ # Support old event format
109
+ elsif status_value == RAISEHAND
79
110
  attendee.engagement[:raisehand] += 1
80
- elsif EMOJI_WHITELIST.include?(status)
111
+ elsif EMOJI_WHITELIST.include?(status_value)
81
112
  attendee.engagement[:emojis] += 1
82
113
  end
83
114
  end
@@ -93,7 +124,24 @@ module BBBEvents
93
124
  attendee.engagement[:talks] += 1
94
125
  attendee.recent_talking_time = timestamp_conversion(e["timestamp"])
95
126
  else
96
- attendee.engagement[:talk_time] += timestamp_conversion(e["timestamp"]) - attendee.recent_talking_time
127
+ record_stop_talking(attendee, e["timestamp"])
128
+ end
129
+ end
130
+
131
+ def record_stop_talking(attendee, timestamp_s)
132
+ return if attendee.recent_talking_time == 0
133
+
134
+ attendee.engagement[:talk_time] += timestamp_conversion(timestamp_s) - attendee.recent_talking_time
135
+ attendee.recent_talking_time = 0
136
+ end
137
+
138
+ def participant_muted_event(e)
139
+ intUserId = e["participant"]
140
+
141
+ return unless attendee = @attendees[@externalUserId[intUserId]]
142
+
143
+ if e["muted"] == "true"
144
+ record_stop_talking(attendee, e["timestamp"])
97
145
  end
98
146
  end
99
147
 
@@ -113,6 +161,12 @@ module BBBEvents
113
161
  return unless attendee = @attendees[@externalUserId[intUserId]]
114
162
 
115
163
  if poll = @polls[poll_id]
164
+ if poll.type == 'R-'
165
+ poll.votes[@externalUserId[intUserId]] = e["answer"]
166
+
167
+ # We want to store the responses as options.
168
+ poll.options.insert(e["answerId"].to_i, e["answer"])
169
+ end
116
170
  poll.votes[@externalUserId[intUserId]] = poll.options[e["answerId"].to_i]
117
171
  end
118
172
 
@@ -127,5 +181,13 @@ module BBBEvents
127
181
  end
128
182
  end
129
183
  end
184
+
185
+ def poll_published_record_event(e)
186
+ unless e["pollId"].nil?
187
+ if poll = @polls[e["pollId"]]
188
+ poll.published = true
189
+ end
190
+ end
191
+ end
130
192
  end
131
193
  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,20 +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
34
  @meeting_id = recording_data["metadata"]["meetingId"]
29
-
35
+
30
36
  internal_meeting_id = recording_data["meeting"]["id"]
31
37
 
32
38
  @timestamp = extract_timestamp(internal_meeting_id)
39
+ @start = Time.at(@timestamp / 1000)
33
40
 
34
- @first_event = events.first["timestamp"].to_i
35
- @last_event = events.last["timestamp"].to_i
36
-
37
- @start = Time.at(@timestamp / 1000)
38
- @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
39
48
  @duration = (@finish - @start).to_i
40
49
 
41
50
  @attendees = {}
@@ -50,7 +59,7 @@ module BBBEvents
50
59
 
51
60
  @attendees.values.each do |att|
52
61
  att.leaves << @finish if att.joins.length > att.leaves.length
53
- att.duration = total_duration(att)
62
+ att.duration = total_duration(@finish, att)
54
63
  end
55
64
  end
56
65
 
@@ -94,15 +103,17 @@ module BBBEvents
94
103
  end
95
104
  end
96
105
 
97
- def to_h
98
- # Transform any CamelCase keys to snake_case.
99
- @metadata.deep_transform_keys! do |key|
100
- k = key.to_s.underscore rescue key
101
- k.to_sym rescue key
102
- end
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
103
113
 
114
+ def to_h
104
115
  {
105
- metadata: @metadata,
116
+ metadata: transform_metadata,
106
117
  meeting_id: @meeting_id,
107
118
  duration: @duration,
108
119
  start: @start,
@@ -113,8 +124,230 @@ module BBBEvents
113
124
  }
114
125
  end
115
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
+
116
140
  def to_json
117
- 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
118
351
  end
119
352
 
120
353
  private
@@ -138,14 +371,8 @@ module BBBEvents
138
371
  end
139
372
 
140
373
  # Calculates an attendee's duration.
141
- def total_duration(att)
142
- return 0 unless att.joins.length == att.leaves.length
143
- total = 0
144
-
145
- att.joins.length.times do |i|
146
- total += att.leaves[i] - att.joins[i]
147
- end
148
- total
374
+ def total_duration(last_event_ts, att)
375
+ calculate_user_duration_based_on_userid(last_event_ts, att.sessions)
149
376
  end
150
377
  end
151
378
  end
@@ -1,3 +1,3 @@
1
1
  module BBBEvents
2
- VERSION = "1.2.0"
2
+ VERSION = "2.0.3"
3
3
  end
data/testdata/.gitkeep ADDED
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bbbevents
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blindside Networks
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-19 00:00:00.000000000 Z
11
+ date: 2026-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,28 +16,28 @@ dependencies:
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
@@ -56,22 +56,50 @@ dependencies:
56
56
  name: activesupport
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 5.0.0.1
62
+ - - "<"
60
63
  - !ruby/object:Gem::Version
61
- version: '5.0'
64
+ version: '8'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
62
69
  - - ">="
63
70
  - !ruby/object:Gem::Version
64
71
  version: 5.0.0.1
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '8'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rexml
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
65
82
  type: :runtime
66
83
  prerelease: false
67
84
  version_requirements: !ruby/object:Gem::Requirement
68
85
  requirements:
69
- - - "~>"
86
+ - - ">="
70
87
  - !ruby/object:Gem::Version
71
- version: '5.0'
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: csv
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
72
93
  - - ">="
73
94
  - !ruby/object:Gem::Version
74
- version: 5.0.0.1
95
+ version: '0'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
75
103
  description: Ruby gem for easily parse data from a BigBlueButton recording's events.xml.
76
104
  email:
77
105
  - ffdixon@blindsidenetworks.com
@@ -79,11 +107,12 @@ executables: []
79
107
  extensions: []
80
108
  extra_rdoc_files: []
81
109
  files:
110
+ - ".github/workflows/ci.yml"
111
+ - ".github/workflows/release.yml"
82
112
  - ".gitignore"
83
113
  - ".rspec"
84
- - ".travis.yml"
114
+ - ".ruby-version"
85
115
  - Gemfile
86
- - Gemfile.lock
87
116
  - LICENSE
88
117
  - README.md
89
118
  - Rakefile
@@ -98,11 +127,12 @@ files:
98
127
  - lib/bbbevents/poll.rb
99
128
  - lib/bbbevents/recording.rb
100
129
  - lib/bbbevents/version.rb
130
+ - testdata/.gitkeep
101
131
  homepage: https://www.blindsidenetworks.com
102
132
  licenses:
103
133
  - LGPL-3.0
104
134
  metadata: {}
105
- post_install_message:
135
+ post_install_message:
106
136
  rdoc_options: []
107
137
  require_paths:
108
138
  - lib
@@ -117,9 +147,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
147
  - !ruby/object:Gem::Version
118
148
  version: '0'
119
149
  requirements: []
120
- rubyforge_project:
121
- rubygems_version: 2.7.6
122
- signing_key:
150
+ rubygems_version: 3.1.2
151
+ signing_key:
123
152
  specification_version: 4
124
153
  summary: Easily parse data from a BigBlueButton recording's events.xml.
125
154
  test_files: []
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.17.3
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.2.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.17.3