-
-
Notifications
You must be signed in to change notification settings - Fork 338
/
notification.rb
239 lines (202 loc) · 7.61 KB
/
notification.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# frozen_string_literal: true
class Notification < ApplicationRecord
include Octobox::Notifications::InclusiveScope
include Octobox::Notifications::ExclusiveScope
include Octobox::Notifications::SyncSubject
SUBJECTABLE_TYPES = SUBJECT_TYPE_COMMIT_RELEASE + SUBJECT_TYPE_ISSUE_REQUEST
include PgSearch::Model
pg_search_scope :search_by_subject_title,
against: :subject_title,
order_within_rank: 'notifications.updated_at DESC',
using: {
tsearch: {
prefix: true,
negation: true,
dictionary: "english"
}
}
belongs_to :user
belongs_to :subject, foreign_key: :subject_url, primary_key: :url, optional: true
belongs_to :repository, foreign_key: :repository_full_name, primary_key: :full_name, optional: true
has_many :labels, through: :subject
has_one :app_installation, through: :repository
validates :subject_url, presence: true
validates :archived, inclusion: [true, false]
after_update :push_if_changed
after_destroy :clean_up_subject
class << self
def attributes_from_api_response(api_response)
attrs = DownloadService::API_ATTRIBUTE_MAP.map do |attr, path|
value = api_response.to_h.dig(*path)
value = value.dup if value.frozen?
value.delete!("\u0000") if value.is_a?(String)
[attr, value]
end.to_h
if "RepositoryInvitation" == api_response.subject.type
attrs[:subject_url] = "#{api_response.repository.html_url}/invitations"
end
attrs[:updated_at] = Time.current if api_response.updated_at.nil?
attrs
end
end
def title
return unless display_subject?
if (body = subject.try(:body))
body.split("\n")[0..3].join
end
end
def state
return unless display_subject?
@state ||= subject.try(:state)
end
def draft?
return unless display_subject?
@draft ||= subject.try(:draft?)
end
def private?
repository.try(:private?)
end
def display?
return true unless private?
return true unless Octobox.io?
repository.try(:display_subject?) || user.has_personal_plan?
end
def self.archive(notifications, value)
value = value ? ActiveRecord::Type::Boolean.new.cast(value) : true
notifications.update_all(archived: value)
user = notifications.first.try(:user)
ArchiveWorker.perform_async_if_configured(user.id, notifications.map(&:github_id)) if user && value
end
def self.archive_on_github(user, notification_ids)
conn = user.github_client.client_without_redirects
manager = Typhoeus::Hydra.new(max_concurrency: Octobox.config.max_concurrency)
begin
conn.in_parallel(manager) do
notification_ids.each do |id|
conn.delete "notifications/threads/#{id}"
end
end
rescue Octokit::Forbidden, Octokit::NotFound, Octokit::Unauthorized
# one or more notifications are for repos the user no longer has access to
end
end
def self.mark_read(notifications)
unread = notifications.select(&:unread)
return if unread.empty?
user = unread.first.user
MarkReadWorker.perform_async_if_configured(user.id, unread.map(&:github_id))
where(id: unread.map(&:id)).update_all(unread: false)
end
def self.mark_read_on_github(user, notification_ids)
conn = user.github_client.client_without_redirects
manager = Typhoeus::Hydra.new(max_concurrency: Octobox.config.max_concurrency)
begin
conn.in_parallel(manager) do
notification_ids.each do |id|
conn.patch "notifications/threads/#{id}"
end
end
rescue Octokit::Forbidden, Octokit::NotFound, Octokit::Unauthorized
# one or more notifications are for repos the user no longer has access to
end
end
def self.mute(notifications)
return if notifications.empty?
user = notifications.to_a.first.user
MuteNotificationsWorker.perform_async_if_configured(user.id, notifications.map(&:github_id))
where(id: notifications.map(&:id)).update_all(archived: true, unread: false, muted_at: Time.current)
end
def self.mute_on_github(user, notification_ids)
conn = user.github_client.client_without_redirects
manager = Typhoeus::Hydra.new(max_concurrency: Octobox.config.max_concurrency)
begin
conn.in_parallel(manager) do
notification_ids.each do |id|
conn.patch "notifications/threads/#{id}"
conn.put "notifications/threads/#{id}/subscription", {ignored: true}.to_json
end
end
rescue Octokit::Forbidden, Octokit::NotFound, Octokit::Unauthorized
# one or more notifications are for repos the user no longer has access to
end
end
def expanded_subject_url
return subject_url unless display_subject?
subject.try(:html_url) || subject_url # Use the sync'd HTML URL if possible, else the API one
end
def web_url
Octobox::SubjectUrlParser.new(expanded_subject_url, latest_comment_url: latest_comment_url)
.to_html_url
end
def repo_url
"#{Octobox.config.github_domain}/#{repository_full_name}"
end
def unarchive_if_updated
return unless self.archived?
change = changes['updated_at']
return unless change
if self.archived && change[1] > change[0]
self.archived = false
end
end
def update_from_api_response(api_response)
attrs = Notification.attributes_from_api_response(api_response)
self.attributes = attrs
self.archived = false if archived.nil? # fixup existing records where archived is nil
unarchive_if_updated
if changed?
save(touch: false)
update_repository(api_response)
update_subject
end
end
def github_app_installed?
Octobox.github_app? && user.try(:github_app_authorized?) && repository.try(:github_app_installed?)
end
def subjectable?
SUBJECTABLE_TYPES.include?(subject_type)
end
def display_subject?
@display_subject ||= subjectable? && (Octobox.fetch_subject? || repository.try(:display_subject?) || user.has_personal_plan?)
end
def upgrade_required?
return nil unless repository.present?
repository.private? && !(repository.required_plan_available? || user.has_personal_plan?)
end
def prerender?
unread? and !['closed', 'merged'].include?(state) and !display_thread?
end
def subject_number
subject_url.scan(/\d+$/).first
end
def display_thread?
@display_thread ||= Octobox.include_comments? && subjectable? && subject.present? && user.try(:display_comments?)
end
def push_if_changed
push_to_channel if (saved_changes.keys & pushable_fields).any?
end
def pushable_fields
['archived', 'reason', 'subject_title', 'subject_url', 'subject_type', 'unread']
end
def push_to_channel
notification = ApplicationController.render(partial: 'notifications/notification', locals: { notification: self})
subject = ApplicationController.render(partial: 'notifications/thread_subject', locals: { notification: self})
ActionCable.server.broadcast "notifications:#{user_id}", { id: self.id, notification: notification, subject: subject }
end
def update_repository(api_response)
repo = repository || Repository.find_or_create_by(github_id: api_response['repository']['id'])
repo.assign_attributes({
full_name: api_response['repository']['full_name'],
private: api_response['repository']['private'],
owner: api_response['repository']['full_name'].split('/').first,
github_id: api_response['repository']['id']
})
if repo.changed?
repo.last_synced_at = Time.current
repo.save
end
end
def clean_up_subject
subject.destroy if subject && subject.notifications.empty?
end
end