Flay: refactor sections of similar code
This commit is contained in:
parent
6dd50086fb
commit
5d40c427d5
@ -1,4 +1,3 @@
|
|||||||
# Set encoding to utf-8
|
|
||||||
# frozen_string_literal: false
|
# frozen_string_literal: false
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -23,10 +22,10 @@ _performance_start = Time.now
|
|||||||
|
|
||||||
# For DEVELOPMENT
|
# For DEVELOPMENT
|
||||||
# Allows us to run the script manually
|
# Allows us to run the script manually
|
||||||
# require File.expand_path('../../../core/lib/recordandplayback', __dir__)
|
# require File.expand_path('../../../core/lib/recordandplayback', __FILE__)
|
||||||
|
|
||||||
# For PRODUCTION
|
# For PRODUCTION
|
||||||
require File.expand_path('../../../lib/recordandplayback', __dir__)
|
require File.expand_path('../../../lib/recordandplayback', __FILE__)
|
||||||
|
|
||||||
require 'rubygems'
|
require 'rubygems'
|
||||||
require 'optimist'
|
require 'optimist'
|
||||||
@ -164,14 +163,10 @@ def svg_render_shape_pencil(g, slide, shape)
|
|||||||
# BigBlueButton.logger.info("Pencil #{shape_unique_id}: Drawing from command string (#{shape_commands.length} commands)")
|
# BigBlueButton.logger.info("Pencil #{shape_unique_id}: Drawing from command string (#{shape_commands.length} commands)")
|
||||||
shape_commands.each do |command|
|
shape_commands.each do |command|
|
||||||
case command
|
case command
|
||||||
when 1 # MOVE_TO
|
when 1, 2 # MOVE_TO, LINE_TO
|
||||||
x = shape_scale_width(slide, data_points.next)
|
x = shape_scale_width(slide, data_points.next)
|
||||||
y = shape_scale_height(slide, data_points.next)
|
y = shape_scale_height(slide, data_points.next)
|
||||||
path.push("M#{x} #{y}")
|
path.push("#{command.eql?(1) ? 'M' : 'L'}#{x} #{y}")
|
||||||
when 2 # LINE_TO
|
|
||||||
x = shape_scale_width(slide, data_points.next)
|
|
||||||
y = shape_scale_height(slide, data_points.next)
|
|
||||||
path.push("L#{x} #{y}")
|
|
||||||
when 3 # Q_CURVE_TO
|
when 3 # Q_CURVE_TO
|
||||||
cx1 = shape_scale_width(slide, data_points.next)
|
cx1 = shape_scale_width(slide, data_points.next)
|
||||||
cy1 = shape_scale_height(slide, data_points.next)
|
cy1 = shape_scale_height(slide, data_points.next)
|
||||||
@ -232,7 +227,7 @@ end
|
|||||||
|
|
||||||
def stroke_attributes(slide, shape)
|
def stroke_attributes(slide, shape)
|
||||||
"stroke:##{shape_color = shape[:color]};stroke-width:#{shape_thickness(slide,
|
"stroke:##{shape_color = shape[:color]};stroke-width:#{shape_thickness(slide,
|
||||||
shape)};visibility:hidden;fill:#{shape[:fill] ? "##{shape_color}" : 'none'}"
|
shape)};visibility:hidden;fill:#{shape[:fill] ? "##{shape_color}" : 'none'}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_render_shape_rect(g, slide, shape)
|
def svg_render_shape_rect(g, slide, shape)
|
||||||
@ -508,6 +503,10 @@ def panzooms_emit_event(rec, panzoom)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def convert_cursor_coordinate(cursor_coord, panzoom_offset, panzoom_ratio)
|
||||||
|
(((cursor_coord / 100.0) + (panzoom_offset * @magic_mystery_number / 100.0)) / (panzoom_ratio / 100.0)).round(5)
|
||||||
|
end
|
||||||
|
|
||||||
def cursors_emit_event(rec, cursor)
|
def cursors_emit_event(rec, cursor)
|
||||||
if (cursor_in = cursor[:in]) == cursor[:out]
|
if (cursor_in = cursor[:in]) == cursor[:out]
|
||||||
# BigBlueButton.logger.info('Cursor: not emitting, duration rounds to 0')
|
# BigBlueButton.logger.info('Cursor: not emitting, duration rounds to 0')
|
||||||
@ -520,10 +519,8 @@ def cursors_emit_event(rec, cursor)
|
|||||||
if @version_atleast_2_0_0
|
if @version_atleast_2_0_0
|
||||||
# In BBB 2.0, the cursor now uses the same coordinate system as annotations
|
# In BBB 2.0, the cursor now uses the same coordinate system as annotations
|
||||||
# Use the panzoom information to convert it to be relative to viewbox
|
# Use the panzoom information to convert it to be relative to viewbox
|
||||||
x = (((cursor[:x] / 100.0) + (panzoom[:x_offset] * @magic_mystery_number / 100.0)) /
|
x = convert_cursor_coordinate(cursor[:x], panzoom[:x_offset], panzoom[:width_ratio])
|
||||||
(panzoom[:width_ratio] / 100.0)).round(5)
|
y = convert_cursor_coordinate(cursor[:y], panzoom[:y_offset], panzoom[:height_ratio])
|
||||||
y = (((cursor[:y] / 100.0) + (panzoom[:y_offset] * @magic_mystery_number / 100.0)) /
|
|
||||||
(panzoom[:height_ratio] / 100.0)).round(5)
|
|
||||||
x = y = -1.0 if x.negative? || (x > 1) || y.negative? || (y > 1)
|
x = y = -1.0 if x.negative? || (x > 1) || y.negative? || (y > 1)
|
||||||
else
|
else
|
||||||
# Cursor position is relative to the visible area
|
# Cursor position is relative to the visible area
|
||||||
@ -583,7 +580,7 @@ def events_parse_shape(shapes, event, current_presentation, current_slide, times
|
|||||||
# These can be missing in old BBB versions, there are fallbacks
|
# These can be missing in old BBB versions, there are fallbacks
|
||||||
user_id = event.at_xpath('userId')
|
user_id = event.at_xpath('userId')
|
||||||
shape[:user_id] = user_id.text unless user_id.nil?
|
shape[:user_id] = user_id.text unless user_id.nil?
|
||||||
|
|
||||||
unless (shape_id = event.at_xpath('id').text).nil?
|
unless (shape_id = event.at_xpath('id').text).nil?
|
||||||
shape[:id] = shape_id
|
shape[:id] = shape_id
|
||||||
end
|
end
|
||||||
@ -682,6 +679,14 @@ def events_parse_shape(shapes, event, current_presentation, current_slide, times
|
|||||||
shapes << shape
|
shapes << shape
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_undo_helper(shapes, key, id, timestamp)
|
||||||
|
shapes.each do |shape|
|
||||||
|
next unless shape[key] == id
|
||||||
|
|
||||||
|
shape[:undo] = timestamp if (shape_undo = shape[:undo]).nil? || (shape_undo > timestamp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
|
def events_parse_undo(shapes, event, current_presentation, current_slide, timestamp)
|
||||||
# Figure out what presentation+slide this undo is for, with fallbacks
|
# Figure out what presentation+slide this undo is for, with fallbacks
|
||||||
# for old BBB where this info isn't in the undo messages
|
# for old BBB where this info isn't in the undo messages
|
||||||
@ -694,7 +699,7 @@ def events_parse_undo(shapes, event, current_presentation, current_slide, timest
|
|||||||
# Newer undo messages have the shape id, making this a lot easier
|
# Newer undo messages have the shape id, making this a lot easier
|
||||||
shape_id = event.at_xpath('shapeId')
|
shape_id = event.at_xpath('shapeId')
|
||||||
shape_id_nil = shape_id.nil?
|
shape_id_nil = shape_id.nil?
|
||||||
shape_id = shape_id.text unless shape_id_nil
|
shape_id = shape_id.text unless shape_id_nil
|
||||||
|
|
||||||
# Set up the shapes data structures if needed
|
# Set up the shapes data structures if needed
|
||||||
shapes[presentation] = {} if shapes[presentation].nil?
|
shapes[presentation] = {} if shapes[presentation].nil?
|
||||||
@ -707,11 +712,8 @@ def events_parse_undo(shapes, event, current_presentation, current_slide, timest
|
|||||||
# If we have the shape id, we simply have to update the undo time on
|
# If we have the shape id, we simply have to update the undo time on
|
||||||
# all the shapes with that id.
|
# all the shapes with that id.
|
||||||
BigBlueButton.logger.info("Undo: removing shape with ID #{shape_id} at #{timestamp}")
|
BigBlueButton.logger.info("Undo: removing shape with ID #{shape_id} at #{timestamp}")
|
||||||
shapes.each do |shape|
|
|
||||||
next unless shape[:id] == shape_id
|
|
||||||
|
|
||||||
shape[:undo] = timestamp if (shape_undo = shape[:undo]).nil? || (shape_undo > timestamp)
|
set_undo_helper(shapes, :id, shape_id, timestamp)
|
||||||
end
|
|
||||||
else
|
else
|
||||||
# The undo command removes the most recently added shape that has not
|
# The undo command removes the most recently added shape that has not
|
||||||
# already been removed by another undo or clear. Find that shape.
|
# already been removed by another undo or clear. Find that shape.
|
||||||
@ -723,11 +725,7 @@ def events_parse_undo(shapes, event, current_presentation, current_slide, timest
|
|||||||
# We have an id number assigned to associate all the updated versions
|
# We have an id number assigned to associate all the updated versions
|
||||||
# of the same shape. Use that to determine which shapes to apply undo
|
# of the same shape. Use that to determine which shapes to apply undo
|
||||||
# times to.
|
# times to.
|
||||||
shapes.each do |shape|
|
set_undo_helper(shapes, :shape_unique_id, undo_shape_unique_id, timestamp)
|
||||||
next unless shape[:shape_unique_id] == undo_shape_unique_id
|
|
||||||
|
|
||||||
shape[:undo] = timestamp if (shape_undo = shape[:undo]).nil? || (shape_undo > timestamp)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
BigBlueButton.logger.info('Undo: no applicable shapes found')
|
BigBlueButton.logger.info('Undo: no applicable shapes found')
|
||||||
end
|
end
|
||||||
@ -1101,7 +1099,7 @@ end
|
|||||||
def get_poll_answers(event)
|
def get_poll_answers(event)
|
||||||
answers = []
|
answers = []
|
||||||
unless (answers_event = event.at_xpath('answers')).nil?
|
unless (answers_event = event.at_xpath('answers')).nil?
|
||||||
answers = JSON.parse(answers_event.content)
|
answers = JSON.parse(answers_event.content)
|
||||||
end
|
end
|
||||||
|
|
||||||
answers
|
answers
|
||||||
@ -1110,7 +1108,7 @@ end
|
|||||||
def get_poll_respondents(event)
|
def get_poll_respondents(event)
|
||||||
respondents = 0
|
respondents = 0
|
||||||
unless (num_respondents = event.at_xpath('numRespondents')).nil?
|
unless (num_respondents = event.at_xpath('numRespondents')).nil?
|
||||||
respondents = num_respondents.text.to_i
|
respondents = num_respondents.text.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
respondents
|
respondents
|
||||||
@ -1119,7 +1117,7 @@ end
|
|||||||
def get_poll_responders(event)
|
def get_poll_responders(event)
|
||||||
responders = 0
|
responders = 0
|
||||||
unless (num_responders = event.at_xpath('numResponders')).nil?
|
unless (num_responders = event.at_xpath('numResponders')).nil?
|
||||||
responders = num_responders.text.to_i
|
responders = num_responders.text.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
responders
|
responders
|
||||||
@ -1128,7 +1126,7 @@ end
|
|||||||
def get_poll_id(event)
|
def get_poll_id(event)
|
||||||
id = ''
|
id = ''
|
||||||
unless (poll_id_event = event.at_xpath('pollId')).nil?
|
unless (poll_id_event = event.at_xpath('pollId')).nil?
|
||||||
id = poll_id_event.text
|
id = poll_id_event.text
|
||||||
end
|
end
|
||||||
|
|
||||||
id
|
id
|
||||||
@ -1150,6 +1148,10 @@ def get_poll_type(events, published_poll_event)
|
|||||||
type
|
type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_json_file(package_dir, filename, contents)
|
||||||
|
File.open("#{package_dir}/#{filename}", 'w') { |f| f.puts(contents.to_json) } unless contents.empty?
|
||||||
|
end
|
||||||
|
|
||||||
def process_poll_events(events, package_dir)
|
def process_poll_events(events, package_dir)
|
||||||
BigBlueButton.logger.info('Processing poll events')
|
BigBlueButton.logger.info('Processing poll events')
|
||||||
|
|
||||||
@ -1169,7 +1171,7 @@ def process_poll_events(events, package_dir)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
File.open("#{package_dir}/polls.json", 'w') { |f| f.puts(published_polls.to_json) } unless published_polls.empty?
|
generate_json_file(package_dir, 'polls.json', published_polls)
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_external_video_events(_events, package_dir)
|
def process_external_video_events(_events, package_dir)
|
||||||
@ -1201,7 +1203,24 @@ def process_external_video_events(_events, package_dir)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
File.open("#{package_dir}/external_videos.json", 'w') { |f| f.puts(external_videos.to_json) } unless external_videos.empty?
|
generate_json_file(package_dir, 'external_videos.json', external_videos)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_done_or_fail_file(success)
|
||||||
|
File.open("#{@recording_dir}/status/published/#{@meeting_id}-presentation#{success ? '.done' : '.fail'}", 'w') do |file|
|
||||||
|
file.write("#{success ? 'Published' : 'Failed publishing'} #{@meeting_id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_media_files_helper(media, media_files, package_dir)
|
||||||
|
BigBlueButton.logger.info("Making #{media} dir")
|
||||||
|
FileUtils.mkdir_p(media_dir = "#{package_dir}/#{media}")
|
||||||
|
|
||||||
|
media_files.each do |media_file|
|
||||||
|
BigBlueButton.logger.info("Made #{media} dir - copying: #{media_file} to -> #{media_dir}")
|
||||||
|
FileUtils.cp(media_file, media_dir)
|
||||||
|
BigBlueButton.logger.info("Copied #{File.extname(media_file)} file")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@shapes_svg_filename = 'shapes.svg'
|
@shapes_svg_filename = 'shapes.svg'
|
||||||
@ -1229,21 +1248,21 @@ begin
|
|||||||
BigBlueButton.logger = logger
|
BigBlueButton.logger = logger
|
||||||
|
|
||||||
BigBlueButton.logger.info('Setting recording dir')
|
BigBlueButton.logger.info('Setting recording dir')
|
||||||
recording_dir = bbb_props['recording_dir']
|
@recording_dir = bbb_props['recording_dir']
|
||||||
|
|
||||||
BigBlueButton.logger.info('Setting process dir')
|
BigBlueButton.logger.info('Setting process dir')
|
||||||
@process_dir = "#{recording_dir}/process/presentation/#{@meeting_id}"
|
@process_dir = "#{@recording_dir}/process/presentation/#{@meeting_id}"
|
||||||
|
|
||||||
BigBlueButton.logger.info('Setting publish dir')
|
BigBlueButton.logger.info('Setting publish dir')
|
||||||
publish_dir = @presentation_props['publish_dir']
|
publish_dir = @presentation_props['publish_dir']
|
||||||
|
|
||||||
BigBlueButton.logger.info('Setting playback url info')
|
BigBlueButton.logger.info('Setting playback url info')
|
||||||
playback_protocol = bbb_props['playback_protocol']
|
playback_protocol = bbb_props['playback_protocol']
|
||||||
playback_host = bbb_props['playback_host']
|
playback_host = bbb_props['playback_host']
|
||||||
|
|
||||||
BigBlueButton.logger.info('Setting target dir')
|
BigBlueButton.logger.info('Setting target dir')
|
||||||
target_dir = "#{recording_dir}/publish/presentation/#{@meeting_id}"
|
target_dir = "#{@recording_dir}/publish/presentation/#{@meeting_id}"
|
||||||
@deskshare_dir = "#{recording_dir}/raw/#{@meeting_id}/deskshare"
|
@deskshare_dir = "#{@recording_dir}/raw/#{@meeting_id}/deskshare"
|
||||||
|
|
||||||
if !FileTest.directory?(target_dir)
|
if !FileTest.directory?(target_dir)
|
||||||
BigBlueButton.logger.info('Making dir target_dir')
|
BigBlueButton.logger.info('Making dir target_dir')
|
||||||
@ -1258,27 +1277,21 @@ begin
|
|||||||
|
|
||||||
video_files = Dir.glob("#{@process_dir}/webcams.{#{video_formats.join(',')}}")
|
video_files = Dir.glob("#{@process_dir}/webcams.{#{video_formats.join(',')}}")
|
||||||
if !video_files.empty?
|
if !video_files.empty?
|
||||||
BigBlueButton.logger.info('Making video dir')
|
copy_media_files_helper('video', video_files, package_dir)
|
||||||
video_dir = "#{package_dir}/video"
|
|
||||||
FileUtils.mkdir_p video_dir
|
|
||||||
video_files.each do |video_file|
|
|
||||||
BigBlueButton.logger.info("Made video dir - copying: #{video_file} to -> #{video_dir}")
|
|
||||||
FileUtils.cp(video_file, video_dir)
|
|
||||||
BigBlueButton.logger.info("Copied #{File.extname(video_file)} file")
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
audio_dir = "#{package_dir}/audio"
|
copy_media_files_helper('audio', ["#{@process_dir}/audio.webm", "#{@process_dir}/audio.ogg"], package_dir)
|
||||||
BigBlueButton.logger.info('Making audio dir')
|
end
|
||||||
FileUtils.mkdir_p audio_dir
|
|
||||||
BigBlueButton.logger.info("Made audio dir - copying: #{@process_dir}/audio.webm to -> #{audio_dir}")
|
video_files = Dir.glob("#{@process_dir}/deskshare.{#{video_formats.join(',')}}")
|
||||||
FileUtils.cp("#{@process_dir}/audio.webm", audio_dir)
|
if !video_files.empty?
|
||||||
BigBlueButton.logger.info("Copied audio.webm file - copying: #{@process_dir}/audio.ogg to -> #{audio_dir}")
|
copy_media_files_helper('deskshare', video_files, package_dir)
|
||||||
FileUtils.cp("#{@process_dir}/audio.ogg", audio_dir)
|
else
|
||||||
BigBlueButton.logger.info('Copied audio.ogg file')
|
BigBlueButton.logger.info("Could not copy deskshares.webm: file doesn't exist")
|
||||||
end
|
end
|
||||||
|
|
||||||
if File.exist?("#{@process_dir}/captions.json")
|
if File.exist?("#{@process_dir}/captions.json")
|
||||||
BigBlueButton.logger.info('Copying caption files')
|
BigBlueButton.logger.info('Copying caption files')
|
||||||
|
|
||||||
FileUtils.cp("#{@process_dir}/captions.json", package_dir)
|
FileUtils.cp("#{@process_dir}/captions.json", package_dir)
|
||||||
Dir.glob("#{@process_dir}/caption_*.vtt").each do |caption|
|
Dir.glob("#{@process_dir}/caption_*.vtt").each do |caption|
|
||||||
BigBlueButton.logger.debug(caption)
|
BigBlueButton.logger.debug(caption)
|
||||||
@ -1286,26 +1299,14 @@ begin
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
video_files = Dir.glob("#{@process_dir}/deskshare.{#{video_formats.join(',')}}")
|
if File.exist?(presentation_text = "#{@process_dir}/presentation_text.json")
|
||||||
if !video_files.empty?
|
FileUtils.cp(presentation_text, package_dir)
|
||||||
BigBlueButton.logger.info('Making deskshare dir')
|
|
||||||
deskshare_dir = "#{package_dir}/deskshare"
|
|
||||||
FileUtils.mkdir_p deskshare_dir
|
|
||||||
video_files.each do |video_file|
|
|
||||||
BigBlueButton.logger.info("Made deskshare dir - copying: #{video_file} to -> #{deskshare_dir}")
|
|
||||||
FileUtils.cp(video_file, deskshare_dir)
|
|
||||||
BigBlueButton.logger.info("Copied #{File.extname(video_file)} file")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
BigBlueButton.logger.info("Could not copy deskshares.webm: file doesn't exist")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if File.exist?("#{@process_dir}/presentation_text.json")
|
if File.exist?(notes = "#{@process_dir}/notes/notes.html")
|
||||||
FileUtils.cp("#{@process_dir}/presentation_text.json", package_dir)
|
FileUtils.cp(notes, package_dir)
|
||||||
end
|
end
|
||||||
|
|
||||||
FileUtils.cp("#{@process_dir}/notes/notes.html", package_dir) if File.exist?("#{@process_dir}/notes/notes.html")
|
|
||||||
|
|
||||||
processing_time = File.read("#{@process_dir}/processing_time")
|
processing_time = File.read("#{@process_dir}/processing_time")
|
||||||
|
|
||||||
@doc = Nokogiri::XML(File.read("#{@process_dir}/events.xml"))
|
@doc = Nokogiri::XML(File.read("#{@process_dir}/events.xml"))
|
||||||
@ -1408,7 +1409,7 @@ begin
|
|||||||
FileUtils.mkdir_p publish_dir unless FileTest.directory?(publish_dir)
|
FileUtils.mkdir_p publish_dir unless FileTest.directory?(publish_dir)
|
||||||
|
|
||||||
# Get raw size of presentation files
|
# Get raw size of presentation files
|
||||||
raw_dir = "#{recording_dir}/raw/#{@meeting_id}"
|
raw_dir = "#{@recording_dir}/raw/#{@meeting_id}"
|
||||||
# After all the processing we'll add the published format and raw sizes to the metadata file
|
# After all the processing we'll add the published format and raw sizes to the metadata file
|
||||||
BigBlueButton.add_raw_size_to_metadata(package_dir, raw_dir)
|
BigBlueButton.add_raw_size_to_metadata(package_dir, raw_dir)
|
||||||
BigBlueButton.add_playback_size_to_metadata(package_dir)
|
BigBlueButton.add_playback_size_to_metadata(package_dir)
|
||||||
@ -1428,10 +1429,7 @@ begin
|
|||||||
end
|
end
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
File.open("#{recording_dir}/status/published/#{@meeting_id}-presentation.done", 'w') do |file|
|
generate_done_or_fail_file(true)
|
||||||
file.write("Published #{@meeting_id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
else
|
||||||
BigBlueButton.logger.info("#{target_dir} is already there")
|
BigBlueButton.logger.info("#{target_dir} is already there")
|
||||||
end
|
end
|
||||||
@ -1441,9 +1439,7 @@ rescue StandardError => e
|
|||||||
e.backtrace.each do |traceline|
|
e.backtrace.each do |traceline|
|
||||||
BigBlueButton.logger.error(traceline)
|
BigBlueButton.logger.error(traceline)
|
||||||
end
|
end
|
||||||
File.open("#{recording_dir}/status/published/#{@meeting_id}-presentation.fail", 'w') do |file|
|
generate_done_or_fail_file(false)
|
||||||
file.write("Failed Publishing #{@meeting_id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user