hipbot 1.0.0.rc2 → 1.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +6 -0
  4. data/Gemfile +1 -2
  5. data/README.md +482 -88
  6. data/Rakefile +1 -1
  7. data/bin/hipbot +4 -4
  8. data/hipbot.gemspec +9 -9
  9. data/lib/hipbot.rb +28 -11
  10. data/lib/hipbot/adapter.rb +80 -0
  11. data/lib/hipbot/adapters/hipchat.rb +53 -0
  12. data/lib/hipbot/adapters/hipchat/initializer.rb +83 -0
  13. data/lib/hipbot/adapters/shell.rb +37 -0
  14. data/lib/hipbot/adapters/telnet.rb +41 -0
  15. data/lib/hipbot/bot.rb +15 -52
  16. data/lib/hipbot/cache.rb +23 -0
  17. data/lib/hipbot/callbacks/base.rb +15 -0
  18. data/lib/hipbot/callbacks/invite.rb +11 -0
  19. data/lib/hipbot/callbacks/lobby_presence.rb +13 -0
  20. data/lib/hipbot/callbacks/message.rb +11 -0
  21. data/lib/hipbot/callbacks/presence.rb +17 -0
  22. data/lib/hipbot/callbacks/private_message.rb +12 -0
  23. data/lib/hipbot/callbacks/room_message.rb +21 -0
  24. data/lib/hipbot/callbacks/room_presence.rb +28 -0
  25. data/lib/hipbot/configurable.rb +22 -0
  26. data/lib/hipbot/configuration.rb +9 -3
  27. data/lib/hipbot/helpers.rb +4 -40
  28. data/lib/hipbot/http.rb +65 -0
  29. data/lib/hipbot/match.rb +21 -9
  30. data/lib/hipbot/matchable.rb +38 -0
  31. data/lib/hipbot/message.rb +24 -9
  32. data/lib/hipbot/plugin.rb +3 -3
  33. data/lib/hipbot/reactable.rb +14 -21
  34. data/lib/hipbot/reaction.rb +27 -15
  35. data/lib/hipbot/reaction_factory.rb +38 -0
  36. data/lib/hipbot/response.rb +18 -10
  37. data/lib/hipbot/room.rb +34 -2
  38. data/lib/hipbot/storages/base.rb +78 -0
  39. data/lib/hipbot/storages/hash.rb +89 -0
  40. data/lib/hipbot/storages/mongoid.rb +18 -0
  41. data/lib/hipbot/user.rb +8 -2
  42. data/lib/hipbot/version.rb +1 -1
  43. data/spec/integration/my_hipbot.rb +24 -4
  44. data/spec/integration/{hipbot_spec.rb → my_hipbot_spec.rb} +41 -23
  45. data/spec/spec_helper.rb +14 -2
  46. data/spec/unit/adapters/hipchat_spec.rb +5 -0
  47. data/spec/unit/{hipbot_spec.rb → bot_spec.rb} +13 -12
  48. data/spec/unit/match_spec.rb +148 -0
  49. data/spec/unit/message_spec.rb +14 -7
  50. data/spec/unit/reaction_factory_spec.rb +54 -0
  51. data/spec/unit/reaction_spec.rb +99 -0
  52. data/spec/unit/storages/hash_spec.rb +75 -0
  53. data/spec/unit/user_spec.rb +0 -2
  54. metadata +64 -54
  55. data/Gemfile.lock +0 -90
  56. data/examples/cleverbot.rb +0 -23
  57. data/examples/google_images.rb +0 -35
  58. data/lib/hipbot/adapters/hipchat/connection.rb +0 -166
  59. data/lib/hipbot/adapters/hipchat/hipchat.rb +0 -13
  60. data/lib/hipbot/adapters/telnet/connection.rb +0 -17
  61. data/lib/hipbot/adapters/telnet/telnet.rb +0 -14
  62. data/lib/hipbot/collection.rb +0 -72
  63. data/lib/hipbot/patches/hipchat_client.rb +0 -230
