Class: Jekyll::ActivityPub::Notifier

Inherits:
Object
  • Object
show all
Defined in:
lib/jekyll/activity_pub/notifier.rb

Overview

Long term store for notifications.

Needs to be a singleton so we can use the same data across all of Jekyll’s build process.

Defined Under Namespace

Classes: PseudoObject

Class Method Summary collapse

Class Method Details

.actorObject



106
107
108
109
110
# File 'lib/jekyll/activity_pub/notifier.rb', line 106

def actor
  data['actor'].tap do |a|
    abort_if_missing('actor', a)
  end
end

.actor=(actor) ⇒ Object



102
103
104
# File 'lib/jekyll/activity_pub/notifier.rb', line 102

def actor=(actor)
  data['actor'] = actor
end

.actor_urlObject



116
117
118
119
120
# File 'lib/jekyll/activity_pub/notifier.rb', line 116

def actor_url
  data['actor_url'].tap do |au|
    abort_if_missing('actor_url', au)
  end
end

.actor_url=(url) ⇒ Object



112
113
114
# File 'lib/jekyll/activity_pub/notifier.rb', line 112

def actor_url=(url)
  data['actor_url'] = url
end

.announce?Boolean

Announce the website?

Returns:

  • (Boolean)


67
68
69
# File 'lib/jekyll/activity_pub/notifier.rb', line 67

def announce?
  !!config['announce']
end

.clientObject



288
289
290
291
292
293
294
295
# File 'lib/jekyll/activity_pub/notifier.rb', line 288

def client
  @@client ||= DistributedPress::V1::Social::Client.new(
    private_key_pem: private_key,
    url: url,
    public_key_url: public_key_url,
    logger: Jekyll.logger
  )
end

.create(path, **opts) ⇒ nil

Creates an activity unless it has been already created

Parameters:

  • :path (String)

Returns:

  • (nil)


205
206
207
208
209
# File 'lib/jekyll/activity_pub/notifier.rb', line 205

def create(path, **opts)
  action(path, 'create', **opts) unless created?(path)

  nil
end

.created?(path) ⇒ Boolean

Already created

Parameters:

  • :path (String)

Returns:

  • (Boolean)


231
232
233
# File 'lib/jekyll/activity_pub/notifier.rb', line 231

def created?(path)
  !status(path)['created_at'].nil?
end

.dataHash

Return data

Returns:

  • (Hash)


173
174
175
# File 'lib/jekyll/activity_pub/notifier.rb', line 173

def data
  @@data ||= site.data['activity_pub']
end

.delete(path) ⇒ nil

Removes an activity if it was previously created

Parameters:

  • :path (String)

Returns:

  • (nil)


181
182
183
# File 'lib/jekyll/activity_pub/notifier.rb', line 181

def delete(path)
  action(path, 'delete') if exist?(path) && !deleted?(path)
end

.deleted?(path) ⇒ Boolean

Parameters:

  • :path (String)

Returns:

  • (Boolean)


241
242
243
# File 'lib/jekyll/activity_pub/notifier.rb', line 241

def deleted?(path)
  !status(path)['deleted_at'].nil?
end

.dereferencerObject



62
63
64
# File 'lib/jekyll/activity_pub/notifier.rb', line 62

def dereferencer
  @@dereferencer ||= DistributedPress::V1::Social::Dereferencer.new(client: client)
end

.done?(path) ⇒ Boolean

Detects if an action was already done

Parameters:

  • :path (String)

Returns:

  • (Boolean)


222
223
224
225
226
# File 'lib/jekyll/activity_pub/notifier.rb', line 222

def done?(path)
  (status(path)['action'] == 'done').tap do |done|
    Jekyll.logger.debug('ActivityPub:', "Skipping notification for #{path}") if done
  end
end

.exist?(path) ⇒ Boolean

Returns:

  • (Boolean)


245
246
247
# File 'lib/jekyll/activity_pub/notifier.rb', line 245

def exist?(path)
  !status(path)['action'].nil?
