lingohub 0.0.4
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.
- data/CHANGELOG.md +4 -0
- data/LICENSE +20 -0
- data/README.md +10 -0
- data/REST.md +0 -0
- data/ROADMAP.md +0 -0
- data/bin/lingohub +14 -0
- data/lib/lingohub.rb +16 -0
- data/lib/lingohub/client.rb +106 -0
- data/lib/lingohub/command.rb +98 -0
- data/lib/lingohub/commands/auth.rb +146 -0
- data/lib/lingohub/commands/base.rb +119 -0
- data/lib/lingohub/commands/collaborator.rb +44 -0
- data/lib/lingohub/commands/help.rb +101 -0
- data/lib/lingohub/commands/project.rb +66 -0
- data/lib/lingohub/commands/translation.rb +97 -0
- data/lib/lingohub/commands/version.rb +7 -0
- data/lib/lingohub/helpers.rb +121 -0
- data/lib/lingohub/models/collaborator.rb +25 -0
- data/lib/lingohub/models/project.rb +130 -0
- data/lib/lingohub/models/projects.rb +34 -0
- data/lib/lingohub/models/resource.rb +15 -0
- data/lib/lingohub/models/user.rb +3 -0
- data/lib/lingohub/rails3/railtie.rb +14 -0
- data/lib/lingohub/version.rb +4 -0
- data/lib/patches/rails3/i18n/i18n.rb +66 -0
- data/lib/vendor/okjson.rb +556 -0
- metadata +233 -0
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011-2012 lingohub GmbH
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/REST.md
ADDED
File without changes
|
data/ROADMAP.md
ADDED
File without changes
|
data/bin/lingohub
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'lingohub'
|
7
|
+
require 'lingohub/command'
|
8
|
+
|
9
|
+
args = ARGV.dup
|
10
|
+
ARGV.clear
|
11
|
+
command = args.shift.strip rescue 'help'
|
12
|
+
|
13
|
+
Lingohub::Command.run(command, args)
|
14
|
+
|
data/lib/lingohub.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "lingohub/client"
|
2
|
+
require "lingohub/rails3/railtie" if defined?(Rails)
|
3
|
+
|
4
|
+
module Lingohub
|
5
|
+
class << self
|
6
|
+
attr_accessor :environments, :protocol, :host, :username, :project
|
7
|
+
|
8
|
+
def configure
|
9
|
+
yield self
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_value?(value)
|
13
|
+
value.start_with?(":")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'uri'
|
3
|
+
require 'time'
|
4
|
+
require 'lingohub/version'
|
5
|
+
require 'vendor/okjson'
|
6
|
+
require 'json'
|
7
|
+
require 'lingohub/models/projects'
|
8
|
+
|
9
|
+
# A Ruby class to call the lingohub REST API. You might use this if you want to
|
10
|
+
# manage your lingohub apps from within a Ruby program, such as Capistrano.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
#
|
14
|
+
# require 'lingohub'
|
15
|
+
# lingohub = Lingohub::Client.new('me@example.com', 'mypass')
|
16
|
+
# lingohub.create('myapp')
|
17
|
+
#
|
18
|
+
class Lingohub::Client
|
19
|
+
|
20
|
+
def self.version
|
21
|
+
Lingohub::VERSION
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.gem_version_string
|
25
|
+
"lingohub-gem/#{version}"
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :host, :user, :password
|
29
|
+
|
30
|
+
def self.auth(options)
|
31
|
+
client = new(options)
|
32
|
+
OkJson.decode client.post('/sessions', {}, :accept => 'json').to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(options)
|
36
|
+
@user = options[:username]
|
37
|
+
@password = options[:password]
|
38
|
+
@auth_token = options[:auth_token]
|
39
|
+
@host = options[:host] || 'lingohub.com'
|
40
|
+
end
|
41
|
+
|
42
|
+
def credentials
|
43
|
+
@auth_token.nil? ? {:username => @user, :password => @password} : {:username => @auth_token, :password => ""}
|
44
|
+
end
|
45
|
+
|
46
|
+
def project(title)
|
47
|
+
project = self.projects[title]
|
48
|
+
raise(Lingohub::Command::CommandFailed, "=== You aren't associated for a project named '#{title}'") if project.nil?
|
49
|
+
project
|
50
|
+
end
|
51
|
+
|
52
|
+
def projects
|
53
|
+
return Lingohub::Models::Projects.new(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(uri, extra_headers={ }) # :nodoc:
|
57
|
+
process(:get, uri, extra_headers)
|
58
|
+
end
|
59
|
+
|
60
|
+
def post(uri, payload="", extra_headers={ }) # :nodoc:
|
61
|
+
process(:post, uri, extra_headers, payload)
|
62
|
+
end
|
63
|
+
|
64
|
+
def put(uri, payload, extra_headers={ }) # :nodoc:
|
65
|
+
process(:put, uri, extra_headers, payload)
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(uri, extra_headers={ }) # :nodoc:
|
69
|
+
process(:delete, uri, extra_headers)
|
70
|
+
end
|
71
|
+
|
72
|
+
def process(method, uri, extra_headers={ }, payload=nil)
|
73
|
+
headers = lingohub_headers.merge(extra_headers)
|
74
|
+
# payload = auth_params.merge(payload)
|
75
|
+
args = [method, payload, headers].compact
|
76
|
+
response = resource(uri, credentials).send(*args)
|
77
|
+
|
78
|
+
response
|
79
|
+
end
|
80
|
+
|
81
|
+
def resource(uri, credentials)
|
82
|
+
RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
|
83
|
+
if uri =~ /^https?/
|
84
|
+
RestClient::Resource.new(uri, :user => credentials[:username], :password => credentials[:password])
|
85
|
+
else
|
86
|
+
host_uri = host =~ /^https?/ ? "#{host}/#{api_uri_part}" : "https://#{host}/#{api_uri_part}"
|
87
|
+
RestClient::Resource.new(host_uri, :user => credentials[:username], :password => credentials[:password])[uri]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def api_uri_part
|
92
|
+
"api/#{Lingohub::API_VERSION}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def lingohub_headers # :nodoc:
|
96
|
+
{
|
97
|
+
'X-lingohub-API-Version' => '1',
|
98
|
+
'User-Agent' => self.class.gem_version_string,
|
99
|
+
'X-Ruby-Version' => RUBY_VERSION,
|
100
|
+
'X-Ruby-Platform' => RUBY_PLATFORM,
|
101
|
+
'content_type' => 'json',
|
102
|
+
'accept' => 'json'
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'lingohub/helpers'
|
2
|
+
require 'lingohub/commands/base'
|
3
|
+
|
4
|
+
Dir["#{File.dirname(__FILE__)}/commands/*.rb"].each { |c| require c }
|
5
|
+
|
6
|
+
module Lingohub
|
7
|
+
module Command
|
8
|
+
class InvalidCommand < RuntimeError; end
|
9
|
+
class CommandFailed < RuntimeError; end
|
10
|
+
|
11
|
+
extend Lingohub::Helpers
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def run(command, args, retries=0)
|
16
|
+
begin
|
17
|
+
run_internal 'auth:reauthorize', args.dup if retries > 0
|
18
|
+
run_internal(command, args.dup)
|
19
|
+
rescue InvalidCommand
|
20
|
+
error "Unknown command. Run 'lingohub help' for usage information."
|
21
|
+
# rescue RestClient::Unauthorized
|
22
|
+
# if retries < 3
|
23
|
+
# STDERR.puts "Authentication failure"
|
24
|
+
# run(command, args, retries+1)
|
25
|
+
# else
|
26
|
+
# error "Authentication failure"
|
27
|
+
# end
|
28
|
+
# rescue RestClient::ResourceNotFound => e
|
29
|
+
# error extract_not_found(e.http_body)
|
30
|
+
# rescue RestClient::RequestFailed => e
|
31
|
+
# error extract_error(e.http_body) unless e.http_code == 402
|
32
|
+
# retry if run_internal('account:confirm_billing', args.dup)
|
33
|
+
# rescue RestClient::RequestTimeout
|
34
|
+
# error "API request timed out. Please try again, or contact team@lingohub.com if this issue persists."
|
35
|
+
rescue CommandFailed => e
|
36
|
+
error e.message
|
37
|
+
rescue Interrupt => e
|
38
|
+
error "\n[canceled]"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_internal(command, args, lingohub=nil)
|
43
|
+
klass, method = parse(command)
|
44
|
+
runner = klass.new(args, lingohub)
|
45
|
+
raise InvalidCommand unless runner.respond_to?(method)
|
46
|
+
runner.send(method)
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(command)
|
50
|
+
parts = command.split(':')
|
51
|
+
case parts.size
|
52
|
+
when 1
|
53
|
+
begin
|
54
|
+
return eval("Lingohub::Command::#{command.capitalize}"), :index
|
55
|
+
rescue NameError, NoMethodError
|
56
|
+
return Lingohub::Command::Project, command.to_sym
|
57
|
+
end
|
58
|
+
else
|
59
|
+
begin
|
60
|
+
const = Lingohub::Command
|
61
|
+
command = parts.pop
|
62
|
+
parts.each { |part| const = const.const_get(part.capitalize) }
|
63
|
+
return const, command.to_sym
|
64
|
+
rescue NameError
|
65
|
+
raise InvalidCommand
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# def extract_not_found(body)
|
71
|
+
# body =~ /^[\w\s]+ not found$/ ? body : "Resource not found"
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# def extract_error(body)
|
75
|
+
# msg = parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || 'Internal server error'
|
76
|
+
# msg.split("\n").map { |line| ' ! ' + line }.join("\n")
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# def parse_error_xml(body)
|
80
|
+
# xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
|
81
|
+
# msg = xml_errors.map { |a| a.text }.join(" / ")
|
82
|
+
# return msg unless msg.empty?
|
83
|
+
# rescue Exception
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# def parse_error_json(body)
|
87
|
+
# json = OkJson.decode(body.to_s)
|
88
|
+
# json['error']
|
89
|
+
# rescue OkJson::ParserError
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# def parse_error_plain(body)
|
93
|
+
# return unless body.respond_to?(:headers) && body.headers[:content_type].include?("text/plain")
|
94
|
+
# body.to_s
|
95
|
+
# end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require "lingohub/client"
|
2
|
+
|
3
|
+
module Lingohub::Command
|
4
|
+
class Auth < Base
|
5
|
+
attr_accessor :credentials
|
6
|
+
|
7
|
+
def client
|
8
|
+
@client ||= init_lingohub
|
9
|
+
end
|
10
|
+
|
11
|
+
def init_lingohub
|
12
|
+
client = Lingohub::Client.new(:username => user, :auth_token => auth_token, :host => host)
|
13
|
+
# client.on_warning { |msg| self.display("\n#{msg}\n\n") }
|
14
|
+
client
|
15
|
+
end
|
16
|
+
|
17
|
+
def host
|
18
|
+
ENV['lingohub_HOST'] || 'lingohub.com'
|
19
|
+
end
|
20
|
+
|
21
|
+
# just a stub; will raise if not authenticated
|
22
|
+
def check
|
23
|
+
client.projects.all
|
24
|
+
end
|
25
|
+
|
26
|
+
def reauthorize
|
27
|
+
@credentials = ask_for_and_save_credentials
|
28
|
+
end
|
29
|
+
|
30
|
+
def user # :nodoc:
|
31
|
+
get_credentials
|
32
|
+
@credentials[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
def auth_token # :nodoc:
|
36
|
+
get_credentials
|
37
|
+
@credentials[1]
|
38
|
+
end
|
39
|
+
|
40
|
+
def credentials_file
|
41
|
+
"#{home_directory}/.lingohub/credentials"
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_credentials # :nodoc:
|
45
|
+
return if @credentials
|
46
|
+
unless @credentials = read_credentials
|
47
|
+
ask_for_and_save_credentials
|
48
|
+
end
|
49
|
+
@credentials
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_credentials
|
53
|
+
File.exists?(credentials_file) and File.read(credentials_file).split("\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
def echo_off
|
57
|
+
system "stty -echo"
|
58
|
+
end
|
59
|
+
|
60
|
+
def echo_on
|
61
|
+
system "stty echo"
|
62
|
+
end
|
63
|
+
|
64
|
+
def ask_for_credentials
|
65
|
+
puts "Enter your Lingohub credentials."
|
66
|
+
|
67
|
+
print "Email: "
|
68
|
+
user = ask
|
69
|
+
|
70
|
+
print "Password: "
|
71
|
+
password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
|
72
|
+
api_key = Lingohub::Client.auth(:username => user, :password => password, :host => host)['api_key']
|
73
|
+
|
74
|
+
[user, api_key]
|
75
|
+
end
|
76
|
+
|
77
|
+
def ask_for_password_on_windows
|
78
|
+
require "Win32API"
|
79
|
+
char = nil
|
80
|
+
password = ''
|
81
|
+
|
82
|
+
while char = Win32API.new("crtdll", "_getch", [], "L").Call do
|
83
|
+
break if char == 10 || char == 13 # received carriage return or newline
|
84
|
+
if char == 127 || char == 8 # backspace and delete
|
85
|
+
password.slice!(-1, 1)
|
86
|
+
else
|
87
|
+
# windows might throw a -1 at us so make sure to handle RangeError
|
88
|
+
(password << char.chr) rescue RangeError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
puts
|
92
|
+
return password
|
93
|
+
end
|
94
|
+
|
95
|
+
def ask_for_password
|
96
|
+
echo_off
|
97
|
+
password = ask
|
98
|
+
puts
|
99
|
+
echo_on
|
100
|
+
return password
|
101
|
+
end
|
102
|
+
|
103
|
+
def ask_for_and_save_credentials
|
104
|
+
begin
|
105
|
+
@credentials = ask_for_credentials
|
106
|
+
write_credentials
|
107
|
+
check
|
108
|
+
rescue ::RestClient::Unauthorized, ::RestClient::ResourceNotFound => e
|
109
|
+
puts "EXCEPTION #{e}"
|
110
|
+
delete_credentials
|
111
|
+
@client = nil
|
112
|
+
@credentials = nil
|
113
|
+
display "Authentication failed."
|
114
|
+
retry if retry_login?
|
115
|
+
exit 1
|
116
|
+
rescue Exception => e
|
117
|
+
delete_credentials
|
118
|
+
raise e
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def retry_login?
|
123
|
+
@login_attempts ||= 0
|
124
|
+
@login_attempts += 1
|
125
|
+
@login_attempts < 3
|
126
|
+
end
|
127
|
+
|
128
|
+
def write_credentials
|
129
|
+
FileUtils.mkdir_p(File.dirname(credentials_file))
|
130
|
+
f = File.open(credentials_file, 'w')
|
131
|
+
f.chmod(0600)
|
132
|
+
f.puts self.credentials
|
133
|
+
f.close
|
134
|
+
set_credentials_permissions
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_credentials_permissions
|
138
|
+
FileUtils.chmod 0700, File.dirname(credentials_file)
|
139
|
+
FileUtils.chmod 0600, credentials_file
|
140
|
+
end
|
141
|
+
|
142
|
+
def delete_credentials
|
143
|
+
# FileUtils.rm_f(credentials_file)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Lingohub::Command
|
4
|
+
class Base
|
5
|
+
include Lingohub::Helpers
|
6
|
+
|
7
|
+
attr_accessor :args
|
8
|
+
attr_reader :autodetected_app
|
9
|
+
|
10
|
+
def initialize(args, lingohub=nil)
|
11
|
+
@args = args
|
12
|
+
@lingohub = lingohub
|
13
|
+
@autodetected_project_name = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def lingohub
|
17
|
+
@lingohub ||= Lingohub::Command.run_internal('auth:client', args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def project_title(force=true)
|
21
|
+
project_title = extract_project_title_from_args
|
22
|
+
unless project_title
|
23
|
+
project_title = extract_project_title_from_git || extract_project_title_from_dir_name ||
|
24
|
+
raise(CommandFailed, "No project specified.\nRun this command from project folder or set it adding --project <title>") if force
|
25
|
+
@autodetected_project_name = true
|
26
|
+
end
|
27
|
+
project_title
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_project_title_from_args
|
31
|
+
project_title = extract_option('--project', false)
|
32
|
+
raise(CommandFailed, "You must specify a project title after --project") if project_title == false
|
33
|
+
project_title
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_project_title_from_dir_name
|
37
|
+
dir = Dir.pwd
|
38
|
+
File.basename(dir)
|
39
|
+
end
|
40
|
+
|
41
|
+
def extract_project_title_from_git
|
42
|
+
dir = Dir.pwd
|
43
|
+
return unless remotes = git_remotes(dir)
|
44
|
+
|
45
|
+
if remote = extract_option('--remote')
|
46
|
+
remotes[remote]
|
47
|
+
elsif remote = extract_app_from_git_config
|
48
|
+
remotes[remote]
|
49
|
+
else
|
50
|
+
apps = remotes.values.uniq
|
51
|
+
return apps.first if apps.size == 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def extract_app_from_git_config
|
56
|
+
remote = git("config heroku.remote")
|
57
|
+
remote == "" ? nil : remote
|
58
|
+
end
|
59
|
+
|
60
|
+
def git_remotes(base_dir=Dir.pwd)
|
61
|
+
remotes = { }
|
62
|
+
original_dir = Dir.pwd
|
63
|
+
Dir.chdir(base_dir)
|
64
|
+
|
65
|
+
# TODO
|
66
|
+
# git("remote -v").split("\n").each do |remote|
|
67
|
+
# name, url, method = remote.split(/\s/)
|
68
|
+
# if url =~ /^git@#{heroku.host}:([\w\d-]+)\.git$/
|
69
|
+
# remotes[name] = $1
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
|
73
|
+
Dir.chdir(original_dir)
|
74
|
+
remotes
|
75
|
+
end
|
76
|
+
|
77
|
+
def extract_option(options, default=true)
|
78
|
+
values = options.is_a?(Array) ? options : [options]
|
79
|
+
return unless opt_index = args.select { |a| values.include? a }.first
|
80
|
+
opt_position = args.index(opt_index) + 1
|
81
|
+
if args.size > opt_position && opt_value = args[opt_position]
|
82
|
+
if opt_value.include?('--')
|
83
|
+
opt_value = nil
|
84
|
+
else
|
85
|
+
args.delete_at(opt_position)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
opt_value ||= default
|
89
|
+
args.delete(opt_index)
|
90
|
+
block_given? ? yield(opt_value) : opt_value
|
91
|
+
end
|
92
|
+
|
93
|
+
def git_url(name)
|
94
|
+
"git@#{heroku.host}:#{name}.git"
|
95
|
+
end
|
96
|
+
|
97
|
+
def app_urls(name)
|
98
|
+
# "#{web_url(name)} | #{git_url(name)}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def escape(value)
|
102
|
+
lingohub.escape(value)
|
103
|
+
end
|
104
|
+
|
105
|
+
def project(title=nil)
|
106
|
+
title ||= project_title
|
107
|
+
@project ||= lingohub.project(title)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class BaseWithApp < Base
|
112
|
+
attr_accessor :app
|
113
|
+
|
114
|
+
def initialize(args, lingohub=nil)
|
115
|
+
super(args, lingohub)
|
116
|
+
@app ||= extract_app
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|