xat 1.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +176 -0
- data/README.md +38 -0
- data/app_template/README.md +14 -0
- data/app_template/app.css +0 -0
- data/app_template/app.js +12 -0
- data/app_template/assets/banner.png +0 -0
- data/app_template/assets/logo-promotion.png +0 -0
- data/app_template/assets/logo-small.png +0 -0
- data/app_template/assets/logo.png +0 -0
- data/app_template/manifest.json.tt +13 -0
- data/app_template/templates/layout.hdbs +10 -0
- data/app_template/translations/en.json +16 -0
- data/app_template_iframe/README.md +14 -0
- data/app_template_iframe/assets/banner.png +0 -0
- data/app_template_iframe/assets/iframe.html +13 -0
- data/app_template_iframe/assets/logo-promotion.png +0 -0
- data/app_template_iframe/assets/logo-small.png +0 -0
- data/app_template_iframe/assets/logo.png +0 -0
- data/app_template_iframe/manifest.json.tt +15 -0
- data/app_template_iframe/translations/en.json +16 -0
- data/bin/xat +5 -0
- data/features/clean.feature +9 -0
- data/features/fixtures/quote_character_translation.json +6 -0
- data/features/new.feature +117 -0
- data/features/package.feature +22 -0
- data/features/step_definitions/app_steps.rb +103 -0
- data/features/support/env.rb +5 -0
- data/features/validate.feature +15 -0
- data/lib/xat.rb +5 -0
- data/lib/zendesk_apps_tools/api_connection.rb +33 -0
- data/lib/zendesk_apps_tools/array_patch.rb +22 -0
- data/lib/zendesk_apps_tools/bump.rb +60 -0
- data/lib/zendesk_apps_tools/cache.rb +25 -0
- data/lib/zendesk_apps_tools/command.rb +183 -0
- data/lib/zendesk_apps_tools/command_helpers.rb +20 -0
- data/lib/zendesk_apps_tools/common.rb +40 -0
- data/lib/zendesk_apps_tools/deploy.rb +94 -0
- data/lib/zendesk_apps_tools/directory.rb +28 -0
- data/lib/zendesk_apps_tools/locale_identifier.rb +10 -0
- data/lib/zendesk_apps_tools/manifest_handler.rb +72 -0
- data/lib/zendesk_apps_tools/package_helper.rb +30 -0
- data/lib/zendesk_apps_tools/server.rb +65 -0
- data/lib/zendesk_apps_tools/settings.rb +82 -0
- data/lib/zendesk_apps_tools/translate.rb +168 -0
- data/templates/translation.erb.tt +13 -0
- metadata +238 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'zendesk_apps_tools/cache'
|
2
|
+
require 'zendesk_apps_tools/common'
|
3
|
+
require 'zendesk_apps_tools/api_connection'
|
4
|
+
require 'zendesk_apps_tools/deploy'
|
5
|
+
require 'zendesk_apps_tools/directory'
|
6
|
+
require 'zendesk_apps_tools/package_helper'
|
7
|
+
require 'zendesk_apps_tools/settings'
|
8
|
+
require 'zendesk_apps_tools/translate'
|
9
|
+
require 'zendesk_apps_tools/bump'
|
10
|
+
|
11
|
+
module ZendeskAppsTools
|
12
|
+
module CommandHelpers
|
13
|
+
include ZendeskAppsTools::Cache
|
14
|
+
include ZendeskAppsTools::Common
|
15
|
+
include ZendeskAppsTools::APIConnection
|
16
|
+
include ZendeskAppsTools::Deploy
|
17
|
+
include ZendeskAppsTools::Directory
|
18
|
+
include ZendeskAppsTools::PackageHelper
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module ZendeskAppsTools
|
4
|
+
module Common
|
5
|
+
def api_request(url, request = Faraday.new)
|
6
|
+
request.get(url)
|
7
|
+
end
|
8
|
+
|
9
|
+
def say_error_and_exit(msg)
|
10
|
+
say msg, :red
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_value_from_stdin(prompt, opts = {})
|
15
|
+
options = {
|
16
|
+
valid_regex: opts[:allow_empty] ? /^.*$/ : /\S+/,
|
17
|
+
error_msg: 'Invalid, try again:',
|
18
|
+
allow_empty: false
|
19
|
+
}.merge(opts)
|
20
|
+
|
21
|
+
while input = ask(prompt)
|
22
|
+
return '' if input.empty? && options[:allow_empty]
|
23
|
+
if input =~ options[:valid_regex]
|
24
|
+
break
|
25
|
+
else
|
26
|
+
say(options[:error_msg], :red)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
input
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_password_from_stdin(prompt)
|
34
|
+
print "#{prompt} "
|
35
|
+
password = STDIN.noecho(&:gets).chomp
|
36
|
+
puts
|
37
|
+
password
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
module Deploy
|
3
|
+
def deploy_app(connection_method, url, body)
|
4
|
+
body[:upload_id] = upload(options[:path]).to_s
|
5
|
+
|
6
|
+
connection = get_connection
|
7
|
+
|
8
|
+
response = connection.send(connection_method) do |req|
|
9
|
+
req.url url
|
10
|
+
req.headers[:content_type] = 'application/json'
|
11
|
+
req.body = JSON.generate body
|
12
|
+
end
|
13
|
+
|
14
|
+
check_status response
|
15
|
+
|
16
|
+
rescue Faraday::Error::ClientError, JSON::ParserError => e
|
17
|
+
say_error_and_exit e.message
|
18
|
+
end
|
19
|
+
|
20
|
+
def upload(path)
|
21
|
+
connection = get_connection :multipart
|
22
|
+
zipfile_path = options[:zipfile]
|
23
|
+
|
24
|
+
if zipfile_path
|
25
|
+
package_path = zipfile_path
|
26
|
+
else
|
27
|
+
package
|
28
|
+
package_path = Dir[File.join path, '/tmp/*.zip'].sort.last
|
29
|
+
end
|
30
|
+
|
31
|
+
payload = { uploaded_data: Faraday::UploadIO.new(package_path, 'application/zip') }
|
32
|
+
|
33
|
+
response = connection.post('/api/v2/apps/uploads.json', payload)
|
34
|
+
JSON.parse(response.body)['id']
|
35
|
+
|
36
|
+
rescue Faraday::Error::ClientError, JSON::ParserError => e
|
37
|
+
say_error_and_exit e.message
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_app_id
|
41
|
+
say_status 'Update', 'app ID is missing, searching...'
|
42
|
+
name = get_value_from_stdin('Enter the name of the app:')
|
43
|
+
|
44
|
+
connection = get_connection
|
45
|
+
|
46
|
+
all_apps = connection.get('/api/v2/apps.json').body
|
47
|
+
|
48
|
+
app_id = JSON.parse(all_apps)['apps'].find { |app| app['name'] == name }['id']
|
49
|
+
|
50
|
+
save_cache 'app_id' => app_id
|
51
|
+
app_id
|
52
|
+
rescue Faraday::Error::ClientError => e
|
53
|
+
say_error_and_exit e.message
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_status(response)
|
57
|
+
job = response.body
|
58
|
+
job_response = JSON.parse(job)
|
59
|
+
say_error_and_exit job_response['error'] if job_response['error']
|
60
|
+
|
61
|
+
job_id = job_response['job_id']
|
62
|
+
check_job job_id
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_job(job_id)
|
66
|
+
connection = get_connection
|
67
|
+
|
68
|
+
loop do
|
69
|
+
response = connection.get("/api/v2/apps/job_statuses/#{job_id}")
|
70
|
+
info = JSON.parse(response.body)
|
71
|
+
status = info['status']
|
72
|
+
message = info['message']
|
73
|
+
app_id = info['app_id']
|
74
|
+
|
75
|
+
if %w(completed failed).include? status
|
76
|
+
case status
|
77
|
+
when 'completed'
|
78
|
+
save_cache 'app_id' => app_id
|
79
|
+
say_status @command, 'OK'
|
80
|
+
when 'failed'
|
81
|
+
say_status @command, message, :red
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
break
|
85
|
+
end
|
86
|
+
|
87
|
+
say_status 'Status', status
|
88
|
+
sleep 3
|
89
|
+
end
|
90
|
+
rescue Faraday::Error::ClientError => e
|
91
|
+
say_error_and_exit e.message
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
module Directory
|
3
|
+
def app_dir
|
4
|
+
@app_dir ||= Pathname.new(destination_root)
|
5
|
+
end
|
6
|
+
|
7
|
+
def tmp_dir
|
8
|
+
@tmp_dir ||= Pathname.new(File.join(app_dir, 'tmp')).tap do |dir|
|
9
|
+
FileUtils.mkdir_p(dir)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def prompt_new_app_dir
|
14
|
+
prompt = "Enter a directory name to save the new app (will create the dir if it does not exist, default to current dir):\n"
|
15
|
+
opts = { valid_regex: /^(\w|\/|\\)*$/, allow_empty: true }
|
16
|
+
while @app_dir = get_value_from_stdin(prompt, opts)
|
17
|
+
@app_dir = './' and break if @app_dir.empty?
|
18
|
+
if !File.exist?(@app_dir)
|
19
|
+
break
|
20
|
+
elsif !File.directory?(@app_dir)
|
21
|
+
puts 'Invalid dir, try again:'
|
22
|
+
else
|
23
|
+
break
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'zendesk_apps_tools/array_patch'
|
2
|
+
|
3
|
+
module ZendeskAppsTools
|
4
|
+
module ManifestHandler
|
5
|
+
VERSION_PARTS = %i(major minor patch)
|
6
|
+
|
7
|
+
attr_reader :semver
|
8
|
+
|
9
|
+
VERSION_PARTS.each do |m|
|
10
|
+
define_method m do
|
11
|
+
load_manifest
|
12
|
+
read_version
|
13
|
+
normalize_version
|
14
|
+
super()
|
15
|
+
update_version
|
16
|
+
write_manifest
|
17
|
+
post_actions
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def manifest_json_path
|
24
|
+
'manifest.json'
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_manifest
|
28
|
+
manifest_json = File.read(manifest_json_path)
|
29
|
+
@manifest = JSON.load(manifest_json)
|
30
|
+
rescue => e
|
31
|
+
say(e.message, :red) and exit 1
|
32
|
+
end
|
33
|
+
|
34
|
+
def read_version
|
35
|
+
version = @manifest.fetch('version', '0.0.0')
|
36
|
+
sem_parts = sub_semver(version)
|
37
|
+
@semver = sem_parts.names.map(&:to_sym).zip(sem_parts.to_a.drop(1)).to_h
|
38
|
+
end
|
39
|
+
|
40
|
+
def normalize_version
|
41
|
+
VERSION_PARTS.each do |part|
|
42
|
+
semver[part] = (semver[part] || '0').to_i
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_version
|
47
|
+
@manifest['version'] = version
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_manifest
|
51
|
+
File.open(manifest_json_path, 'w') do |f|
|
52
|
+
f.write(JSON.pretty_generate(@manifest))
|
53
|
+
f.write("\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def sub_semver(v)
|
58
|
+
v.match(/\A(?<v>v)?(?<major>\d+)(?:\.(?<minor>\d+)(?:\.(?<patch>\d+))?)?\Z/)
|
59
|
+
end
|
60
|
+
|
61
|
+
def version(v: false)
|
62
|
+
[
|
63
|
+
v ? 'v' : semver[:v],
|
64
|
+
[
|
65
|
+
semver[:major],
|
66
|
+
semver[:minor],
|
67
|
+
semver[:patch]
|
68
|
+
].compact.map(&:to_s).join('.')
|
69
|
+
].compact.join
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ZendeskAppsTools
|
2
|
+
require 'zendesk_apps_support'
|
3
|
+
|
4
|
+
module PackageHelper
|
5
|
+
include ZendeskAppsSupport
|
6
|
+
|
7
|
+
def app_package
|
8
|
+
@app_package ||= Package.new(app_dir.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
def zip(archive_path)
|
12
|
+
Zip::ZipFile.open(archive_path, 'w') do |zipfile|
|
13
|
+
app_package.files.each do |file|
|
14
|
+
relative_path = file.relative_path
|
15
|
+
path = relative_path
|
16
|
+
say_status 'package', "adding #{path}"
|
17
|
+
|
18
|
+
# resolve symlink to source path
|
19
|
+
if File.symlink? file.absolute_path
|
20
|
+
path = File.expand_path(File.readlink(file.absolute_path), File.dirname(file.absolute_path))
|
21
|
+
end
|
22
|
+
if file.to_s == 'app.scss'
|
23
|
+
relative_path = relative_path.sub 'app.scss', 'app.css'
|
24
|
+
end
|
25
|
+
zipfile.add(relative_path, app_dir.join(path).to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'zendesk_apps_support/package'
|
3
|
+
|
4
|
+
module ZendeskAppsTools
|
5
|
+
class Server < Sinatra::Base
|
6
|
+
set :protection, :except => :frame_options
|
7
|
+
last_mtime = Time.new(0)
|
8
|
+
ZENDESK_DOMAINS_REGEX = /^http(?:s)?:\/\/[a-z0-9-]+\.(?:zendesk|zopim|zd-(?:dev|master|staging))\.com$/
|
9
|
+
|
10
|
+
get '/app.js' do
|
11
|
+
access_control_allow_origin
|
12
|
+
content_type 'text/javascript'
|
13
|
+
|
14
|
+
if File.exists? settings.config
|
15
|
+
curr_mtime = File.stat(settings.config).mtime
|
16
|
+
if curr_mtime > last_mtime
|
17
|
+
settings_helper = ZendeskAppsTools::Settings.new
|
18
|
+
settings.parameters = settings_helper.get_settings_from_file(settings.config, settings.manifest)
|
19
|
+
last_mtime = curr_mtime
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
package = ZendeskAppsSupport::Package.new(settings.root, false)
|
24
|
+
app_name = package.manifest_json['name'] || 'Local App'
|
25
|
+
installation = ZendeskAppsSupport::Installation.new(
|
26
|
+
id: settings.app_id,
|
27
|
+
app_id: settings.app_id,
|
28
|
+
app_name: app_name,
|
29
|
+
enabled: true,
|
30
|
+
requirements: package.requirements_json,
|
31
|
+
settings: settings.parameters.merge({title: app_name}),
|
32
|
+
updated_at: Time.now.iso8601,
|
33
|
+
created_at: Time.now.iso8601
|
34
|
+
)
|
35
|
+
|
36
|
+
app_js = package.compile_js(
|
37
|
+
app_id: settings.app_id,
|
38
|
+
app_name: package.manifest_json['name'] || 'Local App',
|
39
|
+
assets_dir: "http://localhost:#{settings.port}/",
|
40
|
+
locale: params['locale']
|
41
|
+
)
|
42
|
+
|
43
|
+
ZendeskAppsSupport::Installed.new([app_js], [installation]).compile_js
|
44
|
+
end
|
45
|
+
|
46
|
+
get "/:file" do |file|
|
47
|
+
access_control_allow_origin
|
48
|
+
send_file File.join(settings.root, 'assets', file)
|
49
|
+
end
|
50
|
+
|
51
|
+
# This is for any preflight request
|
52
|
+
# It reads 'Access-Control-Request-Headers' to set 'Access-Control-Allow-Headers'
|
53
|
+
# And also sets 'Access-Control-Allow-Origin' header
|
54
|
+
options "*" do
|
55
|
+
access_control_allow_origin
|
56
|
+
headers 'Access-Control-Allow-Headers' => request.env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] if request.env['HTTP_ORIGIN'] =~ ZENDESK_DOMAINS_REGEX
|
57
|
+
end
|
58
|
+
|
59
|
+
# This sets the 'Access-Control-Allow-Origin' header for requests coming from zendesk
|
60
|
+
def access_control_allow_origin
|
61
|
+
origin = request.env['HTTP_ORIGIN']
|
62
|
+
headers 'Access-Control-Allow-Origin' => origin if origin =~ ZENDESK_DOMAINS_REGEX
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'zendesk_apps_tools/common'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module ZendeskAppsTools
|
5
|
+
class Settings
|
6
|
+
def get_settings_from_user_input(user_input, parameters)
|
7
|
+
return {} if parameters.nil?
|
8
|
+
|
9
|
+
parameters.inject({}) do |settings, param|
|
10
|
+
if param['default']
|
11
|
+
input = user_input.get_value_from_stdin("Enter a value for parameter '#{param['name']}' or press 'Return' to use the default value '#{param['default']}':\n", allow_empty: true)
|
12
|
+
input = param['default'] if input.empty?
|
13
|
+
elsif param['required']
|
14
|
+
input = user_input.get_value_from_stdin("Enter a value for required parameter '#{param['name']}':\n")
|
15
|
+
else
|
16
|
+
input = user_input.get_value_from_stdin("Enter a value for optional parameter '#{param['name']}' or press 'Return' to skip:\n", allow_empty: true)
|
17
|
+
end
|
18
|
+
|
19
|
+
if param['type'] == 'checkbox'
|
20
|
+
input = convert_to_boolean_for_checkbox(input)
|
21
|
+
end
|
22
|
+
|
23
|
+
settings[param['name']] = input if input != ''
|
24
|
+
settings
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_settings_from_file(filepath, parameters)
|
29
|
+
return {} if parameters.nil?
|
30
|
+
return nil unless File.exist? filepath
|
31
|
+
|
32
|
+
begin
|
33
|
+
settings_file = File.read(filepath)
|
34
|
+
|
35
|
+
if filepath =~ /\.json$/ || settings_file =~ /\A\s*{/
|
36
|
+
settings_data = JSON.load(settings_file)
|
37
|
+
else
|
38
|
+
settings_data = YAML.load(settings_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
settings_data.each do |index, setting|
|
42
|
+
if setting.is_a?(Hash) || setting.is_a?(Array)
|
43
|
+
settings_data[index] = JSON.dump(setting)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
rescue => err
|
47
|
+
puts "Failed to load #{filepath}"
|
48
|
+
puts err.message
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
|
52
|
+
parameters.inject({}) do |settings, param|
|
53
|
+
input = settings_data[param['name']]
|
54
|
+
|
55
|
+
if !input && param['default']
|
56
|
+
input = param['default']
|
57
|
+
end
|
58
|
+
|
59
|
+
if !input && param['required']
|
60
|
+
puts "'#{param['name']}' is required but not specified in the config file.\n"
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
|
64
|
+
if param['type'] == 'checkbox'
|
65
|
+
input = convert_to_boolean_for_checkbox(input)
|
66
|
+
end
|
67
|
+
|
68
|
+
settings[param['name']] = input if input != ''
|
69
|
+
settings
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def convert_to_boolean_for_checkbox(input)
|
76
|
+
unless [TrueClass, FalseClass].include?(input.class)
|
77
|
+
return (input =~ /^(true|t|yes|y|1)$/i) ? true : false
|
78
|
+
end
|
79
|
+
input
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|