@@ -1,90 +0,0 @@
1
- GEM
2
- remote: https://rubygems.org/
3
- specs:
4
- activesupport (3.2.12)
5
- i18n (~> 0.6)
6
- multi_json (~> 1.0)
7
- addressable (2.3.3)
8
- coderay (1.0.9)
9
- colorize (0.5.8)
10
- cookiejar (0.3.0)
11
- coveralls (0.6.7)
12
- colorize
13
- multi_json (~> 1.3)
14
- rest-client
15
- simplecov (>= 0.7)
16
- thor
17
- daemons (1.1.9)
18
- diff-lcs (1.2.2)
19
- em-http-request (1.0.3)
20
- addressable (>= 2.2.3)
21
- cookiejar
22
- em-socksify
23
- eventmachine (>= 1.0.0.beta.4)
24
- http_parser.rb (>= 0.5.3)
25
- em-socksify (0.2.1)
26
- eventmachine (>= 1.0.0.beta.4)
27
- eventmachine (1.0.3)
28
- formatador (0.2.4)
29
- guard (1.7.0)
30
- formatador (>= 0.2.4)
31
- listen (>= 0.6.0)
32
- lumberjack (>= 1.0.2)
33
- pry (>= 0.9.10)
34
- thor (>= 0.14.6)
35
- guard-rspec (2.5.2)
36
- guard (>= 1.1)
37
- rspec (~> 2.11)
38
- http_parser.rb (0.5.3)
39
- httparty (0.10.2)
40
- multi_json (~> 1.0)
41
- multi_xml (>= 0.5.2)
42
- i18n (0.6.4)
43
- listen (0.7.3)
44
- lumberjack (1.0.3)
45
- metaclass (0.0.1)
46
- method_source (0.8.1)
47
- mime-types (1.23)
48
- mocha (0.13.3)
49
- metaclass (~> 0.0.1)
50
- multi_json (1.7.2)
51
- multi_xml (0.5.3)
52
- pry (0.9.12)
53
- coderay (~> 1.0.5)
54
- method_source (~> 0.8)
55
- slop (~> 3.4)
56
- rake (10.0.4)
57
- rest-client (1.6.7)
58
- mime-types (>= 1.16)
59
- rspec (2.13.0)
60
- rspec-core (~> 2.13.0)
61
- rspec-expectations (~> 2.13.0)
62
- rspec-mocks (~> 2.13.0)
63
- rspec-core (2.13.1)
64
- rspec-expectations (2.13.0)
65
- diff-lcs (>= 1.1.3, < 2.0)
66
- rspec-mocks (2.13.0)
67
- simplecov (0.7.1)
68
- multi_json (~> 1.0)
69
- simplecov-html (~> 0.7.1)
70
- simplecov-html (0.7.1)
71
- slop (3.4.4)
72
- thor (0.18.1)
73
- xmpp4r (0.5)
74
-
75
- PLATFORMS
76
- ruby
77
-
78
- DEPENDENCIES
79
- activesupport
80
- coveralls
81
- daemons
82
- em-http-request
83
- eventmachine
84
- guard-rspec
85
- httparty
86
- i18n
87
- mocha
88
- rake
89
- rspec
90
- xmpp4r
@@ -1,23 +0,0 @@
1
- # example bot that simulates intelligence
2
- # using http://cleverbot.com/ api
3
-
4
- require 'hipbot'
5
- require 'cleverbot'
6
- require 'htmlentities'
7
-
8
- class CleverHipbot < Hipbot::Bot
9
- configure do |c|
10
- c.jid = ENV['HIPBOT_JID']
11
- c.password = ENV['HIPBOT_PASSWORD']
12
- c.name = ENV['HIPBOT_NAME']
13
- end
14
-
15
- cleverbot = ::Cleverbot::Client.new
16
-
17
- on /(.+)/ do |message|
18
- coder = HTMLEntities.new
19
- reply(coder.decode(cleverbot.write(message)))
20
- end
21
- end
22
-
23
- CleverHipbot.start!
@@ -1,35 +0,0 @@
1
- # Pull out a random image from the Google Images API and display it
2
- # Uses: google_image_api http://rubygems.org/gems/google_image_api
3
- # Author Rushi Vishavadia <rushi.v@gmail.com>
4
-
5
- require 'hipbot'
6
- require 'google_image_api'
7
-
8
- class SampleBot < Hipbot::Bot
9
- configure do |c|
10
- c.jid = ENV['HIPBOT_JID']
11
- c.password = ENV['HIPBOT_PASSWORD']
12
- c.name = ENV['HIPBOT_NAME']
13
- end
14
-
15
- on /\Aimage (.+)/i do |img_str|
16
- max = 8 # max number of results you want to pull a random
17
- puts img_str
18
- begin
19
- results = GoogleImageApi.find(img_str, :rsz => max)
20
- if results.raw_data["responseStatus"] == 200 and results.images.size > 0
21
- reply(results.images.take(max).sample['unescapedUrl'])
22
- elsif results.raw_data["responseStatus"] == 200 and results.images.size == 0
23
- reply("I'm sorry I couldn't find an image for #{img_str}")
24
- else
25
- reply("I'm sorry, an error occurred. Try again please") # Most likely a 403
26
- end
27
- rescue => e
28
- reply("I'm sorry, an error occurred trying to find that image")
29
- p e.message
30
- p e.backtrace
31
- end
32
- end
33
- end
34
-
35
- SampleBot.start!
@@ -1,166 +0,0 @@
1
- module Hipbot
2
- module Adapters
3
- module Hipchat
4
- class Connection
5
- def initialize
6
- Hipbot.connection = self
7
- setup_error_handler && setup_bot && setup_timers
8
- end
9
-
10
- def restart!
11
- exit_all_rooms # TODO: Nice quit
12
- setup_bot
13
- end
14
-
15
- def send_to_room(room, message)
16
- Hipbot.logger.info("REPLY in #{room}: #{message}")
17
- @client.send_message(:groupchat, room.id, message)
18
- end
19
-
20
- def send_to_user(user, message)
21
- Hipbot.logger.info("REPLY to #{user}: #{message}")
22
- @client.send_message(:chat, user.id, message)
23
- end
24
-
25
- def set_topic(room, topic)
26
- Hipbot.logger.info("TOPIC set in #{room} to '#{topic}'")
27
- @client.send_message(:groupchat, room.id, nil, topic)
28
- end
29
-
30
- def set_presence(status = nil, type = :available)
31
- Hipbot.logger.info("PRESENCE set to #{type} with '#{status}'")
32
- @client.set_presence(type, nil, status)
33
- end
34
-
35
- protected
36
-
37
- def setup_bot
38
- initialize_client do
39
- initialize_rooms
40
- initialize_users
41
- initialize_callbacks
42
- set_bot_user
43
- join_rooms
44
- set_presence('Hello humans!')
45
- end
46
- end
47
-
48
- def initialize_client
49
- @client = ::Jabber::MUC::HipchatClient.new(Hipbot.jid)
50
- yield if @client.connect(Hipbot.password)
51
- end
52
-
53
- def initialize_rooms
54
- @client.get_rooms.each do |room_data|
55
- room = Room.find_or_create_by(id: room_data[:item].jid.to_s)
56
- room.update_attributes({
57
- name: room_data[:item].iname,
58
- topic: room_data[:details]['topic'],
59
- })
60
- end
61
- end
62
-
63
- def initialize_users
64
- @client.get_users.each do |user_data|
65
- user = User.find_or_create_by(id: user_data.delete(:jid))
66
- user.update_attributes(user_data)
67
-
68
- if user.attributes['email'].nil?
69
- user.update_attributes(@client.get_user_details(user.id))
70
- end
71
- end
72
- end
73
-
74
- def set_bot_user
75
- Hipbot.configuration.user = User[Hipbot.jid]
76
- @client.name = Hipbot.user
77
- end
78
-
79
- def join_rooms
80
- with_rooms do |room|
81
- @client.join(room.id)
82
- end
83
- end
84
-
85
- def exit_all_rooms
86
- with_rooms do |room|
87
- @client.exit(room.id, 'bye bye!')
88
- end
89
- end
90
-
91
- def initialize_callbacks
92
- @client.on_message{ |*args| message_callback *args }
93
- @client.on_private_message{ |*args| private_message_callback *args }
94
- @client.on_invite{ |*args| invite_callback *args }
95
- @client.on_presence{ |*args| presence_callback *args }
96
- @client.activate_callbacks
97
- end
98
-
99
- def message_callback room_jid, user_name, message
100
- with_sender(room_jid, user_name) do |room, user|
101
- room.update_attribute(:topic, message.subject) if message.subject.present?
102
- return if user_name == Hipbot.name || message.body.blank?
103
- Hipbot.react(user, room, message.body)
104
- end
105
- end
106
-
107
- def invite_callback room_jid, user_name, room_name, topic
108
- Room.create(id: room_jid, name: room_name, topic: topic)
109
- @client.join(room_jid)
110
- end
111
-
112
- def presence_callback room_jid, user_name, pres
113
- with_sender(room_jid, user_name) do |room, user|
114
- if pres == 'unavailable'
115
- if user_name == Hipbot.name
116
- room.delete
117
- elsif user.present?
118
- room.users.delete(user)
119
- end
120
- elsif pres.blank? && room.users.exclude?(user)
121
- room.users << user
122
- end
123
- end
124
- end
125
-
126
- def private_message_callback user_jid, message
127
- with_user(user_jid) do |user|
128
- Hipbot.react(user, nil, message.body) if user.name != Hipbot.name
129
- end if message.body.present?
130
- end
131
-
132
- def with_rooms
133
- return Hipbot.logger.error 'No rooms found' if Room.empty?
134
- Room.each{ |room| yield room }
135
- end
136
-
137
- def with_sender room_id, user_id
138
- room = Room[room_id]
139
- with_user(user_id) do |user|
140
- yield room, user
141
- end if room.present?
142
- end
143
-
144
- def with_user user_id
145
- user = User[user_id]
146
- yield user if user
147
- end
148
-
149
- def setup_timers
150
- ::EM::add_periodic_timer(60) do
151
- @client.keep_alive(Hipbot.password) if @client.present?
152
- end
153
- end
154
-
155
- def setup_error_handler
156
- ::EM.error_handler do |e|
157
- Hipbot.logger.error e.inspect
158
- e.backtrace.each do |line|
159
- Hipbot.logger.error line
160
- end
161
- end
162
- end
163
- end
164
- end
165
- end
166
- end
@@ -1,13 +0,0 @@
1
- module Hipbot
2
- module Adapters
3
- module Hipchat
4
- def start!
5
- connection = Connection.new
6
- end
7
-
8
- def method_missing(sym, *args, &block)
9
- connection.send sym, *args, &block
10
- end
11
- end
12
- end
13
- end
@@ -1,17 +0,0 @@
1
- module Hipbot
2
- module Adapters
3
- module Telnet
4
- class Connection < EM::Connection
5
- def initialize
6
- Hipbot.connection = self
7
- end
8
-
9
- def receive_data(data)
10
- sender, room, message = data.strip.split(':')
11
- Hipbot.react(sender, room, message)
12
- end
13
- end
14
- end
15
- end
16
- end
17
-
@@ -1,14 +0,0 @@
1
- module Hipbot
2
- module Adapters
3
- module Telnet
4
- def send_to_room room, message
5
- connection.send_data("#{self}:#{room}:#{message}\n")
6
- end
7
-
8
- def start!
9
- ::EM::connect('0.0.0.0', 3001, Connection, self)
10
- end
11
- end
12
- end
13
- end
14
-
@@ -1,72 +0,0 @@
1
- module Hipbot
2
- module Collection
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- extend ClassMethods
7
-
8
- attr_accessor :id, :name, :attributes
9
- alias_method :to_s, :name
10
- end
11
-
12
- def initialize params
13
- params = params.with_indifferent_access
14
- self.id = params.delete(:id)
15
- self.name = params.delete(:name)
16
- self.attributes = params
17
- end
18
-
19
- def update_attribute key, value
20
- if key == :name
21
- self.name = value
22
- else
23
- self.attributes[key] = value
24
- end
25
- end
26
-
27
- def update_attributes hash
28
- hash.each do |k, v|
29
- update_attribute k, v
30
- end
31
- end
32
-
33
- def delete
34
- self.class.collection.delete(self.id)
35
- end
36
-
37
- module ClassMethods
38
- def create params, &block
39
- collection[params[:id]] = new(params, &block)
40
- end
41
-
42
- def collection
43
- @collection ||= {}
44
- end
45
-
46
- def [] *items
47
- items.first.is_a?(Array) ? find_many(*items) : find_one(items.first)
48
- end
49
-
50
- def find_one item
51
- collection[item] || find{ |i| i.name == item }
52
- end
53
-
54
- def find_many *items
55
- items.flatten!
56
- items.map{ |i| find_one(i) }.compact.uniq
57
- end
58
-
59
- def find_or_create_by params
60
- find_one(params[:id] || params[:name]) || create(params)
61
- end
62
-
63
- protected
64
-
65
- def method_missing name, *args, &block
66
- return collection.values.public_send(name, *args, &block) if Array.instance_methods.include?(name)
67
- super
68
- end
69
- end
70
- end
71
- end
72
-
@@ -1,230 +0,0 @@
1
- require 'xmpp4r/muc/x/muc'
2
- require 'xmpp4r/muc/iq/mucowner'
3
- require 'xmpp4r/muc/iq/mucadmin'
4
- require 'xmpp4r/dataforms'
5
- require 'xmpp4r/roster'
6
- require 'xmpp4r/vcard'
7
-
8
- module Jabber
9
- module MUC
10
- class HipchatClient
11
-
12
- def initialize(jid)
13
- @my_jid = JID.new(jid)
14
-
15
- @stream = Client.new(@my_jid.strip) # TODO: Error Handling
16
- Jabber::debuglog "Stream initialized"
17
- @chat_domain = @my_jid.domain
18
-
19
- @presence_cbs = CallbackList.new
20
- @message_cbs = CallbackList.new
21
- @private_message_cbs = CallbackList.new
22
- @invite_cbs = CallbackList.new
23
- end
24
-
25
- def join(jid, password = nil, opts = { :history => false })
26
- room_jid = JID.new(jid)
27
- xmuc = XMUC.new
28
- xmuc.password = password
29
-
30
- if !opts[:history]
31
- history = REXML::Element.new('history').tap{ |h| h.add_attribute('maxstanzas','0') }
32
- xmuc.add_element history
33
- end
34
-
35
- room_jid.resource = name
36
- set_presence(:available, room_jid, nil, xmuc) # TODO: Handle all join responses
37
- end
38
-
39
- def exit(jid, reason = nil)
40
- room_jid = JID.new(jid)
41
- Jabber::debuglog "Exiting #{jid}"
42
- set_presence(:unavailable, room_jid, reason)
43
- end
44
-
45
- def keep_alive password
46
- if @stream.is_disconnected?
47
- connect(password)
48
- end
49
- end
50
-
51
- def name
52
- @my_jid.resource
53
- end
54
-
55
- def name= resource
56
- @my_jid.resource = resource
57
- end
58
-
59
- def on_presence(prio = 0, ref = nil, &block)
60
- @presence_cbs.add(prio, ref) do |room_jid, user_name, pres_type|
61
- block.call(room_jid, user_name, pres_type)
62
- false
63
- end
64
- end
65
-
66
- def on_message(prio = 0, ref = nil, &block)
67
- @message_cbs.add(prio, ref) do |room_jid, user_name, message|
68
- block.call(room_jid, user_name, message)
69
- false
70
- end
71
- end
72
-
73
- def on_private_message(prio = 0, ref = nil, &block)
74
- @private_message_cbs.add(prio, ref) do |user_jid, message|
75
- block.call(user_jid, message)
76
- false
77
- end
78
- end
79
-
80
- def on_invite(prio = 0, ref = nil, &block)
81
- @invite_cbs.add(prio, ref) do |room_jid, user_name, room_name, topic|
82
- block.call(room_jid, user_name, room_name, topic)
83
- false
84
- end
85
- end
86
-
87
- def set_presence(type, to = nil, reason = nil, xmuc = nil, &block)
88
- pres = Presence.new(:chat, reason)
89
- pres.type = type
90
- pres.to = to if to
91
- pres.from = @my_jid
92
- pres.add(xmuc) if xmuc
93
- @stream.send(pres) { |r| block.call(r) }
94
- end
95
-
96
- def kick(recipients, room_jid)
97
- iq = Iq.new(:set, room_jid)
98
- iq.from = @my_jid
99
- iq.add(IqQueryMUCAdmin.new)
100
- recipients.each do |recipient|
101
- item = IqQueryMUCAdminItem.new
102
- item.nick = recipient
103
- item.role = :none
104
- iq.query.add(item)
105
- end
106
- @stream.send_with_id(iq)
107
- end
108
-
109
- def invite(recipients, room_jid)
110
- msg = Message.new
111
- msg.from = @my_jid
112
- msg.to = room_jid
113
- x = msg.add(XMUCUser.new)
114
- recipients.each do |jid|
115
- x.add(XMUCUserInvite.new(jid))
116
- end
117
- @stream.send(msg)
118
- end
119
-
120
- def send_message(type, jid, text, subject = nil)
121
- message = Message.new(JID.new(jid), text.to_s)
122
- message.type = type
123
- message.from = @my_jid
124
- message.subject = subject
125
-
126
- @send_thread.join if @send_thread.present? && @send_thread.alive?
127
- @send_thread = Thread.new {
128
- @stream.send(message)
129
- sleep(0.2)
130
- }
131
- end
132
-
133
- def connect password
134
- begin
135
- @stream.connect
136
- Jabber::debuglog "Connected to stream"
137
- @stream.auth(password)
138
- Jabber::debuglog "Authenticated"
139
- @muc_browser = MUCBrowser.new(@stream)
140
- Jabber::debuglog "MUCBrowser initialized"
141
- @conference_domain = @muc_browser.muc_rooms(@chat_domain).keys.first
142
- Jabber::debuglog "No conference domain found" if !@conference_domain.present?
143
- @roster = Roster::Helper.new(@stream) # TODO: Error handling
144
- @vcard = Vcard::Helper.new(@stream) # TODO: Error handling
145
- true
146
- rescue => e
147
- Jabber::debuglog "Connection failed"
148
- false
149
- end
150
- end
151
-
152
- def activate_callbacks
153
- @stream.add_presence_callback(150, self) { |presence|
154
- @presence_cbs.process(presence.from.strip.to_s, presence.from.resource, presence.type.to_s)
155
- }
156
-
157
- @stream.add_message_callback(150, self) { |message|
158
- handle_message(message)
159
- }
160
- Jabber::debuglog "Callbacks activated"
161
- end
162
-
163
- def get_rooms
164
- iq = Iq.new(:get, @conference_domain)
165
- iq.from = @stream.jid
166
- iq.add(Discovery::IqQueryDiscoItems.new)
167
-
168
- rooms = []
169
- @stream.send_with_id(iq) do |answer|
170
- answer.query.each_element('item') do |item|
171
- details = {}
172
- item.first.children.each{ |c| details[c.name] = c.text }
173
- rooms << {
174
- :item => item,
175
- :details => details
176
- }
177
- end
178
- end
179
- rooms
180
- end
181
-
182
- def get_users
183
- @roster.wait_for_roster
184
- @roster.items.map do |jid, item|
185
- {
186
- jid: item.jid.to_s,
187
- name: item.iname,
188
- mention: item.attributes['mention_name'],
189
- }
190
- end
191
- end
192
-
193
- def get_user_details user_jid
194
- vcard = @vcard.get(user_jid)
195
- {
196
- email: vcard['EMAIL/USERID'],
197
- title: vcard['TITLE'],
198
- photo: vcard['PHOTO'],
199
- }
200
- end
201
-
202
- def deactivate_callbacks
203
- @stream.delete_presence_callback(self)
204
- @stream.delete_message_callback(self)
205
- Jabber::debuglog "Callbacks deactivated"
206
- end
207
-
208
- private
209
-
210
- def handle_message(message)
211
- if is_invite?(message)
212
- room_name = message.children.last.first_element_text('name')
213
- topic = message.children.last.first_element_text('topic')
214
- @invite_cbs.process(message.from.strip.to_s, message.from.resource, room_name, topic)
215
- elsif message.type == :chat
216
- @private_message_cbs.process(message.from.strip.to_s, message)
217
- elsif message.type == :groupchat
218
- @message_cbs.process(message.from.strip.to_s, message.from.resource, message)
219
- elsif message.type == :error
220
- false
221
- end
222
- end
223
-
224
- def is_invite?(message)
225
- !message.x.nil? && message.x.kind_of?(XMUCUser) && message.x.first.kind_of?(XMUCUserInvite)
226
- end
227
-
228
- end
229
- end
230
- end