bbbevents 1.2.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 +4 -4
- data/.github/workflows/ci.yml +20 -0
- data/.github/workflows/release.yml +32 -0
- data/.gitignore +8 -1
- data/.ruby-version +1 -0
- data/README.md +3 -3
- data/bbbevents.gemspec +4 -3
- data/example.rb +1 -3
- data/lib/bbbevents/attendee.rb +37 -18
- data/lib/bbbevents/base.rb +4 -0
- data/lib/bbbevents/events.rb +57 -4
- data/lib/bbbevents/poll.rb +17 -4
- data/lib/bbbevents/recording.rb +250 -23
- data/lib/bbbevents/version.rb +1 -1
- data/testdata/.gitkeep +0 -0
- metadata +31 -16
- data/.travis.yml +0 -14
- data/Gemfile.lock +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea120800577b78c239ae7c6b0e1007162493fdad13904dcb88e7f9fe8b2762c2
|
4
|
+
data.tar.gz: 61671fcd6e70a3b341aee4e07d1b1961f06aeb3149d9f55785f422cb80af5b3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
105
|
+
# Returns an Array containing possible options.
|
106
106
|
poll.options
|
107
107
|
|
108
|
-
# Returns a Hash
|
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", "~>
|
25
|
-
spec.add_development_dependency "rake", "~>
|
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
|
30
31
|
|
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
|
-
|
data/lib/bbbevents/attendee.rb
CHANGED
@@ -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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
data/lib/bbbevents/base.rb
CHANGED
data/lib/bbbevents/events.rb
CHANGED
@@ -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 <<
|
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
|
-
|
52
|
-
att.leaves <<
|
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
|
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
|
|
@@ -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
|
data/lib/bbbevents/poll.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
25
|
-
|
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
|
data/lib/bbbevents/recording.rb
CHANGED
@@ -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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
98
|
-
|
99
|
-
@metadata.deep_transform_keys
|
100
|
-
|
101
|
-
|
102
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
data/lib/bbbevents/version.rb
CHANGED
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:
|
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:
|
11
|
+
date: 2023-06-23 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: '
|
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: '
|
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: '
|
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: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,22 +56,36 @@ dependencies:
|
|
56
56
|
name: activesupport
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '5.0'
|
62
59
|
- - ">="
|
63
60
|
- !ruby/object:Gem::Version
|
64
61
|
version: 5.0.0.1
|
62
|
+
- - "<"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '8'
|
65
65
|
type: :runtime
|
66
66
|
prerelease: false
|
67
67
|
version_requirements: !ruby/object:Gem::Requirement
|
68
68
|
requirements:
|
69
|
-
- - "~>"
|
70
|
-
- !ruby/object:Gem::Version
|
71
|
-
version: '5.0'
|
72
69
|
- - ">="
|
73
70
|
- !ruby/object:Gem::Version
|
74
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'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
75
89
|
description: Ruby gem for easily parse data from a BigBlueButton recording's events.xml.
|
76
90
|
email:
|
77
91
|
- ffdixon@blindsidenetworks.com
|
@@ -79,11 +93,12 @@ executables: []
|
|
79
93
|
extensions: []
|
80
94
|
extra_rdoc_files: []
|
81
95
|
files:
|
96
|
+
- ".github/workflows/ci.yml"
|
97
|
+
- ".github/workflows/release.yml"
|
82
98
|
- ".gitignore"
|
83
99
|
- ".rspec"
|
84
|
-
- ".
|
100
|
+
- ".ruby-version"
|
85
101
|
- Gemfile
|
86
|
-
- Gemfile.lock
|
87
102
|
- LICENSE
|
88
103
|
- README.md
|
89
104
|
- Rakefile
|
@@ -98,6 +113,7 @@ files:
|
|
98
113
|
- lib/bbbevents/poll.rb
|
99
114
|
- lib/bbbevents/recording.rb
|
100
115
|
- lib/bbbevents/version.rb
|
116
|
+
- testdata/.gitkeep
|
101
117
|
homepage: https://www.blindsidenetworks.com
|
102
118
|
licenses:
|
103
119
|
- LGPL-3.0
|
@@ -117,8 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
133
|
- !ruby/object:Gem::Version
|
118
134
|
version: '0'
|
119
135
|
requirements: []
|
120
|
-
|
121
|
-
rubygems_version: 2.7.6
|
136
|
+
rubygems_version: 3.1.2
|
122
137
|
signing_key:
|
123
138
|
specification_version: 4
|
124
139
|
summary: Easily parse data from a BigBlueButton recording's events.xml.
|
data/.travis.yml
DELETED
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
|