end

.followers_urlString

Returns:

  • (String)


82
83
84
# File 'lib/jekyll/activity_pub/notifier.rb', line 82

def followers_url
  "#{url}/v1/#{actor}/followers"
end

.inboxObject



297
298
299
# File 'lib/jekyll/activity_pub/notifier.rb', line 297

def inbox
  @@inbox ||= DistributedPress::V1::Social::Inbox.new(client: client, actor: actor)
end

.likes(activity) ⇒ DistributedPress::V1::Social::Likes

Generates a Likes client per activity

Parameters:

  • activity (String)

    Activity ID

Returns:

  • (DistributedPress::V1::Social::Likes)


318
319
320
321
# File 'lib/jekyll/activity_pub/notifier.rb', line 318

def likes(activity)
  @@likes ||= {}
  @@likes[activity] ||= DistributedPress::V1::Social::Likes.new(client: client, actor: actor, activity: activity)
end

.manually_approves_followers?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'lib/jekyll/activity_pub/notifier.rb', line 71

def manually_approves_followers?
  !!config['manually_approves_followers']
end

.notify!Object

Send notifications

  1. Wait for public key propagation

  2. Create/update inbox

  3. Send create, update, and delete



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
# File 'lib/jekyll/activity_pub/notifier.rb', line 127

def notify!
  # TODO: request several times with a timeout
  response = HTTParty.get(public_key_url)

  unless response.ok?
    raise NotificationError,
          "Could't fetch public key (#{response.code}: #{response.message}). It needs to be available at #{public_key_url} before notifications can work, because it's used for signature verification."
  end

  unless client.private_key.compare? OpenSSL::PKey::RSA.new(response.parsed_response.dig('publicKey', 'publicKeyPem'))
    raise NotificationError, "Public key at #{public_key_url} differs from local version"
  end

  actor_object = object_for(site.in_dest_dir(URI.parse(actor_url).path))

  # Create/Update inbox
  unless (response = inbox.create(actor_url, announce: announce?, manually_approves_followers: manually_approves_followers?)).ok?
    raise NotificationError, "Couldn't create/update inbox (#{response.code}: #{response.message})"
  end

  # Remove notifications already performed and notify
  data['notifications'].reject do |object_url, _|
    done? object_url
  end.each do |object_url, status|
    process_object(actor_object, object_for(object_url), status)
  end

  # Update actor profile
  if actor_object.updated_at > actor_object.date
    Jekyll.logger.debug 'ActivityPub:', 'Updating Actor profile'
    actor_update = Jekyll::ActivityPub::Update.new(site, actor_object, actor_object)

    unless (response = outbox.post(activity: actor_update)).ok?
      raise NotificationError, "Couldn't update actor (#{response.code}: #{response.message})"
    end
  end

  # Store everything for later
  save
rescue NotificationError => e
  Jekyll.logger.abort_with 'ActivityPub:', e.message
end

.outboxObject



301
302
303
# File 'lib/jekyll/activity_pub/notifier.rb', line 301

def outbox
  @@outbox ||= DistributedPress::V1::Social::Outbox.new(client: client, actor: actor)
end

.pathString

Returns the path for the storage

Returns:

  • (String)


277
278
279
# File 'lib/jekyll/activity_pub/notifier.rb', line 277

def path
  @@path ||= site.in_source_dir(site.config['data_dir'], 'activity_pub.yml')
end

.public_key_urlString

Public key URL, raises error if missing

Returns:

  • (String)


96
97
98
99
100
# File 'lib/jekyll/activity_pub/notifier.rb', line 96

def public_key_url
  data['public_key_url'].tap do |pk|
    abort_if_missing('public_key_url', pk)
  end
end

.public_key_url=(url) ⇒ Object

Save the public key URL for later

Parameters:

  • :url (String)


89
90
91
# File 'lib/jekyll/activity_pub/notifier.rb', line 89

def public_key_url=(url)
  data['public_key_url'] = url
end

.relative_pathString

