noticed 1.6.2 → 2.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +122 -147
  3. data/app/jobs/noticed/application_job.rb +9 -0
  4. data/app/jobs/noticed/event_job.rb +19 -0
  5. data/app/models/concerns/noticed/deliverable.rb +115 -0
  6. data/app/models/concerns/noticed/notification_methods.rb +17 -0
  7. data/app/models/concerns/noticed/readable.rb +62 -0
  8. data/app/models/noticed/application_record.rb +6 -0
  9. data/app/models/noticed/deliverable/deliver_by.rb +43 -0
  10. data/app/models/noticed/event.rb +15 -0
  11. data/app/models/noticed/notification.rb +16 -0
  12. data/db/migrate/20231215190233_create_noticed_tables.rb +25 -0
  13. data/lib/generators/noticed/delivery_method_generator.rb +1 -1
  14. data/lib/generators/noticed/install_generator.rb +19 -0
  15. data/lib/generators/noticed/{notification_generator.rb → notifier_generator.rb} +2 -2
  16. data/lib/generators/noticed/templates/README +5 -4
  17. data/lib/generators/noticed/templates/notifier.rb.tt +24 -0
  18. data/lib/noticed/api_client.rb +44 -0
  19. data/lib/noticed/bulk_delivery_method.rb +46 -0
  20. data/lib/noticed/bulk_delivery_methods/discord.rb +11 -0
  21. data/lib/noticed/bulk_delivery_methods/slack.rb +17 -0
  22. data/lib/noticed/bulk_delivery_methods/webhook.rb +18 -0
  23. data/lib/noticed/coder.rb +2 -0
  24. data/lib/noticed/delivery_method.rb +50 -0
  25. data/lib/noticed/delivery_methods/action_cable.rb +7 -39
  26. data/lib/noticed/delivery_methods/discord.rb +11 -0
  27. data/lib/noticed/delivery_methods/email.rb +9 -45
  28. data/lib/noticed/delivery_methods/fcm.rb +23 -64
  29. data/lib/noticed/delivery_methods/ios.rb +25 -112
  30. data/lib/noticed/delivery_methods/microsoft_teams.rb +5 -22
  31. data/lib/noticed/delivery_methods/slack.rb +6 -16
  32. data/lib/noticed/delivery_methods/test.rb +2 -12
  33. data/lib/noticed/delivery_methods/twilio_messaging.rb +37 -0
  34. data/lib/noticed/delivery_methods/vonage_sms.rb +20 -0
  35. data/lib/noticed/delivery_methods/webhook.rb +17 -0
  36. data/lib/noticed/engine.rb +1 -9
  37. data/lib/noticed/required_options.rb +21 -0
  38. data/lib/noticed/translation.rb +7 -3
  39. data/lib/noticed/version.rb +1 -1
  40. data/lib/noticed.rb +30 -15
  41. metadata +29 -40
  42. data/lib/generators/noticed/model/base_generator.rb +0 -48
  43. data/lib/generators/noticed/model/mysql_generator.rb +0 -18
  44. data/lib/generators/noticed/model/postgresql_generator.rb +0 -18
  45. data/lib/generators/noticed/model/sqlite3_generator.rb +0 -18
  46. data/lib/generators/noticed/model_generator.rb +0 -63
  47. data/lib/generators/noticed/templates/notification.rb.tt +0 -27
  48. data/lib/noticed/base.rb +0 -160
  49. data/lib/noticed/delivery_methods/base.rb +0 -93
  50. data/lib/noticed/delivery_methods/database.rb +0 -34
  51. data/lib/noticed/delivery_methods/twilio.rb +0 -51
  52. data/lib/noticed/delivery_methods/vonage.rb +0 -40
  53. data/lib/noticed/has_notifications.rb +0 -49
  54. data/lib/noticed/model.rb +0 -85
  55. data/lib/noticed/notification_channel.rb +0 -15
  56. data/lib/noticed/text_coder.rb +0 -16
  57. data/lib/rails_6_polyfills/actioncable/test_adapter.rb +0 -70
  58. data/lib/rails_6_polyfills/actioncable/test_helper.rb +0 -143
  59. data/lib/rails_6_polyfills/activejob/serializers.rb +0 -240
  60. data/lib/rails_6_polyfills/base.rb +0 -18
  61. data/lib/tasks/noticed_tasks.rake +0 -4
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators/named_base"
4
- require_relative "base_generator"
5
-
6
- module Noticed
7
- module Generators
8
- module Model
9
- class MysqlGenerator < BaseGenerator
10
- private
11
-
12
- def json_column_type
13
- "json"
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators/named_base"
4
- require_relative "base_generator"
5
-
6
- module Noticed
7
- module Generators
8
- module Model
9
- class PostgresqlGenerator < BaseGenerator
10
- private
11
-
12
- def json_column_type
13
- "jsonb"
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators/named_base"
4
- require_relative "base_generator"
5
-
6
- module Noticed
7
- module Generators
8
- module Model
9
- class Sqlite3Generator < BaseGenerator
10
- private
11
-
12
- def json_column_type
13
- "json"
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators/named_base"
4
-
5
- module Noticed
6
- module Generators
7
- class ModelGenerator < Rails::Generators::NamedBase
8
- include Rails::Generators::ResourceHelpers
9
-
10
- source_root File.expand_path("../templates", __FILE__)
11
-
12
- desc "Generates a Notification model for storing notifications."
13
-
14
- argument :name, type: :string, default: "Notification", banner: "Notification"
15
- argument :attributes, type: :array, default: [], banner: "field:type field:type"
16
-
17
- def generate_notification
18
- generate :model, name, "recipient:references{polymorphic}", "type", params_column, "read_at:datetime:index", *attributes
19
- end
20
-
21
- def add_noticed_model
22
- inject_into_class model_path, class_name, " include Noticed::Model\n"
23
- end
24
-
25
- def add_not_nullable
26
- migration_path = Dir.glob(Rails.root.join("db/migrate/*")).max_by { |f| File.mtime(f) }
27
-
28
- # Force is required because null: false already exists in the file and Thor isn't smart enough to tell the difference
29
- insert_into_file migration_path, after: "t.string :type", force: true do
30
- ", null: false"
31
- end
32
- end
33
-
34
- def done
35
- readme "README" if behavior == :invoke
36
- end
37
-
38
- private
39
-
40
- def model_path
41
- @model_path ||= File.join("app", "models", "#{file_path}.rb")
42
- end
43
-
44
- def params_column
45
- case current_adapter
46
- when "postgresql", "postgis"
47
- "params:jsonb"
48
- else
49
- # MySQL and SQLite both support json
50
- "params:json"
51
- end
52
- end
53
-
54
- def current_adapter
55
- if ActiveRecord::Base.respond_to?(:connection_db_config)
56
- ActiveRecord::Base.connection_db_config.adapter
57
- else
58
- ActiveRecord::Base.connection_config[:adapter]
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,27 +0,0 @@
1
- # To deliver this notification:
2
- #
3
- # <%= class_name %>.with(post: @post).deliver_later(current_user)
4
- # <%= class_name %>.with(post: @post).deliver(current_user)
5
-
6
- class <%= class_name %> < Noticed::Base
7
- # Add your delivery methods
8
- #
9
- # deliver_by :database
10
- # deliver_by :email, mailer: "UserMailer"
11
- # deliver_by :slack
12
- # deliver_by :custom, class: "MyDeliveryMethod"
13
-
14
- # Add required params
15
- #
16
- # param :post
17
-
18
- # Define helper methods to make rendering easier.
19
- #
20
- # def message
21
- # t(".message")
22
- # end
23
- #
24
- # def url
25
- # post_path(params[:post])
26
- # end
27
- end
data/lib/noticed/base.rb DELETED
@@ -1,160 +0,0 @@
1
- module Noticed
2
- class Base
3
- include Translation
4
- include Rails.application.routes.url_helpers
5
-
6
- extend ActiveModel::Callbacks
7
- define_model_callbacks :deliver
8
-
9
- class_attribute :delivery_methods, instance_writer: false, default: []
10
- class_attribute :param_names, instance_writer: false, default: []
11
-
12
- # Gives notifications access to the record and recipient during delivery
13
- attr_accessor :record, :recipient
14
-
15
- delegate :read?, :unread?, to: :record
16
-
17
- class << self
18
- def deliver_by(name, options = {})
19
- delivery_methods.push(name: name, options: options)
20
- define_model_callbacks(name)
21
- end
22
-
23
- # Copy delivery methods from parent
24
- def inherited(base) # :nodoc:
25
- base.delivery_methods = delivery_methods.dup
26
- base.param_names = param_names.dup
27
- super
28
- end
29
-
30
- def with(params)
31
- new(params)
32
- end
33
-
34
- # Shortcut for delivering without params
35
- def deliver(recipients)
36
- new.deliver(recipients)
37
- end
38
-
39
- # Shortcut for delivering later without params
40
- def deliver_later(recipients)
41
- new.deliver_later(recipients)
42
- end
43
-
44
- def params(*names)
45
- param_names.concat Array.wrap(names)
46
- end
47
- alias_method :param, :params
48
- end
49
-
50
- def initialize(params = {})
51
- @params = params
52
- end
53
-
54
- def deliver(recipients)
55
- validate!
56
-
57
- run_callbacks :deliver do
58
- Array.wrap(recipients).uniq.each do |recipient|
59
- run_delivery(recipient, enqueue: false)
60
- end
61
- end
62
- end
63
-
64
- def deliver_later(recipients)
65
- validate!
66
-
67
- run_callbacks :deliver do
68
- Array.wrap(recipients).uniq.each do |recipient|
69
- run_delivery(recipient, enqueue: true)
70
- end
71
- end
72
- end
73
-
74
- def params
75
- @params || {}
76
- end
77
-
78
- def clear_recipient
79
- self.recipient = nil
80
- end
81
-
82
- private
83
-
84
- # Runs all delivery methods for a notification
85
- def run_delivery(recipient, enqueue: true)
86
- delivery_methods = self.class.delivery_methods.dup
87
-
88
- self.recipient = recipient
89
-
90
- # Run database delivery inline first if it exists so other methods have access to the record
91
- if (index = delivery_methods.find_index { |m| m[:name] == :database })
92
- delivery_method = delivery_methods.delete_at(index)
93
- self.record = run_delivery_method(delivery_method, recipient: recipient, enqueue: false, record: nil)
94
- end
95
-
96
- delivery_methods.each do |delivery_method|
97
- run_delivery_method(delivery_method, recipient: recipient, enqueue: enqueue, record: record)
98
- end
99
- end
100
-
101
- # Actually runs an individual delivery
102
- def run_delivery_method(delivery_method, recipient:, enqueue:, record:)
103
- args = {
104
- notification_class: self.class.name,
105
- options: delivery_method[:options],
106
- params: params,
107
- recipient: recipient,
108
- record: record
109
- }
110
-
111
- run_callbacks delivery_method[:name] do
112
- method = delivery_method_for(delivery_method[:name], delivery_method[:options])
113
-
114
- # If the queue is `nil`, ActiveJob will use a default queue name.
115
- queue = delivery_method.dig(:options, :queue)
116
-
117
- # Always perfrom later if a delay is present
118
- if (delay = delivery_method.dig(:options, :delay))
119
- # Dynamic delays with metho calls or
120
- delay = send(delay) if delay.is_a? Symbol
121
-
122
- method.set(wait: delay, queue: queue).perform_later(args)
123
- elsif enqueue
124
- method.set(queue: queue).perform_later(args)
125
- else
126
- method.perform_now(args)
127
- end
128
- end
129
- end
130
-
131
- def delivery_method_for(name, options)
132
- if options[:class]
133
- options[:class].constantize
134
- else
135
- "Noticed::DeliveryMethods::#{name.to_s.camelize}".constantize
136
- end
137
- end
138
-
139
- def validate!
140
- validate_params_present!
141
- validate_options_of_delivery_methods!
142
- end
143
-
144
- # Validates that all params are present
145
- def validate_params_present!
146
- self.class.param_names.each do |param_name|
147
- if params[param_name].nil?
148
- raise ValidationError, "#{param_name} is missing."
149
- end
150
- end
151
- end
152
-
153
- def validate_options_of_delivery_methods!
154
- delivery_methods.each do |delivery_method|
155
- method = delivery_method_for(delivery_method[:name], delivery_method[:options])
156
- method.validate!(delivery_method[:options])
157
- end
158
- end
159
- end
160
- end
@@ -1,93 +0,0 @@
1
- module Noticed
2
- module DeliveryMethods
3
- class Base < Noticed.parent_class.constantize
4
- extend ActiveModel::Callbacks
5
- define_model_callbacks :deliver
6
-
7
- class_attribute :option_names, instance_writer: false, default: []
8
-
9
- attr_reader :notification, :options, :params, :recipient, :record
10
-
11
- class << self
12
- # Copy option names from parent
13
- def inherited(base) # :nodoc:
14
- base.option_names = option_names.dup
15
- super
16
- end
17
-
18
- def options(*names)
19
- option_names.concat Array.wrap(names)
20
- end
21
- alias_method :option, :options
22
-
23
- def validate!(delivery_method_options)
24
- option_names.each do |option_name|
25
- unless delivery_method_options.key? option_name
26
- raise ValidationError, "option `#{option_name}` must be set for #{name}"
27
- end
28
- end
29
- end
30
- end
31
-
32
- def assign_args(args)
33
- @notification = args.fetch(:notification_class).constantize.new(args[:params])
34
- @options = args[:options] || {}
35
- @params = args[:params]
36
- @recipient = args[:recipient]
37
- @record = args[:record]
38
-
39
- # Make notification aware of database record and recipient during delivery
40
- @notification.record = args[:record]
41
- @notification.recipient = args[:recipient]
42
- self
43
- end
44
-
45
- def perform(args)
46
- assign_args(args)
47
-
48
- return if (condition = @options[:if]) && !@notification.send(condition)
49
- return if (condition = @options[:unless]) && @notification.send(condition)
50
-
51
- run_callbacks :deliver do
52
- deliver
53
- end
54
- end
55
-
56
- def deliver
57
- raise NotImplementedError, "Delivery methods must implement a deliver method"
58
- end
59
-
60
- private
61
-
62
- # Helper method for making POST requests from delivery methods
63
- #
64
- # Usage:
65
- # post("http://example.com", basic_auth: {user:, pass:}, headers: {}, json: {}, form: {})
66
- #
67
- def post(url, args = {})
68
- options ||= {}
69
- basic_auth = args.delete(:basic_auth)
70
- headers = args.delete(:headers)
71
-
72
- request = HTTP
73
- request = request.basic_auth(user: basic_auth[:user], pass: basic_auth[:pass]) if basic_auth
74
- request = request.headers(headers) if headers
75
-
76
- response = request.post(url, args)
77
-
78
- if options[:debug]
79
- Rails.logger.debug("POST #{url}")
80
- Rails.logger.debug("Response: #{response.code}: #{response}")
81
- end
82
-
83
- if !options[:ignore_failure] && !response.status.success?
84
- puts response.status
85
- puts response.body
86
- raise ResponseUnsuccessful.new(response)
87
- end
88
-
89
- response
90
- end
91
- end
92
- end
93
- end
@@ -1,34 +0,0 @@
1
- module Noticed
2
- module DeliveryMethods
3
- class Database < Base
4
- # Must return the database record
5
- def deliver
6
- recipient.send(association_name).create!(attributes)
7
- end
8
-
9
- def self.validate!(options)
10
- super
11
-
12
- # Must be executed right away so the other deliveries can access the db record
13
- raise ArgumentError, "database delivery cannot be delayed" if options.key?(:delay)
14
- end
15
-
16
- private
17
-
18
- def association_name
19
- options[:association] || :notifications
20
- end
21
-
22
- def attributes
23
- if (method = options[:format])
24
- notification.send(method)
25
- else
26
- {
27
- type: notification.class.name,
28
- params: notification.params
29
- }
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,51 +0,0 @@
1
- module Noticed
2
- module DeliveryMethods
3
- class Twilio < Base
4
- def deliver
5
- post(url, basic_auth: {user: account_sid, pass: auth_token}, form: format)
6
- end
7
-
8
- private
9
-
10
- def format
11
- if (method = options[:format])
12
- notification.send(method)
13
- else
14
- {
15
- From: phone_number,
16
- To: recipient.phone_number,
17
- Body: notification.params[:message]
18
- }
19
- end
20
- end
21
-
22
- def url
23
- if (method = options[:url])
24
- notification.send(method)
25
- else
26
- "https://api.twilio.com/2010-04-01/Accounts/#{account_sid}/Messages.json"
27
- end
28
- end
29
-
30
- def account_sid
31
- credentials.fetch(:account_sid)
32
- end
33
-
34
- def auth_token
35
- credentials.fetch(:auth_token)
36
- end
37
-
38
- def phone_number
39
- credentials.fetch(:phone_number)
40
- end
41
-
42
- def credentials
43
- if (method = options[:credentials])
44
- notification.send(method)
45
- else
46
- Rails.application.credentials.twilio
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,40 +0,0 @@
1
- module Noticed
2
- module DeliveryMethods
3
- class Vonage < Base
4
- def deliver
5
- response = post("https://rest.nexmo.com/sms/json", json: format)
6
- status = response.parse.dig("messages", 0, "status")
7
- if !options[:ignore_failure] && status != "0"
8
- raise ResponseUnsuccessful.new(response)
9
- end
10
-
11
- response
12
- end
13
-
14
- private
15
-
16
- def format
17
- if (method = options[:format])
18
- notification.send(method)
19
- else
20
- {
21
- api_key: credentials[:api_key],
22
- api_secret: credentials[:api_secret],
23
- from: notification.params[:from],
24
- text: notification.params[:body],
25
- to: notification.params[:to],
26
- type: "unicode"
27
- }
28
- end
29
- end
30
-
31
- def credentials
32
- if (method = options[:credentials])
33
- notification.send(method)
34
- else
35
- Rails.application.credentials.vonage
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,49 +0,0 @@
1
- module Noticed
2
- module HasNotifications
3
- # Defines a method for the association and a before_destroy callback to remove notifications
4
- # where this record is a param
5
- #
6
- # class User < ApplicationRecord
7
- # has_noticed_notifications
8
- # has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
9
- # end
10
- #
11
- # @user.notifications_as_user
12
- # @user.notifications_as_owner
13
-
14
- extend ActiveSupport::Concern
15
-
16
- class_methods do
17
- def has_noticed_notifications(param_name: model_name.singular, **options)
18
- define_method "notifications_as_#{param_name}" do
19
- model = options.fetch(:model_name, "Notification").constantize
20
- case current_adapter
21
- when "postgresql", "postgis"
22
- model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
23
- when "mysql2"
24
- model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
25
- when "sqlite3"
26
- model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
27
- else
28
- # This will perform an exact match which isn't ideal
29
- model.where(params: {param_name.to_sym => self})
30
- end
31
- end
32
-
33
- if options.fetch(:destroy, true)
34
- before_destroy do
35
- send("notifications_as_#{param_name}").destroy_all
36
- end
37
- end
38
- end
39
- end
40
-
41
- def current_adapter
42
- if ActiveRecord::Base.respond_to?(:connection_db_config)
43
- ActiveRecord::Base.connection_db_config.adapter
44
- else
45
- ActiveRecord::Base.connection_config[:adapter]
46
- end
47
- end
48
- end
49
- end
data/lib/noticed/model.rb DELETED
@@ -1,85 +0,0 @@
1
- module Noticed
2
- module Model
3
- DATABASE_ERROR_CLASS_NAMES = lambda {
4
- classes = [ActiveRecord::NoDatabaseError]
5
- classes << ActiveRecord::ConnectionNotEstablished
6
- classes << Mysql2::Error if defined?(::Mysql2)
7
- classes << PG::ConnectionBad if defined?(::PG)
8
- classes
9
- }.call.freeze
10
-
11
- extend ActiveSupport::Concern
12
-
13
- included do
14
- self.inheritance_column = nil
15
-
16
- if Rails.gem_version >= Gem::Version.new("7.1.0.alpha")
17
- serialize :params, coder: noticed_coder
18
- else
19
- serialize :params, noticed_coder
20
- end
21
-
22
- belongs_to :recipient, polymorphic: true
23
-
24
- scope :newest_first, -> { order(created_at: :desc) }
25
- scope :unread, -> { where(read_at: nil) }
26
- scope :read, -> { where.not(read_at: nil) }
27
- end
28
-
29
- class_methods do
30
- def mark_as_read!
31
- update_all(read_at: Time.current, updated_at: Time.current)
32
- end
33
-
34
- def mark_as_unread!
35
- update_all(read_at: nil, updated_at: Time.current)
36
- end
37
-
38
- def noticed_coder
39
- return Noticed::TextCoder unless table_exists?
40
-
41
- case attribute_types["params"].type
42
- when :json, :jsonb
43
- Noticed::Coder
44
- else
45
- Noticed::TextCoder
46
- end
47
- rescue *DATABASE_ERROR_CLASS_NAMES => _error
48
- warn("Noticed was unable to bootstrap correctly as the database is unavailable.")
49
-
50
- Noticed::TextCoder
51
- end
52
- end
53
-
54
- # Rehydrate the database notification into the Notification object for rendering
55
- def to_notification
56
- @_notification ||= begin
57
- instance = type.constantize.with(params)
58
- instance.record = self
59
- instance.recipient = recipient
60
- instance
61
- end
62
- end
63
-
64
- def mark_as_read!
65
- update(read_at: Time.current)
66
- end
67
-
68
- def mark_as_unread!
69
- update(read_at: nil)
70
- end
71
-
72
- def unread?
73
- !read?
74
- end
75
-
76
- def read?
77
- read_at?
78
- end
79
-
80
- # If a GlobalID record in params is no longer found, the params will default with a noticed_error key
81
- def deserialize_error?
82
- !!params[:noticed_error]
83
- end
84
- end
85
- end
@@ -1,15 +0,0 @@
1
- module Noticed
2
- class NotificationChannel < ApplicationCable::Channel
3
- def subscribed
4
- stream_for current_user
5
- end
6
-
7
- def unsubscribed
8
- stop_all_streams
9
- end
10
-
11
- def mark_as_read(data)
12
- current_user.notifications.where(id: data["ids"]).mark_as_read!
13
- end
14
- end
15
- end
@@ -1,16 +0,0 @@
1
- module Noticed
2
- class TextCoder
3
- def self.load(data)
4
- return if data.nil?
5
-
6
- # Text columns need JSON parsing
7
- data = JSON.parse(data)
8
- ActiveJob::Arguments.send(:deserialize_argument, data)
9
- end
10
-
11
- def self.dump(data)
12
- return if data.nil?
13
- ActiveJob::Arguments.send(:serialize_argument, data).to_json
14
- end
15
- end
16
- end