txbr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,14 @@
1
+ module Txbr
2
+ module Commands
3
+ def self.upload_all
4
+ Txbr::Config.projects.each do |project|
5
+ begin
6
+ Txbr::Uploader.new(project).upload_all
7
+ rescue => e
8
+ puts "An error occurred: #{e.message}"
9
+ puts e.backtrace
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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
@@ -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