railsquest 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +26 -0
- data/Readme.md +134 -0
- data/bin/railsquest +61 -0
- data/development.log +1 -0
- data/lib/railsquest.rb +119 -0
- data/lib/railsquest/badge.rb +84 -0
- data/lib/railsquest/bonjour.rb +9 -0
- data/lib/railsquest/bonjour/advertiser.rb +66 -0
- data/lib/railsquest/bonjour/browser.rb +58 -0
- data/lib/railsquest/bonjour/person.rb +25 -0
- data/lib/railsquest/bonjour/quest.rb +39 -0
- data/lib/railsquest/bonjour/quest_browser.rb +36 -0
- data/lib/railsquest/bonjour/railsquest_browser.rb +32 -0
- data/lib/railsquest/commands.rb +53 -0
- data/lib/railsquest/grit_extensions.rb +11 -0
- data/lib/railsquest/helpers.rb +100 -0
- data/lib/railsquest/quest.rb +86 -0
- data/lib/railsquest/version.rb +3 -0
- data/railsquest.gemspec +21 -0
- data/sinatra/app.rb +162 -0
- data/sinatra/config.ru +2 -0
- data/sinatra/lib/diff_helpers.rb +92 -0
- data/sinatra/lib/mock_browsers.rb +55 -0
- data/sinatra/public/jquery-1.3.2.min.js +19 -0
- data/sinatra/public/loader.gif +0 -0
- data/sinatra/public/logo.png +0 -0
- data/sinatra/public/pbjt.swf +0 -0
- data/sinatra/public/peanut.png +0 -0
- data/sinatra/public/rm.gif +0 -0
- data/sinatra/public/theme.mp3 +0 -0
- data/sinatra/views/home.haml +32 -0
- data/sinatra/views/layout.haml +346 -0
- data/sinatra/views/readme.haml +52 -0
- data/sinatra/views/run.haml +1 -0
- data/sinatra/views/user.haml +21 -0
- metadata +103 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'dnssd'
|
2
|
+
|
3
|
+
class Railsquest::Bonjour::Advertiser
|
4
|
+
def initialize
|
5
|
+
@services = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def go!
|
9
|
+
register_app
|
10
|
+
register_quests
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def register_app
|
15
|
+
STDOUT.puts "Registering #{Railsquest.web_uri}"
|
16
|
+
tr = DNSSD::TextRecord.new
|
17
|
+
tr["name"] = Railsquest.config.name
|
18
|
+
tr["email"] = Railsquest.config.email
|
19
|
+
tr["uri"] = Railsquest.web_uri
|
20
|
+
tr["gravatar"] = Railsquest.gravatar
|
21
|
+
tr["version"] = Railsquest::VERSION
|
22
|
+
DNSSD.register("#{Railsquest.config.name}'s railsquest", "_http._tcp,_railsquest", nil, Railsquest.web_port, tr)
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_quests
|
26
|
+
loop do
|
27
|
+
stop_old_services
|
28
|
+
register_new_quests
|
29
|
+
sleep(1)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop_old_services
|
34
|
+
old_services.each do |old_service|
|
35
|
+
STDOUT.puts "Unregistering #{old_service.quest.uri}"
|
36
|
+
old_service.stop
|
37
|
+
@services.delete(old_service)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def old_services
|
42
|
+
@services.reject {|s| Railsquest.quests.include?(s.quest)}
|
43
|
+
end
|
44
|
+
|
45
|
+
def register_new_quests
|
46
|
+
new_quests.each do |new_quest|
|
47
|
+
STDOUT.puts "Registering #{new_quest.uri}"
|
48
|
+
tr = DNSSD::TextRecord.new
|
49
|
+
tr["name"] = new_quest.name
|
50
|
+
tr["uri"] = new_quest.uri
|
51
|
+
tr["bjour-name"] = Railsquest.config.name
|
52
|
+
tr["bjour-email"] = Railsquest.config.email
|
53
|
+
tr["bjour-uri"] = Railsquest.web_uri
|
54
|
+
tr["bjour-gravatar"] = Railsquest.gravatar
|
55
|
+
tr["bjour-version"] = Railsquest::VERSION
|
56
|
+
service = DNSSD.register(new_quest.name, "_git._tcp,_railsquest", nil, 9877, tr)
|
57
|
+
service.class.instance_eval { attr_accessor(:quest) }
|
58
|
+
service.quest = new_quest
|
59
|
+
@services << service
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def new_quests
|
64
|
+
Railsquest.quests.select {|quest| !@services.any? {|s| s.quest == quest } }
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'dnssd'
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
Thread.abort_on_exception = true
|
7
|
+
|
8
|
+
# Generic bonjour browser
|
9
|
+
#
|
10
|
+
# Example use:
|
11
|
+
#
|
12
|
+
# browser = BonjourBrowser.new("_git._tcp,_railsquest")
|
13
|
+
# loop do
|
14
|
+
# sleep(1)
|
15
|
+
# pp browser.replies.map {|r| r.name}
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Probably gem-worthy
|
19
|
+
class Railsquest::Bonjour::Browser
|
20
|
+
def initialize(service)
|
21
|
+
@service = service
|
22
|
+
@mutex = Mutex.new
|
23
|
+
@replies = {}
|
24
|
+
watch!
|
25
|
+
end
|
26
|
+
|
27
|
+
def replies
|
28
|
+
@mutex.synchronize { @replies.values }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def watch!
|
33
|
+
Thread.new(@service, @mutex, @replies) do |service, mutex, replies|
|
34
|
+
begin
|
35
|
+
DNSSD.browse!(service) do |reply|
|
36
|
+
Thread.new(reply, replies, mutex) do |reply, replies, mutex|
|
37
|
+
begin
|
38
|
+
DNSSD.resolve!(reply.name, reply.type, reply.domain) do |resolve_reply|
|
39
|
+
mutex.synchronize do
|
40
|
+
if reply.flags.add?
|
41
|
+
replies[reply.fullname] = resolve_reply
|
42
|
+
else
|
43
|
+
replies.delete(reply.fullname)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
resolve_reply.service.stop unless resolve_reply.service.stopped?
|
47
|
+
end
|
48
|
+
rescue DNSSD::BadParamError
|
49
|
+
# Ignore em
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue DNSSD::BadParamError
|
54
|
+
# Ignore em
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Railsquest::Bonjour::Person
|
2
|
+
|
3
|
+
attr_accessor :name, :email, :uri , :gravatar
|
4
|
+
|
5
|
+
def initialize(name, email, uri, gravatar)
|
6
|
+
@name, @email, @uri, @gravatar = name, email, uri, gravatar
|
7
|
+
end
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
self.uri == other.uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def hash
|
14
|
+
to_hash.hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{"name" => name, "email" => email, "uri" => uri, "gravatar" => gravatar}
|
19
|
+
end
|
20
|
+
|
21
|
+
def host_name
|
22
|
+
uri.gsub(/http:\/\//, '').gsub(/:9876/, '')
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Railsquest::Bonjour::Quest
|
2
|
+
|
3
|
+
attr_accessor :name, :uri, :person
|
4
|
+
|
5
|
+
def initialize(name, uri, person)
|
6
|
+
@name, @uri, @person = name, uri, person
|
7
|
+
end
|
8
|
+
|
9
|
+
def html_id
|
10
|
+
Railsquest::Quest.html_id(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
self.uri == other.uri
|
15
|
+
end
|
16
|
+
|
17
|
+
def hash
|
18
|
+
to_hash.hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def json_uri
|
22
|
+
"#{person.uri}#{name}.json"
|
23
|
+
end
|
24
|
+
|
25
|
+
def web_uri
|
26
|
+
"#{person.uri}##{html_id}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
{
|
31
|
+
"name" => name,
|
32
|
+
"uri" => uri,
|
33
|
+
"json_uri" => json_uri,
|
34
|
+
"web_uri" => web_uri,
|
35
|
+
"person" => person.to_hash
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Railsquest::Bonjour
|
2
|
+
class QuestBrowser
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@browser = Browser.new('_git._tcp,_railsquest')
|
6
|
+
end
|
7
|
+
|
8
|
+
def quests
|
9
|
+
@browser.replies.map do |reply|
|
10
|
+
Quest.new(
|
11
|
+
reply.text_record["name"],
|
12
|
+
reply.text_record["uri"],
|
13
|
+
Person.new(
|
14
|
+
reply.text_record["bjour-name"],
|
15
|
+
reply.text_record["bjour-email"],
|
16
|
+
reply.text_record["bjour-uri"],
|
17
|
+
reply.text_record["bjour-gravatar"]
|
18
|
+
)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def other_quests
|
24
|
+
quests.reject {|r| Railsquest.quests.any? {|my_quest| my_quest.name == r.name}}
|
25
|
+
end
|
26
|
+
|
27
|
+
def quests_similar_to(quest)
|
28
|
+
quests.select {|r| r.name == quest.name}
|
29
|
+
end
|
30
|
+
|
31
|
+
def quests_for(person)
|
32
|
+
quests.select {|r| r.person == person}
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Railsquest::Bonjour
|
2
|
+
class RailsquestBrowser
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@browser = Browser.new('_http._tcp,_railsquest')
|
6
|
+
end
|
7
|
+
|
8
|
+
def railsquests
|
9
|
+
@browser.replies.map do |reply|
|
10
|
+
Person.new(
|
11
|
+
reply.text_record["name"],
|
12
|
+
reply.text_record["email"],
|
13
|
+
reply.text_record["uri"],
|
14
|
+
reply.text_record["gravatar"]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def other_railsquests
|
20
|
+
railsquests.reject {|b| b.uri == Railsquest.web_uri}
|
21
|
+
end
|
22
|
+
|
23
|
+
def all_railsquests
|
24
|
+
railsquests
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_person(hostname)
|
28
|
+
railsquests.find { |u| u.uri =~ /hostname/}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Railsquest::Commands
|
5
|
+
|
6
|
+
# Start sinatra app.
|
7
|
+
def serve_web!
|
8
|
+
puts "* Starting " + web_uri.foreground(:yellow)
|
9
|
+
fork do
|
10
|
+
ENV["RACK_ENV"] ||= "production"
|
11
|
+
require "railsquest/../../sinatra/app"
|
12
|
+
Sinatra::Application.set :port, web_port
|
13
|
+
Sinatra::Application.set :server, "thin"
|
14
|
+
Sinatra::Application.run!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def advertise!
|
19
|
+
fork { Railsquest::Bonjour::Advertiser.new.go! }
|
20
|
+
end
|
21
|
+
|
22
|
+
def add!(port, name = nil)
|
23
|
+
|
24
|
+
if name.nil?
|
25
|
+
default_name = "My Awesome Quest"
|
26
|
+
print "Quest Name?".foreground(:yellow) + " [#{default_name}] "
|
27
|
+
name = (STDIN.gets || "").strip
|
28
|
+
name = default_name if name.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
quest = Railsquest::Quest.for_name(name)
|
32
|
+
quest.port = port
|
33
|
+
|
34
|
+
if quest.exist?
|
35
|
+
abort "You've already a quest #{quest}."
|
36
|
+
end
|
37
|
+
|
38
|
+
quest.init!
|
39
|
+
|
40
|
+
puts init_success_message(quest.dirname)
|
41
|
+
|
42
|
+
quest
|
43
|
+
end
|
44
|
+
|
45
|
+
def init_success_message(quest_dirname)
|
46
|
+
plain_init_success_message(quest_dirname)
|
47
|
+
end
|
48
|
+
|
49
|
+
def plain_init_success_message(quest_dirname)
|
50
|
+
"Railsquest quest #{quest_dirname} created."
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Railsquest
|
4
|
+
module GravatarHelpers
|
5
|
+
def gravatar
|
6
|
+
gravatar_uri(self.config.email)
|
7
|
+
end
|
8
|
+
def gravatar_uri(email)
|
9
|
+
"http://gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}.png"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Lifted from Rails
|
14
|
+
module DateHelpers
|
15
|
+
# Reports the approximate distance in time between two Time or Date objects or integers as seconds.
|
16
|
+
# Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs
|
17
|
+
# Distances are questrted based on the following table:
|
18
|
+
#
|
19
|
+
# 0 <-> 29 secs # => less than a minute
|
20
|
+
# 30 secs <-> 1 min, 29 secs # => 1 minute
|
21
|
+
# 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
|
22
|
+
# 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
|
23
|
+
# 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
|
24
|
+
# 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day
|
25
|
+
# 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
|
26
|
+
# 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
|
27
|
+
# 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
|
28
|
+
# 1 yr <-> 2 yrs minus 1 secs # => about 1 year
|
29
|
+
# 2 yrs <-> max time or date # => over [2..X] years
|
30
|
+
#
|
31
|
+
# With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds:
|
32
|
+
# 0-4 secs # => less than 5 seconds
|
33
|
+
# 5-9 secs # => less than 10 seconds
|
34
|
+
# 10-19 secs # => less than 20 seconds
|
35
|
+
# 20-39 secs # => half a minute
|
36
|
+
# 40-59 secs # => less than a minute
|
37
|
+
# 60-89 secs # => 1 minute
|
38
|
+
#
|
39
|
+
# ==== Examples
|
40
|
+
# from_time = Time.now
|
41
|
+
# distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
|
42
|
+
# distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
|
43
|
+
# distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
|
44
|
+
# distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
|
45
|
+
# distance_of_time_in_words(from_time, 3.years.from_now) # => over 3 years
|
46
|
+
# distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days
|
47
|
+
# distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute
|
48
|
+
# distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
|
49
|
+
# distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
|
50
|
+
# distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
|
51
|
+
# distance_of_time_in_words(from_time, from_time + 4.years + 15.days + 30.minutes + 5.seconds) # => over 4 years
|
52
|
+
#
|
53
|
+
# to_time = Time.now + 6.years + 19.days
|
54
|
+
# distance_of_time_in_words(from_time, to_time, true) # => over 6 years
|
55
|
+
# distance_of_time_in_words(to_time, from_time, true) # => over 6 years
|
56
|
+
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
|
57
|
+
#
|
58
|
+
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
|
59
|
+
from_time = from_time.to_time if from_time.respond_to?(:to_time)
|
60
|
+
to_time = to_time.to_time if to_time.respond_to?(:to_time)
|
61
|
+
distance_in_minutes = (((to_time - from_time).abs)/60).round
|
62
|
+
distance_in_seconds = ((to_time - from_time).abs).round
|
63
|
+
|
64
|
+
case distance_in_minutes
|
65
|
+
when 0..1
|
66
|
+
return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
|
67
|
+
case distance_in_seconds
|
68
|
+
when 0..4 then 'less than 5 seconds'
|
69
|
+
when 5..9 then 'less than 10 seconds'
|
70
|
+
when 10..19 then 'less than 20 seconds'
|
71
|
+
when 20..39 then 'half a minute'
|
72
|
+
when 40..59 then 'less than a minute'
|
73
|
+
else '1 minute'
|
74
|
+
end
|
75
|
+
|
76
|
+
when 2..44 then "#{distance_in_minutes} minutes"
|
77
|
+
when 45..89 then 'about 1 hour'
|
78
|
+
when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
|
79
|
+
when 1440..2879 then '1 day'
|
80
|
+
when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
|
81
|
+
when 43200..86399 then 'about 1 month'
|
82
|
+
when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
|
83
|
+
when 525600..1051199 then 'about 1 year'
|
84
|
+
else "over #{(distance_in_minutes / 525600).round} years"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
|
89
|
+
#
|
90
|
+
# ==== Examples
|
91
|
+
# time_ago_in_words(3.minutes.from_now) # => 3 minutes
|
92
|
+
# time_ago_in_words(Time.now - 15.hours) # => 15 hours
|
93
|
+
# time_ago_in_words(Time.now) # => less than a minute
|
94
|
+
#
|
95
|
+
# from_time = Time.now - 3.days - 14.minutes - 25.seconds # => 3 days
|
96
|
+
def time_ago_in_words(from_time, include_seconds = false)
|
97
|
+
distance_of_time_in_words(from_time, Time.now, include_seconds)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'grit'
|
2
|
+
require 'pathname'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Railsquest
|
6
|
+
class Quest
|
7
|
+
|
8
|
+
attr_accessor :name, :port
|
9
|
+
attr_reader :secret
|
10
|
+
|
11
|
+
def self.for_name(name)
|
12
|
+
n = name.gsub(/[^A-Za-z-]+/, '_')
|
13
|
+
q = new(Railsquest.quests_path.join(n + ".quest"))
|
14
|
+
q.name = n
|
15
|
+
q
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.html_id(name)
|
19
|
+
name.gsub(/[^A-Za-z-]+/, '').downcase
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(path)
|
23
|
+
@path = Pathname(path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def ==(other)
|
27
|
+
other.respond_to?(:path) && self.path == other.path
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :path
|
31
|
+
|
32
|
+
def exist?
|
33
|
+
path.exist?
|
34
|
+
end
|
35
|
+
|
36
|
+
def init!
|
37
|
+
secret = `uuidgen`.strip
|
38
|
+
contents = '{\"secret\" : \"' + secret + '\", \"port\" : ' + port + '}'
|
39
|
+
Dir.chdir(Railsquest.quests_path) { `echo "#{contents}" >> #{path}` }
|
40
|
+
end
|
41
|
+
|
42
|
+
def uri
|
43
|
+
Railsquest.quest_uri.gsub(/\/$/, '') + ':' + File.open(path) { |f| JSON.parse(f.gets)["port"].to_s }
|
44
|
+
end
|
45
|
+
|
46
|
+
def secret
|
47
|
+
File.open(path) { |f| JSON.parse(f.gets)["secret"] }
|
48
|
+
end
|
49
|
+
|
50
|
+
def name
|
51
|
+
dirname.sub(".quest",'')
|
52
|
+
end
|
53
|
+
|
54
|
+
def html_id
|
55
|
+
self.class.html_id(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def dirname
|
59
|
+
path.split.last.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
name
|
64
|
+
end
|
65
|
+
|
66
|
+
def web_uri
|
67
|
+
Railsquest.web_uri + "#" + html_id
|
68
|
+
end
|
69
|
+
|
70
|
+
def attempts
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
def remove!
|
75
|
+
path.rmtree
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_hash
|
79
|
+
{
|
80
|
+
"name" => name,
|
81
|
+
"uri" => uri,
|
82
|
+
"host_name" => Railsquest.host_name
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|