bns 0.2.0 → 0.3.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 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