Storage path relative to site source

Returns:

  • (String)


284
285
286
# File 'lib/jekyll/activity_pub/notifier.rb', line 284

def relative_path
  @@relative_path ||= Pathname.new(path).relative_path_from(site.source).to_s
end

.replies(activity) ⇒ DistributedPress::V1::Social::Replies

Generates a Replies client per activity

Parameters:

  • activity (String)

    Activity ID

Returns:

  • (DistributedPress::V1::Social::Replies)


309
310
311
312
# File 'lib/jekyll/activity_pub/notifier.rb', line 309

def replies(activity)
  @@replies ||= {}
  @@replies[activity] ||= DistributedPress::V1::Social::Replies.new(client: client, actor: actor, activity: activity)
end

.savenil

Stores data back to a file and optionally commits it

Returns:

  • (nil)


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/jekyll/activity_pub/notifier.rb', line 252

def save
  # TODO: Send warning if CI is detected
  Jekyll.logger.info 'ActivityPub:', "Saving data to #{relative_path}"

  FileUtils.mkdir_p(File.dirname(path))

  File.open(path, 'w') do |f|
    f.flock(File::LOCK_EX)
    f.rewind
    f.write(YAML.dump(data.reject { |_, v| v.empty? }))
    f.flush
    f.truncate(f.pos)
  end

  if ENV['JEKYLL_ENV'] == 'production' && site.respond_to?(:repository)
    site.staged_files << relative_path
    site.repository.commit 'ActivityPub'
  end

  nil
end

.shares(activity) ⇒ DistributedPress::V1::Social::Shares

Generates a Shares client per activity

Parameters:

  • activity (String)

    Activity ID

Returns:

  • (DistributedPress::V1::Social::Shares)


327
328
329
330
# File 'lib/jekyll/activity_pub/notifier.rb', line 327

def shares(activity)
  @@shares ||= {}
  @@shares[activity] ||= DistributedPress::V1::Social::Shares.new(client: client, actor: actor, activity: activity)
end

.siteJekyll::Site

Returns:

  • (Jekyll::Site)


58
59
60
# File 'lib/jekyll/activity_pub/notifier.rb', line 58

def site
  @@site
end

.site=(site) ⇒ Jekyll::Site

Set the site and initialize data

Parameters:

  • :site (Jekyll::Site)

Returns:

  • (Jekyll::Site)


50
51
52
53
54
55
# File 'lib/jekyll/activity_pub/notifier.rb', line 50

def site=(site)
  @@site = site.tap do |s|
    s.data['activity_pub'] ||= {}
    s.data['activity_pub']['notifications'] ||= {}
  end
end

.status(path) ⇒ Hash

Gets status

Parameters:

  • :path (String)

Returns:

  • (Hash)


215
216
217
# File 'lib/jekyll/activity_pub/notifier.rb', line 215

def status(path)
  data['notifications'][path_relative_to_dest(path)] || {}
end

.update(path, **opts) ⇒ nil

Updates an activity if it was previously created, otherwise create it, or update it again if it was modified later

Parameters:

  • :path (String)

Returns:

  • (nil)


190
191
192
193
194
195
196
197
198
199
# File 'lib/jekyll/activity_pub/notifier.rb', line 190

def update(path, **opts)
  # Compare Unix timestamps
  if created?(path) && (object_for(path)&.updated_at&.to_i || 0) > (status(path)['updated_at'] || 0)
    action(path, 'update', **opts)
  else
    create(path, **opts)
  end

  nil
end

.updated?(path) ⇒ Boolean

Parameters:

  • :path (String)

Returns:

  • (Boolean)


236
237
238
# File 'lib/jekyll/activity_pub/notifier.rb', line 236

def updated?(path)
  !status(path)['updated_at'].nil?
end

.urlObject



75
76
77
78
79
# File 'lib/jekyll/activity_pub/notifier.rb', line 75

def url
  config['url'].tap do |u|
    abort_if_missing('activity_pub.url', u, '_config.yml')
  end
end