txbr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|