mail_to_hip_chat 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.rbenv-version +1 -0
- data/.yardopts +5 -0
- data/Gemfile +4 -0
- data/LICENSE +24 -0
- data/README.md +90 -0
- data/Rakefile +17 -0
- data/lib/mail_to_hip_chat/chute_chain.rb +32 -0
- data/lib/mail_to_hip_chat/exceptions.rb +4 -0
- data/lib/mail_to_hip_chat/message_chute.rb +20 -0
- data/lib/mail_to_hip_chat/message_chutes/airbrake.rb +58 -0
- data/lib/mail_to_hip_chat/message_chutes/test_email.rb +18 -0
- data/lib/mail_to_hip_chat/rack_app/builder.rb +47 -0
- data/lib/mail_to_hip_chat/rack_app.rb +57 -0
- data/lib/mail_to_hip_chat/version.rb +3 -0
- data/lib/mail_to_hip_chat.rb +2 -0
- data/mail_to_hip_chat.gemspec +31 -0
- data/support/config.ru +18 -0
- data/test/fixtures/airbrake_exception_body.txt +27 -0
- data/test/fixtures/airbrake_request_dump.txt +325 -0
- data/test/fixtures/test_email_request_dump.txt +89 -0
- data/test/integration/airbrake_processing_test.rb +46 -0
- data/test/test_helper.rb +59 -0
- data/test/unit/chute_chain_test.rb +48 -0
- data/test/unit/message_chute_test.rb +33 -0
- data/test/unit/message_chutes/airbrake_test.rb +39 -0
- data/test/unit/message_chutes/test_email_test.rb +21 -0
- data/test/unit/rack_app_test.rb +54 -0
- data/test.rb +10 -0
- metadata +184 -0
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.2-p290
|
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (C) 2011, Gabriel Gironda
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
copy of this software and associated documentation files (the "Software"),
|
5
|
+
to deal in the Software without restriction, including without limitation
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
20
|
+
|
21
|
+
Except as contained in this notice, the name of Gabriel Gironda shall not
|
22
|
+
be used in advertising or otherwise to promote the sale, use or other
|
23
|
+
dealings in this Software without prior written authorization from Gabriel
|
24
|
+
Gironda.
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
_
|
2
|
+
[_| . . . . .
|
3
|
+
.-----|--, |-. . ,-. ,-. |-. ,-. |- ,-,-. ,-. . | ," . . ,-. ,-. ,-. |
|
4
|
+
/ | /_\_ | | | | | | | | ,-| | -- | | | ,-| | | -- |- | | | | | | |-' |
|
5
|
+
| |__.-| ' ' ' |-' `-' ' ' `-^ `' ' ' ' `-^ ' `' | `-^ ' ' ' ' `-' `'
|
6
|
+
| |__\_| | '
|
7
|
+
'---,-,-;---' '
|
8
|
+
| |
|
9
|
+
|
10
|
+
# Mail To HipChat
|
11
|
+
|
12
|
+
Mail To HipChat lets you wire up email notifications to [HipChat](http://hipchat.com/r/30ad1). This is useful in situations where a third-party service doesn't have a real [WebHook](http://www.webhooks.org/) available, but you still want to be able to dump notifications into HipChat. The [Airbrake](http://airbrake.io) exception notification service is one example.
|
13
|
+
|
14
|
+
## How It Works
|
15
|
+
|
16
|
+
Mail To HipChat is designed to be deployed on [Heroku](http://heroku.com) and used with the [CloudMailIn](http://cloudmailin.com/) add-on. Adding the CloudMailIn incoming email address to the recipients list of the service you wish to integrate with will shuffle emails off to your instance of this tool as a HTTP POST request. A list of mail handlers is checked, and the appropriate one is invoked to format a message and send it off to one or more HipChat rooms.
|
17
|
+
|
18
|
+
## Prerequisites
|
19
|
+
|
20
|
+
You will need an account on Heroku and admin access to your HipChat group.
|
21
|
+
|
22
|
+
## Initial Setup
|
23
|
+
|
24
|
+
1. Make a home for your shiny new Mail To HipChat instance to live in.
|
25
|
+
|
26
|
+
$ mkdir my-mail_to_hip_chat
|
27
|
+
$ cd my-mail_to_hip_chat
|
28
|
+
$ git init
|
29
|
+
|
30
|
+
2. Do the bundler dance.
|
31
|
+
|
32
|
+
$ echo 'source "http://rubygems.org"' >> Gemfile
|
33
|
+
$ echo 'gem "mail_to_hip_chat"' >> Gemfile
|
34
|
+
$ bundle install
|
35
|
+
$ git commit -a -m "Getting down with bundler"
|
36
|
+
|
37
|
+
3. Copy over the default config.ru.
|
38
|
+
|
39
|
+
$ cp "`bundle show mail_to_hip_chat`/support/config.ru" .
|
40
|
+
$ git commit -a -m "Adding default config.ru"
|
41
|
+
|
42
|
+
4. Set up a new Heroku application with CloudMailIn and Piggyback SSL.
|
43
|
+
|
44
|
+
$ heroku create --stack cedar
|
45
|
+
$ heroku addons:add cloudmailin
|
46
|
+
$ heroku addons:add ssl:piggyback
|
47
|
+
|
48
|
+
5. Setup the CloudMailIn target address to point at your app.
|
49
|
+
|
50
|
+
Get the target address for CloudMailIn to hit when it receives an email.
|
51
|
+
|
52
|
+
$ echo "`heroku info -r | grep web_url | cut -d '=' -f 2 | sed 's/^http/https/'`notifications/create"
|
53
|
+
|
54
|
+
Get your CloudMailIn username and password from the Heroku app config.
|
55
|
+
|
56
|
+
$ heroku config | grep CLOUDMAILIN
|
57
|
+
|
58
|
+
[Log in to CloudMailIn](https://cloudmailin.com/users/sign_in) using the given `CLOUDMAILIN_USERNAME` and `CLOUDMAILIN_PASSWORD`. You'll see the entry for `CLOUDMAILIN_FORWARD_ADDRESS` in the list. Hit "Manage" and then "Edit Target", and set the target to the target address we retrieved above.
|
59
|
+
|
60
|
+
6. Get a HipChat API token and the ID for the Room(s) to send messages to.
|
61
|
+
|
62
|
+
Visit your [HipChat API Admin page](http://hipchat.com/group_admin/api). If there's a token you want to already use, use that one. If not, create a new token of type "Notification". Once you have the token, set it as an environment variable on Heroku.
|
63
|
+
|
64
|
+
$ heroku config:add HIPCHAT_API_TOKEN=fd3deeef7b88b95c1780e6237c41c30f
|
65
|
+
|
66
|
+
Visit your [HipChat Chat History page](https://hipchat.com/history). The integer in the URL for the history of each room (for example, `https://gabe.hipchat.com/history/room/31373`) is the room ID. Once you have the ID(s) of the rooms you wish to send messages to, set it as a comma separated environment variable on Heroku.
|
67
|
+
|
68
|
+
$ heroku config:add HIPCHAT_ROOMS=31373,31374
|
69
|
+
|
70
|
+
7. Deploy this sucker.
|
71
|
+
|
72
|
+
$ git push heroku master
|
73
|
+
|
74
|
+
8. Get your CloudMailIn forwarding address and send a test email.
|
75
|
+
|
76
|
+
$ heroku config --long | grep CLOUDMAILIN_FORWARD_ADDRESS
|
77
|
+
|
78
|
+
Send an email to the `CLOUDMAILIN_FORWARD_ADDRESS`, with a subject of "Testing Setup". The message should appear in the rooms you've configured Mail To HipChat to send messages to.
|
79
|
+
|
80
|
+
## Hooking it up to Airbrake
|
81
|
+
|
82
|
+
Just add a new user to your project, set their email address to `CLOUDMAILIN_FORWARD_ADDRESS`, and watch the exceptions roll in. Then turn your face from God and weep in abject horror.
|
83
|
+
|
84
|
+
## Adding new message handlers
|
85
|
+
|
86
|
+
## FAQ
|
87
|
+
|
88
|
+
## TODO
|
89
|
+
|
90
|
+
* There are too many steps for initial setup. This needs to be cut down.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "yard"
|
4
|
+
require "mail_to_hip_chat"
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList['test/{unit,integration}/**/*_test.rb']
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
YARD::Rake::YardocTask.new do |t|
|
14
|
+
t.options += ['--title', "Mail To HipChat #{MailToHipChat::VERSION} Documentation"]
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => :test
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module MailToHipChat
|
2
|
+
# A {ChuteChain} is used to hold a collection of chutes and to check whether any of those chutes is
|
3
|
+
# able to handle a message to hand off to HipChat. Anything that responds to #call can be pushed
|
4
|
+
# onto the chain.
|
5
|
+
class ChuteChain
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@chutes = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Pushes a chute onto the chain.
|
12
|
+
#
|
13
|
+
# @param [#call] chute The chute to push onto the chain.
|
14
|
+
#
|
15
|
+
# @return [self]
|
16
|
+
def push(chute)
|
17
|
+
@chutes.push(chute)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Takes in a message and traverses the chain looking for a chute that will handle it.
|
22
|
+
#
|
23
|
+
# @param [Hash] message The message to give to the chutes in the chain.
|
24
|
+
#
|
25
|
+
# @return [true] True if a chute accepts the message.
|
26
|
+
# @return [false] False if no chute can accept the message.
|
27
|
+
def accept(message)
|
28
|
+
@chutes.any? { |chute| chute.call(message) }
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "hipchat-api"
|
2
|
+
|
3
|
+
module MailToHipChat
|
4
|
+
module MessageChute
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
@rooms = Array(opts[:rooms])
|
8
|
+
@hipchat_api = opts[:hipchat_api] || HipChat::API.new(opts[:api_token])
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def message_rooms(from, message)
|
14
|
+
@rooms.each do |room_id|
|
15
|
+
@hipchat_api.rooms_message(room_id, from, message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "mail_to_hip_chat/message_chute"
|
2
|
+
require "mustache"
|
3
|
+
|
4
|
+
module MailToHipChat
|
5
|
+
module MessageChutes
|
6
|
+
class Airbrake
|
7
|
+
include MessageChute
|
8
|
+
|
9
|
+
def call(params)
|
10
|
+
return false unless process_message(params["plain"])
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def airbrake_domain
|
17
|
+
"airbrake.io"
|
18
|
+
end
|
19
|
+
|
20
|
+
def extraction_expression
|
21
|
+
%r[\A\n+Project:\s([^\n]+)\n+ # Pull out the project name
|
22
|
+
Environment:\s([^\n]+)\n+ # Pull out the project environment
|
23
|
+
# Pull out the URL to the exception
|
24
|
+
^(http://[^.]+\.#{Regexp.escape(airbrake_domain)}/errors/\d+)\n+
|
25
|
+
# Pull out the first line of the error message
|
26
|
+
Error\sMessage:\n-{14}\n([^\n]+)]mx
|
27
|
+
end
|
28
|
+
|
29
|
+
def hipchat_sender
|
30
|
+
"Airbrake"
|
31
|
+
end
|
32
|
+
|
33
|
+
def message_template
|
34
|
+
%q[<b>{{project}} - {{environment}}</b><br /><a href="{{url}}">{{message}}</a>]
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_message(plaintext)
|
38
|
+
return nil unless parts = extract_parts(plaintext)
|
39
|
+
send_notifications(parts)
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_parts(plaintext)
|
44
|
+
return nil unless parts = plaintext.match(extraction_expression)
|
45
|
+
[:project, :environment, :url, :message].each_with_index.inject({}) do |hash,(key,idx)|
|
46
|
+
hash[key] = parts[idx + 1]
|
47
|
+
hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def send_notifications(message_parts)
|
52
|
+
hipchat_message = Mustache.render(message_template, message_parts)
|
53
|
+
message_rooms(hipchat_sender, hipchat_message)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "mail_to_hip_chat/message_chute"
|
2
|
+
require "mustache"
|
3
|
+
|
4
|
+
module MailToHipChat
|
5
|
+
module MessageChutes
|
6
|
+
class TestEmail
|
7
|
+
include MessageChute
|
8
|
+
|
9
|
+
def call(params)
|
10
|
+
return false unless params["subject"] =~ /testing setup/i
|
11
|
+
message = Mustache.render("Message:<br />{{message}}", :message => params["plain"])
|
12
|
+
message_rooms("Testing", message)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "mail_to_hip_chat/rack_app"
|
2
|
+
require "rack/builder"
|
3
|
+
require "rack/urlmap"
|
4
|
+
require "rack/commonlogger"
|
5
|
+
|
6
|
+
module MailToHipChat
|
7
|
+
class RackApp
|
8
|
+
class Builder
|
9
|
+
attr_accessor :secret, :rooms, :api_token, :mount_point
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@chutes = []
|
13
|
+
@mount_point = '/notifications/create'
|
14
|
+
yield(self) if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def use_chute(chute_klass)
|
18
|
+
@chutes << chute_klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_app
|
22
|
+
return @app if @app
|
23
|
+
app = build_app
|
24
|
+
mnt = mount_point
|
25
|
+
@app = Rack::Builder.new do
|
26
|
+
use Rack::CommonLogger
|
27
|
+
map(mnt) { run app }
|
28
|
+
end.to_app
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def build_app
|
34
|
+
MailToHipChat::RackApp.new(:secret => @secret) do |f|
|
35
|
+
@chutes.each do |chute_klass|
|
36
|
+
f.use_chute(chute_klass.new(:api_token => @api_token, :rooms => split_rooms))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def split_rooms
|
42
|
+
@rooms.split(/,\s*/)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "mail_to_hip_chat/rack_app/builder"
|
2
|
+
require "mail_to_hip_chat/chute_chain"
|
3
|
+
require "mail_to_hip_chat/exceptions"
|
4
|
+
require "rack/request"
|
5
|
+
require "digest/md5"
|
6
|
+
|
7
|
+
module MailToHipChat
|
8
|
+
class RackApp
|
9
|
+
SIGNATURE_PARAM_NAME = "signature"
|
10
|
+
|
11
|
+
def initialize(opts = {})
|
12
|
+
@secret = opts[:secret]
|
13
|
+
@chute_chain = opts[:chute_chain] || ChuteChain.new
|
14
|
+
|
15
|
+
yield(self) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(rack_env)
|
19
|
+
request = Rack::Request.new(rack_env)
|
20
|
+
return [400, {}, 'Bad Request.'] unless valid_request?(request)
|
21
|
+
|
22
|
+
if @chute_chain.accept(request.params)
|
23
|
+
[200, {}, 'OK']
|
24
|
+
else
|
25
|
+
[404, {}, 'Not Found.']
|
26
|
+
end
|
27
|
+
|
28
|
+
rescue Exception => error
|
29
|
+
error.extend(InternalError)
|
30
|
+
raise error
|
31
|
+
end
|
32
|
+
|
33
|
+
def use_chute(chute)
|
34
|
+
@chute_chain.push(chute)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def valid_request?(request)
|
40
|
+
computed_sig = create_sig_from_params(request.params)
|
41
|
+
|
42
|
+
computed_sig == request.params[SIGNATURE_PARAM_NAME]
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def create_sig_from_params(params)
|
47
|
+
sorted_param_names = params.keys.sort
|
48
|
+
sorted_param_names.delete(SIGNATURE_PARAM_NAME)
|
49
|
+
|
50
|
+
param_vals_with_secret = params.values_at(*sorted_param_names)
|
51
|
+
param_vals_with_secret.push(@secret)
|
52
|
+
|
53
|
+
Digest::MD5.hexdigest(param_vals_with_secret.join)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "mail_to_hip_chat/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "mail_to_hip_chat"
|
7
|
+
s.version = MailToHipChat::VERSION
|
8
|
+
s.authors = ["Gabriel Gironda"]
|
9
|
+
s.email = ["gabriel@gironda.org"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Funnels email into HipChat}
|
12
|
+
s.description = %q{Funnels email into HipChat using CloudMailIn}
|
13
|
+
|
14
|
+
s.rubyforge_project = "mail_to_hip_chat"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "http_parser.rb", "~> 0.5"
|
22
|
+
s.add_development_dependency "mocha", "~> 0.10"
|
23
|
+
s.add_development_dependency "yard", "~> 0.7"
|
24
|
+
s.add_development_dependency "rdiscount", "~> 1.6"
|
25
|
+
s.add_development_dependency "webmock", "~> 1.7"
|
26
|
+
s.add_development_dependency "rake"
|
27
|
+
|
28
|
+
s.add_runtime_dependency "rack", "~> 1.3"
|
29
|
+
s.add_runtime_dependency "hipchat-api", "~> 1.0"
|
30
|
+
s.add_runtime_dependency "mustache", "~> 0.99"
|
31
|
+
end
|
data/support/config.ru
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "mail_to_hip_chat"
|
2
|
+
require "mail_to_hip_chat/rack_app"
|
3
|
+
require "mail_to_hip_chat/message_chutes/airbrake"
|
4
|
+
require "mail_to_hip_chat/message_chutes/test_email"
|
5
|
+
|
6
|
+
app = MailToHipChat::RackApp::Builder.new do |builder|
|
7
|
+
builder.secret = ENV['CLOUDMAILIN_SECRET']
|
8
|
+
builder.rooms = ENV['HIPCHAT_ROOMS']
|
9
|
+
builder.api_token = ENV['HIPCHAT_API_TOKEN']
|
10
|
+
|
11
|
+
# An included chute to process messages from Airbrake.
|
12
|
+
builder.use_chute MailToHipChat::MessageChutes::Airbrake
|
13
|
+
|
14
|
+
# This chute should be removed once you've confirmed your setup works.
|
15
|
+
builder.use_chute MailToHipChat::MessageChutes::TestEmail
|
16
|
+
end.to_app
|
17
|
+
|
18
|
+
run app
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
Project: Echelon
|
4
|
+
Environment: Staging
|
5
|
+
|
6
|
+
|
7
|
+
http://the-nsa.airbrake.io/errors/11082122
|
8
|
+
|
9
|
+
Error Message:
|
10
|
+
--------------
|
11
|
+
AirbrakeTestingException: Testing airbrake via "rake airbrake:test". If you can see this, it works.
|
12
|
+
|
13
|
+
Where:
|
14
|
+
------
|
15
|
+
application#verify
|
16
|
+
[PROJECT_ROOT]/vendor/bundle/ruby/1.9.1/gems/activesupport-3.1.0/lib/active_support/callbacks.rb, line 412
|
17
|
+
|
18
|
+
URL:
|
19
|
+
----
|
20
|
+
http://example.org/verify
|
21
|
+
|
22
|
+
Backtrace Summary:
|
23
|
+
------------------
|
24
|
+
[PROJECT_ROOT]/lib/sha1_unhasher.rb:13:in `block in _call'
|
25
|
+
[PROJECT_ROOT]/lib/sha1_unhasher.rb.rb:12:in `_call'
|
26
|
+
[PROJECT_ROOT]/lib/sha1_unhasher.rb.rb:7:in `call'
|
27
|
+
|