bns 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f27d7368a358fcf6dea96e51922690ff399670b5582387b70f56db0d5b25feb
4
- data.tar.gz: a46adf0a088dd65740c85aadc5d3005654f3654e2f12cecf345700be7f44f2b1
3
+ metadata.gz: f45452ff8e3569ad4ffed2d32d8cc7801cd5a4379205ca70d7d401814abb7d50
4
+ data.tar.gz: 64cd26aaa2269270f77f50ec5b1d4371272d4f53d280bed5b0ccd727b0e628b7
5
5
  SHA512:
6
- metadata.gz: bfa9c1394d5fe161f90fe86e29a44acace8b2a7809ee8bd233ee12d0be082b11a0081e3fb98efefcdfe22c64a50f27335a95ac24f9c8baeec10c5a9f8d381754
7
- data.tar.gz: a13978067c0c9ad5a96c8abc8ae2f7781674a0f4b3accc9b2229a11dde525f706a52ee448effad8570c34b2141a5e9c3ad0a3457f29dea2f5d60d42c6ba7bd4e
6
+ metadata.gz: a61ce154f73bcf97b76828f437cf4cbd0af1809458532de9ec232343e9a92c85ec118d336a5f13b4eb95cb8e792e2b10bd941c90c79c720c2ae7b4fd5272543f
7
+ data.tar.gz: '088fc7b072e971ebc37259cc2e017bcc22aa69d9c8d2f035958bb33b578108235686f889c1faccdb6a6566e39b44cddacc6a356c737a81924cebd10d6379d057'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (19.03.2024)
4
+ - [Add Component - Email fetcher](https://github.com/kommitters/bns/issues/40)
5
+ - [Use case - Email based notifications](https://github.com/kommitters/bns/issues/45)
6
+ - [Dynamic WIP limit value](https://github.com/kommitters/bns/issues/48)
7
+
3
8
  ## 0.2.0 (29.02.2024)
4
9
  - [Add a Postgres fetcher component](https://github.com/kommitters/bns/issues/28)
5
10
  - [Use case - PTO's Postgres-Slack implementation](https://github.com/kommitters/bns/issues/30)
data/Gemfile CHANGED
@@ -7,6 +7,8 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
9
 
10
+ gem "net-imap", "~> 0.4.10"
11
+ gem "net-smtp", "~> 0.4.0.1"
10
12
  gem "rspec", "~> 3.0"
11
13
  gem "rubocop", "~> 1.21"
12
14
  gem "simplecov", require: false, group: :test
@@ -18,3 +20,5 @@ gem "webmock"
18
20
  gem "httparty"
19
21
 
20
22
  gem "pg", "~> 1.5", ">= 1.5.4"
23
+
24
+ gem "gmail_xoauth"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domain
4
+ ##
5
+ # The Domain::Email class provides a domain-specific representation of an Email object.
6
+ # It encapsulates information about an email, including the subject, the sender, and the date.
7
+ #
8
+ class Email
9
+ attr_reader :subject, :sender
10
+ attr_accessor :date
11
+
12
+ ATTRIBUTES = %w[subject sender date].freeze
13
+
14
+ # Initializes a Domain::Email instance with the specified subject, sender, and date.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>String</tt> email subject.
19
+ # * <tt>String</tt> Email of the sender.
20
+ # * <tt>String</tt> Reception date
21
+ #
22
+ def initialize(subject, sender, date)
23
+ @subject = subject
24
+ @sender = sender
25
+ @date = parse_to_datetime(date)
26
+ end
27
+
28
+ private
29
+
30
+ def parse_to_datetime(date)
31
+ DateTime.parse(date).to_time
32
+ end
33
+ end
34
+ end
@@ -3,37 +3,23 @@
3
3
  module Domain
4
4
  ##
5
5
  # The Domain::WorkItemsLimit class provides a domain-specific representation of a Work Item object.
6
- # It encapsulates information about a work items limit, including the domain, total, and wip_limit.
6
+ # It encapsulates information about a work items limit, including the domain and total.
7
7
  #
8
8
  class WorkItemsLimit
9
- attr_reader :domain, :total, :wip_limit
9
+ attr_reader :domain, :total
10
10
 
11
- ATTRIBUTES = %w[domain total wip_limit].freeze
11
+ ATTRIBUTES = %w[domain total].freeze
12
12
 
13
- # Initializes a Domain::WorkItemsLimit instance with the specified domain, total and wip_limit.
13
+ # Initializes a Domain::WorkItemsLimit instance with the specified domain and total.
14
14
  #
15
15
  # <br>
16
16
  # <b>Params:</b>
17
17
  # * <tt>String</tt> 'domain' responsible domain of the work items.
18
18
  # * <tt>String</tt> 'total' total 'in progress' work items.
19
- # * <tt>String</tt> 'wip_limit' maximum 'in progress' work items for the domain
20
19
  #
21
20
  def initialize(domain, total)
22
21
  @domain = domain
23
22
  @total = total
24
- @wip_limit = domain_wip_limit(domain)
25
- end
26
-
27
- private
28
-
29
- def domain_wip_limit(domain)
30
- case domain
31
- when "kommit.ops" then 5
32
- when "kommit.sales" then 3
33
- when "kommit.marketing" then 4
34
- when "kommit.engineering" then 12
35
- else 6
36
- end
37
23
  end
38
24
  end
39
25
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/imap"
4
+ require "gmail_xoauth"
5
+
6
+ require_relative "../base"
7
+ require_relative "./types/response"
8
+
9
+ module Fetcher
10
+ module Imap
11
+ ##
12
+ # This class is an implementation of the Fetcher::Base interface, specifically designed
13
+ # for fetching data from an IMAP server.
14
+ #
15
+ class Base < Fetcher::Base
16
+ protected
17
+
18
+ # Implements the data fetching logic for emails data from an IMAP server.
19
+ # It connects to an IMAP server inbox, request emails base on a filter,
20
+ # and returns a validated response.
21
+ #
22
+ def execute(email_domain, email_port, token_uri, query)
23
+ access_token = refresh_token(token_uri)
24
+
25
+ imap_fetch(email_domain, email_port, query, access_token)
26
+
27
+ Fetcher::Imap::Types::Response.new(@emails)
28
+ end
29
+
30
+ private
31
+
32
+ def imap_fetch(email_domain, email_port, query, access_token)
33
+ imap = Net::IMAP.new(email_domain, port: email_port, ssl: true)
34
+
35
+ imap.authenticate("XOAUTH2", config[:user], access_token)
36
+
37
+ imap.examine(config[:inbox])
38
+
39
+ @emails = fetch_emails(imap, query)
40
+
41
+ imap.logout
42
+ imap.disconnect
43
+ end
44
+
45
+ def fetch_emails(imap, query)
46
+ imap.search(query).map do |message_id|
47
+ imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"]
48
+ end
49
+ end
50
+
51
+ def refresh_token(token_uri)
52
+ uri = URI.parse(token_uri)
53
+
54
+ response = Net::HTTP.post_form(uri, params)
55
+ token_data = JSON.parse(response.body)
56
+
57
+ token_data["access_token"]
58
+ end
59
+
60
+ def params
61
+ {
62
+ "grant_type" => "refresh_token",
63
+ "refresh_token" => config[:refresh_token],
64
+ "client_id" => config[:client_id],
65
+ "client_secret" => config[:client_secret]
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fetcher
4
+ module Imap
5
+ module Types
6
+ ##
7
+ # Represents a response received from the Imap client. It encapsulates essential
8
+ # information about the response, providing a structured way to handle and analyze
9
+ # it's responses.
10
+ class Response
11
+ attr_reader :status_code, :message, :results
12
+
13
+ def initialize(response)
14
+ if response.empty?
15
+ @status_code = 404
16
+ @message = "no result were found"
17
+ @results = []
18
+ else
19
+ @status_code = 200
20
+ @message = "success"
21
+ @results = response
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module Fetcher
6
+ module Imap
7
+ ##
8
+ # This class is an implementation of the Fetcher::Imap::Base interface, specifically designed
9
+ # for fetching support email from a Google Gmail account.
10
+ #
11
+ class SupportEmails < Imap::Base
12
+ TOKEN_URI = "https://oauth2.googleapis.com/token"
13
+ EMAIL_DOMAIN = "imap.gmail.com"
14
+ EMAIL_PORT = 993
15
+
16
+ # Implements the data fetching filter for support emails from Google Gmail.
17
+ #
18
+ def fetch
19
+ yesterday = (Time.now - (60 * 60 * 24)).strftime("%e-%b-%Y")
20
+ query = ["TO", config[:search_email], "SINCE", yesterday]
21
+
22
+ execute(EMAIL_DOMAIN, EMAIL_PORT, TOKEN_URI, query)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  require_relative "../domain/pto"
4
6
  require_relative "./exceptions/invalid_data"
5
7
  require_relative "./base"
@@ -9,13 +11,15 @@ module Formatter
9
11
  # This class implements methods from the Formatter::Base module, tailored to format the
10
12
  # Domain::Pto structure for a dispatcher.
11
13
  class Pto < Base
14
+ DEFAULT_TIME_ZONE = "+00:00"
15
+
12
16
  # Initializes the Slack formatter with essential configuration parameters.
13
17
  #
14
18
  # <b>timezone</b> : expect an string with the time difference relative to the UTC. Example: "-05:00"
15
19
  def initialize(config = {})
16
20
  super(config)
17
21
 
18
- @timezone = config[:timezone]
22
+ @timezone = config[:timezone] || DEFAULT_TIME_ZONE
19
23
  end
20
24
 
21
25
  # Implements the logic for building a formatted payload with the given template for PTO's.
@@ -64,7 +68,9 @@ module Formatter
64
68
  end
65
69
 
66
70
  def format_timezone(date)
67
- @timezone.nil? ? Time.new(date) : Time.new(date, in: @timezone)
71
+ date_time = build_date(date)
72
+
73
+ Time.at(date_time, in: @timezone)
68
74
  end
69
75
 
70
76
  def today?(date)
@@ -72,5 +78,11 @@ module Formatter
72
78
 
73
79
  date == format_timezone(time_now).strftime("%F")
74
80
  end
81
+
82
+ def build_date(date)
83
+ date_time = date.include?("T") ? date : "#{date}T00:00:00.000#{@timezone}"
84
+
85
+ DateTime.parse(date_time).to_time
86
+ end
75
87
  end
76
88
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/email"
4
+ require_relative "./exceptions/invalid_data"
5
+ require_relative "./base"
6
+
7
+ module Formatter
8
+ ##
9
+ # This class implements methods from the Formatter::Base module, tailored to format the
10
+ # Domain::Email structure for a dispatcher.
11
+ class SupportEmails < Base
12
+ DEFAULT_TIME_ZONE = "+00:00"
13
+
14
+ # Initializes the formatter with essential configuration parameters.
15
+ #
16
+ # <b>timezone</b> : expect an string with the time difference relative to the UTC. Example: "-05:00"
17
+ def initialize(config = {})
18
+ super(config)
19
+
20
+ @timezone = config[:timezone] || DEFAULT_TIME_ZONE
21
+ @frecuency = config[:frecuency]
22
+ end
23
+
24
+ # Implements the logic for building a formatted payload with the given template for support emails.
25
+ #
26
+ # <br>
27
+ # <b>Params:</b>
28
+ # * <tt>List<Domain::Email></tt> support_emails_list: list of support emails.
29
+ #
30
+ # <br>
31
+ # <b>raises</b> <tt>Formatter::Exceptions::InvalidData</tt> when invalid data is provided.
32
+ #
33
+ # <br>
34
+ # <b>returns</b> <tt>String</tt> payload: formatted payload suitable for a Dispatcher.
35
+ #
36
+ def format(support_emails_list)
37
+ raise Formatter::Exceptions::InvalidData unless support_emails_list.all? do |support_email|
38
+ support_email.is_a?(Domain::Email)
39
+ end
40
+
41
+ process_emails(support_emails_list).reduce("") do |payload, support_email|
42
+ payload + build_template(Domain::Email::ATTRIBUTES, support_email)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def process_emails(emails)
49
+ emails.each { |email| email.date = at_timezone(email.date) }
50
+ emails.filter! { |email| email.date > time_window } unless @frecuency.nil?
51
+
52
+ format_timestamp(emails)
53
+ end
54
+
55
+ def format_timestamp(emails)
56
+ emails.each { |email| email.date = email.date.strftime("%F %r") }
57
+ end
58
+
59
+ def time_window
60
+ date_time = Time.now - (60 * 60 * @frecuency)
61
+
62
+ at_timezone(date_time)
63
+ end
64
+
65
+ def at_timezone(date)
66
+ Time.at(date, in: @timezone)
67
+ end
68
+ end
69
+ end
@@ -9,7 +9,16 @@ module Formatter
9
9
  # This class implements methods from the Formatter::Base module, tailored to format the
10
10
  # Domain::WorkItemsLimit structure for a dispatcher.
11
11
  class WorkItemsLimit < Base
12
- attr_reader :limit
12
+ DEFAULT_DOMAIN_LIMIT = 6
13
+
14
+ # Initializes the formatter with essential configuration parameters.
15
+ #
16
+ # <b>limits</b> : expect a map with the wip limits by domain. Example: { "ops": 5 }
17
+ def initialize(config = {})
18
+ super(config)
19
+
20
+ @limits = config[:limits]
21
+ end
13
22
 
14
23
  # Implements the logic for building a formatted payload with the given template for wip limits.
15
24
  #
@@ -30,14 +39,26 @@ module Formatter
30
39
  end
31
40
 
32
41
  exceeded_domains(work_items_list).reduce("") do |payload, work_items_limit|
33
- payload + build_template(Domain::WorkItemsLimit::ATTRIBUTES, work_items_limit)
42
+ built_template = build_template(Domain::WorkItemsLimit::ATTRIBUTES, work_items_limit)
43
+ payload + format_message_by_case(built_template.gsub("\n", ""), work_items_limit)
34
44
  end
35
45
  end
36
46
 
37
47
  private
38
48
 
49
+ def format_message_by_case(template, work_items_limit)
50
+ total_items = work_items_limit.total
51
+ limit = domain_limit(work_items_limit.domain)
52
+
53
+ template + ", #{total_items} of #{limit}\n"
54
+ end
55
+
39
56
  def exceeded_domains(work_items_list)
40
- work_items_list.filter { |work_item| work_item.total > work_item.wip_limit }
57
+ work_items_list.filter { |work_item| work_item.total > domain_limit(work_item.domain) }
58
+ end
59
+
60
+ def domain_limit(domain)
61
+ @limits[domain.to_sym] || DEFAULT_DOMAIN_LIMIT
41
62
  end
42
63
  end
43
64
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/email"
4
+ require_relative "../base"
5
+
6
+ module Mapper
7
+ module Imap
8
+ ##
9
+ # This class implementats the methods of the Mapper::Base module, specifically designed for
10
+ # preparing or shaping support emails data coming from a Fetcher::Base implementation.
11
+ class SupportEmails
12
+ include Base
13
+
14
+ # Implements the logic for shaping the results from a fetcher response.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>Fetcher::Imap::Types::Response</tt> imap_response: Array of imap emails data.
19
+ #
20
+ # <br>
21
+ # <b>return</b> <tt>List<Domain::Email></tt> support_emails_list, mapped support emails to be used by a
22
+ # Formatter::Base implementation.
23
+ #
24
+ def map(imap_response)
25
+ return [] if imap_response.results.empty?
26
+
27
+ normalized_email_data = normalize_response(imap_response.results)
28
+
29
+ normalized_email_data.map do |email|
30
+ Domain::Email.new(email["subject"], email["sender"], email["date"])
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_response(results)
37
+ return [] if results.nil?
38
+
39
+ results.map do |value|
40
+ {
41
+ "sender" => extract_sender(value),
42
+ "date" => value.date,
43
+ "subject" => value.subject
44
+ }
45
+ end
46
+ end
47
+
48
+ def extract_sender(value)
49
+ mailbox = value.sender[0]["mailbox"]
50
+ host = value.sender[0]["host"]
51
+
52
+ "#{mailbox}@#{host}"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,21 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # fetcher
3
4
  require_relative "../fetcher/notion/use_case/birthday_today"
4
5
  require_relative "../fetcher/notion/use_case/birthday_next_week"
5
6
  require_relative "../fetcher/notion/use_case/pto_today"
6
7
  require_relative "../fetcher/notion/use_case/pto_next_week"
7
8
  require_relative "../fetcher/notion/use_case/work_items_limit"
8
9
  require_relative "../fetcher/postgres/use_case/pto_today"
10
+ require_relative "../fetcher/imap/use_case/support_emails"
9
11
 
12
+ # mapper
10
13
  require_relative "../mapper/notion/birthday_today"
11
14
  require_relative "../mapper/notion/pto_today"
12
15
  require_relative "../mapper/notion/work_items_limit"
13
16
  require_relative "../mapper/postgres/pto_today"
17
+ require_relative "../mapper/imap/support_emails"
14
18
 
19
+ # formatter
15
20
  require_relative "../formatter/birthday"
16
21
  require_relative "../formatter/pto"
17
22
  require_relative "../formatter/work_items_limit"
23
+ require_relative "../formatter/support_emails"
18
24
 
25
+ # dispatcher
19
26
  require_relative "../dispatcher/discord/implementation"
20
27
  require_relative "../dispatcher/slack/implementation"
21
28
 
@@ -325,4 +332,46 @@ module UseCases
325
332
 
326
333
  UseCases::UseCase.new(use_case_config)
327
334
  end
335
+
336
+ # Provides an instance of the support emails from an google IMAP server to Discord use case implementation.
337
+ #
338
+ # <br>
339
+ # <b>Example</b>
340
+ #
341
+ # options = {
342
+ # fetch_options: {
343
+ # user: 'info@email.co',
344
+ # refresh_token: REFRESH_TOKEN,
345
+ # client_id: CLIENT_ID,
346
+ # client_secret: CLIENT_SECRET,
347
+ # inbox: 'INBOX',
348
+ # search_email: 'support@email.co'
349
+ # },
350
+ # dispatch_options: {
351
+ # webhook: "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
352
+ # name: "emailSupport"
353
+ # }
354
+ # }
355
+ #
356
+ # use_case = UseCases.notify_support_email_from_imap_to_discord(options)
357
+ # use_case.perform
358
+ #
359
+ # #################################################################################
360
+ #
361
+ # Requirements:
362
+ # * A google gmail account with IMAP support activated.
363
+ # * A set of authorization parameters like a client_id, client_secret, and a resfresh_token. To
364
+ # generate them, follow this instructions: https://developers.google.com/identity/protocols/oauth2
365
+ # * A webhook key, which can be generated directly on discrod on the desired channel, following this instructions:
366
+ # https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
367
+ #
368
+ def self.notify_support_email_from_imap_to_discord(options)
369
+ fetcher = Fetcher::Imap::SupportEmails.new(options[:fetch_options])
370
+ mapper = Mapper::Imap::SupportEmails.new
371
+ formatter = Formatter::SupportEmails.new(options[:format_options])
372
+ dispatcher = Dispatcher::Discord::Implementation.new(options[:dispatch_options])
373
+ use_case_config = UseCases::Types::Config.new(fetcher, mapper, formatter, dispatcher)
374
+
375
+ UseCases::UseCase.new(use_case_config)
376
+ end
328
377
  end
data/lib/bns/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bns
4
4
  # Gem version
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bns
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kommitters Open Source
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-29 00:00:00.000000000 Z
11
+ date: 2024-03-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A versatile business notification system offering key components for
14
14
  building various use cases. It provides an easy-to-use tool for implementing
@@ -39,10 +39,14 @@ files:
39
39
  - lib/bns/dispatcher/slack/implementation.rb
40
40
  - lib/bns/dispatcher/slack/types/response.rb
41
41
  - lib/bns/domain/birthday.rb
42
+ - lib/bns/domain/email.rb
42
43
  - lib/bns/domain/exceptions/function_not_implemented.rb
43
44
  - lib/bns/domain/pto.rb
44
45
  - lib/bns/domain/work_items_limit.rb
45
46
  - lib/bns/fetcher/base.rb
47
+ - lib/bns/fetcher/imap/base.rb
48
+ - lib/bns/fetcher/imap/types/response.rb
49
+ - lib/bns/fetcher/imap/use_case/support_emails.rb
46
50
  - lib/bns/fetcher/notion/base.rb
47
51
  - lib/bns/fetcher/notion/exceptions/invalid_api_key.rb
48
52
  - lib/bns/fetcher/notion/exceptions/invalid_database_id.rb
@@ -61,8 +65,10 @@ files:
61
65
  - lib/bns/formatter/birthday.rb
62
66
  - lib/bns/formatter/exceptions/invalid_data.rb
63
67
  - lib/bns/formatter/pto.rb
68
+ - lib/bns/formatter/support_emails.rb
64
69
  - lib/bns/formatter/work_items_limit.rb
65
70
  - lib/bns/mapper/base.rb
71
+ - lib/bns/mapper/imap/support_emails.rb
66
72
  - lib/bns/mapper/notion/birthday_today.rb
67
73
  - lib/bns/mapper/notion/pto_today.rb
68
74
  - lib/bns/mapper/notion/work_items_limit.rb