txbr 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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +137 -0
- data/lib/txbr.rb +81 -0
- data/lib/txbr/application.rb +48 -0
- data/lib/txbr/braze_api.rb +42 -0
- data/lib/txbr/braze_session.rb +39 -0
- data/lib/txbr/braze_session_api.rb +73 -0
- data/lib/txbr/commands.rb +14 -0
- data/lib/txbr/config.rb +57 -0
- data/lib/txbr/email_template.rb +70 -0
- data/lib/txbr/email_template_component.rb +94 -0
- data/lib/txbr/email_template_handler.rb +24 -0
- data/lib/txbr/project.rb +50 -0
- data/lib/txbr/request_methods.rb +45 -0
- data/lib/txbr/strings_manifest.rb +55 -0
- data/lib/txbr/tasks.rb +8 -0
- data/lib/txbr/uploader.rb +23 -0
- data/lib/txbr/utils.rb +9 -0
- data/lib/txbr/version.rb +3 -0
- data/spec/application_spec.rb +78 -0
- data/spec/braze_session_api_spec.rb +108 -0
- data/spec/braze_session_spec.rb +49 -0
- data/spec/config_spec.rb +69 -0
- data/spec/email_template_spec.rb +133 -0
- data/spec/fixtures/cassettes/braze_login.yml +324 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/strings_manifest_spec.rb +45 -0
- data/spec/support/env_helpers.rb +13 -0
- data/spec/support/fake_braze_session.rb +14 -0
- data/spec/support/fake_connection.rb +78 -0
- data/spec/support/standard_setup.rb +20 -0
- data/spec/support/test_config.rb +20 -0
- data/spec/uploader_spec.rb +84 -0
- data/spec/utils_spec.rb +17 -0
- data/txbr.gemspec +27 -0
- metadata +190 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'faraday_middleware'
|
|
3
|
+
|
|
4
|
+
module Txbr
|
|
5
|
+
class BrazeApi
|
|
6
|
+
include RequestMethods
|
|
7
|
+
|
|
8
|
+
attr_reader :api_key, :api_url
|
|
9
|
+
|
|
10
|
+
def initialize(api_key, api_url)
|
|
11
|
+
@api_key = api_key
|
|
12
|
+
@api_url = api_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def each_email_template
|
|
16
|
+
raise NotImplementedError, 'Braze does not support this operation yet'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_email_template_details(email_template_id:)
|
|
20
|
+
raise NotImplementedError, 'Braze does not support this operation yet'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def connection
|
|
26
|
+
@connection ||= begin
|
|
27
|
+
options = {
|
|
28
|
+
url: api_url,
|
|
29
|
+
params: { api_key: api_key },
|
|
30
|
+
headers: { Accept: 'application/json' }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Faraday.new(options) do |faraday|
|
|
34
|
+
faraday.request(:json)
|
|
35
|
+
faraday.response(:logger)
|
|
36
|
+
faraday.use(FaradayMiddleware::FollowRedirects)
|
|
37
|
+
faraday.adapter(:net_http)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require 'mechanize'
|
|
3
|
+
|
|
4
|
+
module Txbr
|
|
5
|
+
class BrazeSession
|
|
6
|
+
attr_reader :api_url, :email_address, :password
|
|
7
|
+
|
|
8
|
+
def initialize(api_url, email_address, password)
|
|
9
|
+
@api_url = api_url
|
|
10
|
+
@email_address = email_address
|
|
11
|
+
@password = password
|
|
12
|
+
|
|
13
|
+
reset!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def session_id
|
|
17
|
+
@session_id ||= begin
|
|
18
|
+
agent = Mechanize.new
|
|
19
|
+
url = Txbr::Utils.url_join(api_url, "auth?email=#{CGI.escape(email_address)}")
|
|
20
|
+
|
|
21
|
+
agent.get(url) do |page|
|
|
22
|
+
page.form_with(id: 'developer_signin').tap do |form|
|
|
23
|
+
form['developer[password]'] = password
|
|
24
|
+
form.submit
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
agent
|
|
29
|
+
.cookies
|
|
30
|
+
.find { |cookie| cookie.name == '_session_id' }
|
|
31
|
+
.value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset!
|
|
36
|
+
@session_id = nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'faraday_middleware'
|
|
3
|
+
|
|
4
|
+
module Txbr
|
|
5
|
+
class BrazeSessionApi
|
|
6
|
+
EMAIL_TEMPLATE_BATCH_SIZE = 35
|
|
7
|
+
|
|
8
|
+
include RequestMethods
|
|
9
|
+
|
|
10
|
+
attr_reader :session, :app_group_id
|
|
11
|
+
|
|
12
|
+
def initialize(session, app_group_id, connection: nil)
|
|
13
|
+
@session = session
|
|
14
|
+
@app_group_id = app_group_id
|
|
15
|
+
@connection = connection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each_email_template(start: 0, &block)
|
|
19
|
+
return to_enum(__method__, start: start) unless block_given?
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
templates = get_json(
|
|
23
|
+
'engagement/email_templates',
|
|
24
|
+
start: start,
|
|
25
|
+
limit: EMAIL_TEMPLATE_BATCH_SIZE
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
templates['results'].each(&block)
|
|
29
|
+
start += templates['results'].size
|
|
30
|
+
break if templates['results'].size < EMAIL_TEMPLATE_BATCH_SIZE
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def get_email_template_details(email_template_id:)
|
|
35
|
+
get_json("engagement/email_templates/#{email_template_id}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def act(*args)
|
|
41
|
+
retried = false
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
super(*args, { cookie: "_session_id=#{session.session_id}" })
|
|
45
|
+
rescue BrazeUnauthorizedError => e
|
|
46
|
+
raise e if retried
|
|
47
|
+
reset!
|
|
48
|
+
retried = true
|
|
49
|
+
retry
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reset!
|
|
54
|
+
session.reset!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def connection
|
|
58
|
+
@connection ||= begin
|
|
59
|
+
options = {
|
|
60
|
+
url: session.api_url,
|
|
61
|
+
params: { app_group_id: app_group_id }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Faraday.new(options) do |faraday|
|
|
65
|
+
faraday.request(:json)
|
|
66
|
+
faraday.response(:logger)
|
|
67
|
+
faraday.use(FaradayMiddleware::FollowRedirects)
|
|
68
|
+
faraday.adapter(:net_http)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/txbr/config.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module Txbr
|
|
4
|
+
class Config
|
|
5
|
+
class << self
|
|
6
|
+
def projects
|
|
7
|
+
@projects ||= raw_config[:projects].map do |conf|
|
|
8
|
+
Project.new(conf)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def transifex_api_username
|
|
13
|
+
raw_config[:transifex_api_username]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def transifex_api_password
|
|
17
|
+
raw_config[:transifex_api_password]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def raw_config
|
|
23
|
+
@raw_config ||= begin
|
|
24
|
+
scheme_end_idx = ENV['TXBR_CONFIG'].index('://')
|
|
25
|
+
scheme = ENV['TXBR_CONFIG'][0...scheme_end_idx]
|
|
26
|
+
payload = ENV['TXBR_CONFIG'][(scheme_end_idx + 3)..-1]
|
|
27
|
+
send(:"load_#{scheme}", payload)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load_file(payload)
|
|
32
|
+
deep_symbolize_keys(YAML.load_file(payload))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def load_raw(payload)
|
|
36
|
+
deep_symbolize_keys(YAML.load(payload))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deep_symbolize_keys(obj)
|
|
40
|
+
case obj
|
|
41
|
+
when Hash
|
|
42
|
+
obj.each_with_object({}) do |(k, v), ret|
|
|
43
|
+
ret[k.to_sym] = deep_symbolize_keys(v)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
when Array
|
|
47
|
+
obj.map do |elem|
|
|
48
|
+
deep_symbolize_keys(elem)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
else
|
|
52
|
+
obj
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'liquid'
|
|
2
|
+
|
|
3
|
+
module Txbr
|
|
4
|
+
class EmailTemplate
|
|
5
|
+
attr_reader :project, :email_template_id
|
|
6
|
+
|
|
7
|
+
def initialize(project, email_template_id)
|
|
8
|
+
@project = project
|
|
9
|
+
@email_template_id = email_template_id
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def each_resource
|
|
13
|
+
return to_enum(__method__) unless block_given?
|
|
14
|
+
|
|
15
|
+
strings_map.each_pair do |project_slug, resource_map|
|
|
16
|
+
resource_map.each_pair do |resource_slug, strings_manifest|
|
|
17
|
+
phrases = strings_manifest.each_string
|
|
18
|
+
.reject { |_, value| value.nil? }
|
|
19
|
+
.map do |path, value|
|
|
20
|
+
{ 'key' => path.join('.'), 'string' => value }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
next if phrases.empty?
|
|
24
|
+
|
|
25
|
+
resource = Txgh::TxResource.new(
|
|
26
|
+
project_slug,
|
|
27
|
+
resource_slug,
|
|
28
|
+
project.strings_format,
|
|
29
|
+
project.source_lang,
|
|
30
|
+
template_name,
|
|
31
|
+
{}, # lang_map (none)
|
|
32
|
+
nil # translation_file (none)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
yield Txgh::ResourceContents.from_phrase_list(resource, phrases)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def strings_map
|
|
43
|
+
@strings_map ||= %w(template subject preheader).each_with_object({}) do |name, ret|
|
|
44
|
+
component = EmailTemplateComponent.new(
|
|
45
|
+
Liquid::Template.parse(details[name])
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
next unless component.assignments['project_slug']
|
|
49
|
+
next unless component.assignments['resource_slug']
|
|
50
|
+
next unless component.translation_enabled?
|
|
51
|
+
|
|
52
|
+
(ret[component.project_slug] ||= {}).tap do |proj_map|
|
|
53
|
+
(proj_map[component.resource_slug] ||= StringsManifest.new).tap do |manifest|
|
|
54
|
+
manifest.merge!(component.strings)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def template_name
|
|
61
|
+
details['name']
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def details
|
|
65
|
+
@details ||= project.braze_api.get_email_template_details(
|
|
66
|
+
email_template_id: email_template_id
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module Txbr
|
|
2
|
+
class EmailTemplateComponent
|
|
3
|
+
ASSIGNMENTS = %w(project_slug resource_slug translation_enabled)
|
|
4
|
+
|
|
5
|
+
attr_reader :liquid_template
|
|
6
|
+
|
|
7
|
+
def initialize(liquid_template)
|
|
8
|
+
@liquid_template = liquid_template
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def project_slug
|
|
12
|
+
# blow up with KeyError if not found
|
|
13
|
+
assignments.fetch('project_slug')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def resource_slug
|
|
17
|
+
# blow up with KeyError if not found
|
|
18
|
+
assignments.fetch('resource_slug')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def translation_enabled?
|
|
22
|
+
# translation is disabled by default
|
|
23
|
+
assignments.fetch('translation_enabled', true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def assignments
|
|
27
|
+
@assignments ||= {}.tap do |assgn|
|
|
28
|
+
liquid_template.root.nodelist.each do |node|
|
|
29
|
+
case node
|
|
30
|
+
when Liquid::Assign
|
|
31
|
+
to = node.instance_variable_get(:@to)
|
|
32
|
+
|
|
33
|
+
if ASSIGNMENTS.include?(to)
|
|
34
|
+
from = node.instance_variable_get(:@from).name
|
|
35
|
+
assgn[to] = from
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def strings
|
|
43
|
+
@strings ||= StringsManifest.new.tap do |manifest|
|
|
44
|
+
extract_strings_from(liquid_template.root, manifest)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def extract_strings_from(root, manifest)
|
|
51
|
+
return unless root.nodelist
|
|
52
|
+
|
|
53
|
+
root.nodelist.each do |node|
|
|
54
|
+
case node
|
|
55
|
+
# We only care about Liquid variables, which are written
|
|
56
|
+
# like {{prefix.foo.bar}}. We identify the prefix (i.e.
|
|
57
|
+
# the first lookup, or path segment) to verify it's
|
|
58
|
+
# associated with a connected_content call. Then we add
|
|
59
|
+
# the prefix and the rest of the lookups to the strings
|
|
60
|
+
# manifest along with the value. The prefix is used to
|
|
61
|
+
# divide the strings into individual Transifex resources
|
|
62
|
+
# while the rest of the lookups form the string's key.
|
|
63
|
+
when Liquid::Variable
|
|
64
|
+
prefix = node.name.name
|
|
65
|
+
path = node.name.lookups
|
|
66
|
+
|
|
67
|
+
# the English translation (or whatever language your
|
|
68
|
+
# source strings are written in) is provided using
|
|
69
|
+
# Liquid's built-in "default" filter
|
|
70
|
+
next unless connected_content_prefixes.include?(prefix)
|
|
71
|
+
default = node.filters.find { |f| f.first == 'default' }
|
|
72
|
+
|
|
73
|
+
manifest.add(path, default&.last&.first)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if node.respond_to?(:nodelist)
|
|
77
|
+
extract_strings_from(node, manifest)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def connected_content_prefixes
|
|
83
|
+
@connected_content_prefixes ||= connected_content_tags.map(&:prefix)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def connected_content_tags
|
|
87
|
+
# this assumes these are basically at the top of the template and not
|
|
88
|
+
# nested inside other liquid tags
|
|
89
|
+
@connected_content_tags ||= liquid_template.root.nodelist.select do |node|
|
|
90
|
+
node.is_a?(Txbr::ConnectedContentTag)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Txbr
|
|
2
|
+
class EmailTemplateHandler
|
|
3
|
+
attr_reader :project
|
|
4
|
+
|
|
5
|
+
def initialize(project)
|
|
6
|
+
@project = project
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def each_resource(&block)
|
|
10
|
+
return to_enum(__method__) unless block_given?
|
|
11
|
+
each_template { |tmpl| tmpl.each_resource(&block) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def each_template
|
|
17
|
+
return to_enum(__method__) unless block_given?
|
|
18
|
+
|
|
19
|
+
project.braze_api.each_email_template do |tmpl|
|
|
20
|
+
yield EmailTemplate.new(project, tmpl['id'])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/txbr/project.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'txgh'
|
|
2
|
+
|
|
3
|
+
module Txbr
|
|
4
|
+
class Project
|
|
5
|
+
attr_reader :braze_api_url, :braze_api_key, :handler_id
|
|
6
|
+
attr_reader :strings_format, :source_lang
|
|
7
|
+
|
|
8
|
+
# @TODO: remove these when Braze gives us the endpoints we asked for
|
|
9
|
+
attr_reader :braze_app_group_id, :braze_email_address, :braze_password
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@braze_api_url = options.fetch(:braze_api_url)
|
|
13
|
+
@braze_api_key = options.fetch(:braze_api_key)
|
|
14
|
+
@handler_id = options.fetch(:handler_id)
|
|
15
|
+
@strings_format = options.fetch(:strings_format)
|
|
16
|
+
@source_lang = options.fetch(:source_lang)
|
|
17
|
+
|
|
18
|
+
# @TODO: remove these when Braze gives us the endpoints we asked for
|
|
19
|
+
@braze_app_group_id = options.fetch(:braze_app_group_id)
|
|
20
|
+
@braze_email_address = options.fetch(:braze_email_address)
|
|
21
|
+
@braze_password = options.fetch(:braze_password)
|
|
22
|
+
|
|
23
|
+
@braze_api = options[:braze_api]
|
|
24
|
+
@transifex_api = options[:transifex_api]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def braze_session
|
|
28
|
+
@@braze_session ||= Txbr::BrazeSession.new(
|
|
29
|
+
braze_api_url, braze_email_address, braze_password
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def braze_api
|
|
34
|
+
# @TODO: use BrazeApi when Braze gives us the endpoints we asked for
|
|
35
|
+
# @braze_api ||= Txbr::BrazeApi.new(braze_api_key, braze_api_url)
|
|
36
|
+
@braze_api ||= Txbr::BrazeSessionApi.new(braze_session, braze_app_group_id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def transifex_api
|
|
40
|
+
@transifex_api ||= Txgh::TransifexApi.create_from_credentials(
|
|
41
|
+
Txbr::Config.transifex_api_username,
|
|
42
|
+
Txbr::Config.transifex_api_password
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handler
|
|
47
|
+
@handler ||= Txbr.handler_for(self)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|