jekyll-activity-pub 0.1.0rc12 → 0.1.0rc14

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: 52d19e3cacf79bb7a5c041ae37b4caae232947f50fc94eb7b269edebe37a15b7
4
- data.tar.gz: 976bf99864bc71f49bec91090de41b4ddb829cfc296ba0addaa7dc43400daddd
3
+ metadata.gz: aaea38dc037b09e85e37555dfcf89e0f974107a0699a600d9c2f8f5f031f6a72
4
+ data.tar.gz: fef291e74969a128d2ed950ea9a0f2fda5fc93b5fc45fa851ea8b6f07d15aed9
5
5
  SHA512:
6
- metadata.gz: abfa3e386181380e62a2223b64c3201f5b3751921f669c4129e637959c0600c8fa529f8ce929540a51c975154f7a5c007e4d39522561e1c05ce82ff1eda7bce5
7
- data.tar.gz: 98bb2c9fcec4955dc7b79582421fca47d6d50008bce879a821e5d3d2dc4c800901566230dc402ec1ea03f173c1ce1457d2c3c6751a5f8b9991b16c329edd7604
6
+ metadata.gz: e27745239477eef2a66b1d0dee32af27b4e58219d381e5720abf0d158a38ea31a3e113a9b812b40b4faadbfed48925e0e242c7012d9c4cd428fd725e77f18f07
7
+ data.tar.gz: ee3dff54f5c6f549286bc3009063d99513a2d2fc73ffaec6d188f9988866d1c15135dc8662ca0cc1a4b9d9dfc4891546a9391bec7e579ef88481e4f6e6f64176
data/README.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # Jekyll ActivityPub
2
2
 
3
- ## Immediate goals
3
+ Generates an [ActivityPub](https://www.w3.org/TR/activitypub/)
4
+ representation of your site and delegates notifications and followers to
5
+ the [Distributed Press](https://distributed.press) Social Inbox.
4
6
 
5
- * Create an outbox from a Jekyll website
6
- * Send notifications to [Distributed Press](https://distributed.press/)
7
+ ## Installation and Usage
8
+
9
+ See _docs/
10
+
11
+ ## Development
12
+
13
+ After checking out the repo, run `bundle` to install dependencies.
14
+
15
+ To release a new version, update the version number in
16
+ `jekyll-activity-pub.gemspec`, and then run `go-task release`, which
17
+ will push the `.gem` file to [rubygems.org](https://rubygems.org).
18
+
19
+ ## Contributing
20
+
21
+ Bug reports and pull requests are welcome on 0xacab.org at
22
+ <https://0xacab.org/sutty/jekyll/jekyll-activity-pub>. This
23
+ project is intended to be a safe, welcoming space for collaboration, and
24
+ contributors are expected to adhere to the [Sutty code of
25
+ conduct](https://sutty.nl/en/code-of-conduct/).
26
+
27
+ ## License
28
+
29
+ The gem is available as free software under the terms of the Apache2
30
+ License.
31
+
32
+ ## Code of Conduct
33
+
34
+ Everyone interacting in the jekyll-activity-pub project’s
35
+ codebases, issue trackers, chat rooms and mailing lists is expected to
36
+ follow the [code of conduct](https://sutty.nl/en/code-of-conduct/).
@@ -54,10 +54,11 @@ module Jekyll
54
54
  'to' => [
55
55
  'https://www.w3.org/ns/activitystreams#Public'
56
56
  ],
57
- 'cc' => [ Notifier.followers_url ],
57
+ 'cc' => [Notifier.followers_url],
58
58
  'inReplyTo' => doc.data['in_reply_to'],
59
59
  'sensitive' => sensitive?,
60
60
  'content' => doc.content,
61
+ 'name' => doc.data['title'],
61
62
  'contentMap' => {
62
63
  locale => doc.content
63
64
  },
@@ -75,7 +76,7 @@ module Jekyll
75
76
  #
76
77
  # @return [String,nil]
77
78
  def summary
78
- @summary ||= find_best_value_for(doc.data, 'summary', 'description', 'title')
79
+ @summary ||= doc.data.slice('title', 'summary').values.join(separator)
79
80
  end
80
81
 
81
82
  # Should it have a content warning?
@@ -83,6 +84,13 @@ module Jekyll
83
84
  !!doc.data.fetch('sensitive', false)
84
85
  end
85
86
 
87
+ # Separator to join title and summary by
88
+ #
89
+ # @return [String]
90
+ def separator
91
+ @separator ||= site.config.dig('activity_pub', 'separator') || ' // '
92
+ end
93
+
86
94
  # Find attachments
87
95
  #
88
96
  # @return [Array]
@@ -46,6 +46,7 @@ module Jekyll
46
46
  ],
47
47
  'type' => 'Person',
48
48
  'id' => absolute_url(url),
49
+ 'url' => site.config['url'],
49
50
  'outbox' => nil,
50
51
  'inbox' => inbox,
51
52
  'following' => nil,
@@ -53,16 +54,29 @@ module Jekyll
53
54
  'preferredUsername' => username,
54
55
  'name' => public_name,
55
56
  'summary' => summary,
56
- 'icon' => icons,
57
+ 'icon' => icons.first,
58
+ 'image' => images.first,
57
59
  'publicKey' => nil,
60
+ 'published' => date.xmlschema,
61
+ 'updated' => updated.xmlschema,
58
62
  'attachment' => [
59
- PropertyValue.new(website_name, absolute_url(site.config['url']))
63
+ PropertyValue.new(website_name, website_link)
60
64
  ]
61
65
  }
62
66
  end
63
67
 
68
+ # @return [Time]
69
+ def date
70
+ @date ||= site.config.dig('activity_pub', 'published') || site.time
71
+ end
72
+
64
73
  private
65
74
 
75
+ # @return [Time]
76
+ def updated
77
+ @updated ||= site.config.dig('activity_pub', 'updated') || site.time
78
+ end
79
+
66
80
  # Finds public name
67
81
  #
68
82
  # @return [String,nil]
@@ -110,6 +124,24 @@ module Jekyll
110
124
  inbox << '/inbox'
111
125
  end
112
126
  end
127
+
128
+ # Find images
129
+ #
130
+ # @return [Array]
131
+ def images
132
+ @images ||= [find_best_value_for(site.config, %w[activity_pub images],
133
+ %w[image path])].flatten.compact.map do |icon|
134
+ Image.new(site, icon, summary)
135
+ end
136
+ end
137
+
138
+ # @return [String]
139
+ def website_link
140
+ @website_link ||=
141
+ <<~LINK
142
+ <a rel="me" href="#{site.config['url']}">#{site.config['url']}</a>
143
+ LINK
144
+ end
113
145
  end
114
146
  end
115
147
  end
@@ -23,7 +23,7 @@ module Jekyll
23
23
  'id' => absolute_url(object.url).sub('.jsonld', '/delete.jsonld'),
24
24
  'type' => 'Delete',
25
25
  'actor' => absolute_url(actor.url),
26
- 'object' => absolute_url(object.url)
26
+ 'object' => object.data['id']
27
27
  }
28
28
 
29
29
  trigger_hooks :post_init
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
3
4
  require 'jekyll/hooks'
4
5
 
5
6
  module Jekyll
@@ -7,6 +8,10 @@ module Jekyll
7
8
  # Container for common tools
8
9
  module Helper
9
10
  include Jekyll::Filters::URLFilters
11
+ extend Forwardable
12
+
13
+ # So we can deep dig between linked objects
14
+ def_delegators :data, :dig
10
15
 
11
16
  # Some filters needs a Liquid-like context
12
17
  StubContext = Struct.new(:registers, keyword_init: true)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'marcel'
4
+ require 'pathname'
3
5
  require_relative 'helper'
4
6
 
5
7
  module Jekyll
@@ -20,7 +22,7 @@ module Jekyll
20
22
 
21
23
  @data = {
22
24
  'type' => 'Image',
23
- 'mediaType' => "image/#{File.extname(path).sub('.', '')}",
25
+ 'mediaType' => Marcel::MimeType.for(Pathname.new(site.in_source_dir(path))),
24
26
  'url' => absolute_url(path),
25
27
  'name' => description.to_s
26
28
  }
@@ -22,6 +22,12 @@ module Jekyll
22
22
  site.in_dest_dir(relative_path)
23
23
  end
24
24
 
25
+ def date
26
+ @date ||= Time.parse(data['published'])
27
+ rescue StandardError
28
+ nil
29
+ end
30
+
25
31
  # @return [Time, nil]
26
32
  def updated_at
27
33
  @updated_at ||= Time.parse(data['updated'])
@@ -47,14 +53,20 @@ module Jekyll
47
53
  @@site
48
54
  end
49
55
 
56
+ def url
57
+ config['url'].tap do |u|
58
+ abort_if_missing('activity_pub.url', u, '_config.yml')
59
+ end
60
+ end
61
+
50
62
  # @return [String]
51
63
  def followers_url
52
- "#{config['url']}/v1/#{actor}/followers"
64
+ "#{url}/v1/#{actor}/followers"
53
65
  end
54
66
 
55
67
  # @return [String]
56
68
  def following_url
57
- "#{config['url']}/v1/#{actor}/following"
69
+ "#{url}/v1/#{actor}/following"
58
70
  end
59
71
 
60
72
  # Save the public key URL for later
@@ -64,11 +76,13 @@ module Jekyll
64
76
  data['public_key_url'] = url
65
77
  end
66
78
 
67
- # Public key URL
79
+ # Public key URL, raises error if missing
68
80
  #
69
- # @return [String,nil]
81
+ # @return [String]
70
82
  def public_key_url
71
- data['public_key_url']
83
+ data['public_key_url'].tap do |pk|
84
+ abort_if_missing('public_key_url', pk)
85
+ end
72
86
  end
73
87
 
74
88
  def actor=(actor)
@@ -76,7 +90,9 @@ module Jekyll
76
90
  end
77
91
 
78
92
  def actor
79
- data['actor']
93
+ data['actor'].tap do |a|
94
+ abort_if_missing('actor', a)
95
+ end
80
96
  end
81
97
 
82
98
  def actor_url=(url)
@@ -84,7 +100,9 @@ module Jekyll
84
100
  end
85
101
 
86
102
  def actor_url
87
- data['actor_url']
103
+ data['actor_url'].tap do |au|
104
+ abort_if_missing('actor_url', au)
105
+ end
88
106
  end
89
107
 
90
108
  # Send notifications
@@ -107,7 +125,7 @@ module Jekyll
107
125
 
108
126
  base_endpoint = "/v1/#{actor}"
109
127
  outbox_endpoint = "#{base_endpoint}/outbox"
110
- actor_object = PseudoObject.new(url: actor_url)
128
+ actor_object = object_for(site.in_dest_dir(actor_url.sub(site.config['url'], '')))
111
129
  # TODO: Move to API client
112
130
  inbox_body = {
113
131
  'actorUrl' => actor_url,
@@ -130,13 +148,20 @@ module Jekyll
130
148
  process_object(outbox_endpoint, actor_object, object_for(object_url), status)
131
149
  end
132
150
 
151
+ # Update actor profile
152
+ if actor_object.updated_at > actor_object.date
153
+ Jekyll.logger.debug 'ActivityPub:', 'Updating Actor profile'
154
+ actor_update = Jekyll::ActivityPub::Update.new(site, actor_object, actor_object)
155
+
156
+ unless (response = client.post(endpoint: outbox_endpoint, body: actor_update)).ok?
157
+ raise NotificationError, "Couldn't update actor (#{response.code}: #{response.message})"
158
+ end
159
+ end
160
+
133
161
  # Store everything for later
134
162
  save
135
- end
136
-
137
- # @return [Jekyll::Site]
138
- def site
139
- @@site
163
+ rescue NotificationError => e
164
+ Jekyll.logger.abort_with 'ActivityPub:', e.message
140
165
  end
141
166
 
142
167
  # Return data
@@ -159,12 +184,12 @@ module Jekyll
159
184
  #
160
185
  # @param :path [String]
161
186
  # @return [nil]
162
- def update(path)
187
+ def update(path, **opts)
163
188
  # Compare Unix timestamps
164
189
  if created?(path) && (object_for(path)&.updated_at&.to_i || 0) > (status(path)['updated_at'] || 0)
165
- action(path, 'update')
190
+ action(path, 'update', **opts)
166
191
  else
167
- create(path)
192
+ create(path, **opts)
168
193
  end
169
194
 
170
195
  nil
@@ -174,8 +199,8 @@ module Jekyll
174
199
  #
175
200
  # @param :path [String]
176
201
  # @return [nil]
177
- def create(path)
178
- action(path, 'create') unless created?(path)
202
+ def create(path, **opts)
203
+ action(path, 'create', **opts) unless created?(path)
179
204
 
180
205
  nil
181
206
  end
@@ -201,21 +226,21 @@ module Jekyll
201
226
  #
202
227
  # @param :path [String]
203
228
  def created?(path)
204
- !(status(path)['created_at'].nil?)
229
+ !status(path)['created_at'].nil?
205
230
  end
206
231
 
207
232
  # @param :path [String]
208
233
  def updated?(path)
209
- !(status(path)['updated_at'].nil?)
234
+ !status(path)['updated_at'].nil?
210
235
  end
211
236
 
212
237
  # @param :path [String]
213
238
  def deleted?(path)
214
- !(status(path)['deleted_at'].nil?)
239
+ !status(path)['deleted_at'].nil?
215
240
  end
216
241
 
217
242
  def exist?(path)
218
- !(status(path)['action'].nil?)
243
+ !status(path)['action'].nil?
219
244
  end
220
245
 
221
246
  # Stores data back to a file and optionally commits it
@@ -263,7 +288,9 @@ module Jekyll
263
288
  #
264
289
  # @return [String, nil]
265
290
  def private_key_path
266
- @@private_key_path ||= site.config['activity_pub_private_key']
291
+ @@private_key_path ||= site.config['activity_pub_private_key'].tap do |pk|
292
+ abort_if_missing '--key', pk, 'notify command'
293
+ end
267
294
  end
268
295
 
269
296
  # Returns the private key
@@ -272,8 +299,7 @@ module Jekyll
272
299
  def private_key
273
300
  @@private_key ||= File.read private_key_path
274
301
  rescue StandardError
275
- Jekyll.logger.warn 'ActivityPub:', 'There\'s an issue with your private key'
276
- raise
302
+ Jekyll.logger.abort_with 'ActivityPub:', 'There\'s an issue with your private key'
277
303
  end
278
304
 
279
305
  # @return [Hash]
@@ -284,7 +310,7 @@ module Jekyll
284
310
  def client
285
311
  @@client ||= DistributedPress::V1::Social::Client.new(
286
312
  private_key_pem: private_key,
287
- url: config['url'],
313
+ url: url,
288
314
  public_key_url: public_key_url,
289
315
  logger: Jekyll.logger
290
316
  )
@@ -294,12 +320,15 @@ module Jekyll
294
320
  #
295
321
  # @param :path [String]
296
322
  # @param :action [String]
297
- # @return [Hash]
298
- def action(path, action)
323
+ # @return [nil]
324
+ def action(path, action, **opts)
299
325
  path = path_relative_to_dest(path)
300
326
 
301
327
  data['notifications'][path] ||= {}
302
328
  data['notifications'][path]['action'] = action.to_s
329
+ data['notifications'][path].merge! opts.transform_keys(&:to_s)
330
+
331
+ nil
303
332
  end
304
333
 
305
334
  # Paths are relative to site destination
@@ -342,10 +371,17 @@ module Jekyll
342
371
  rel = path_relative_to_dest(path)
343
372
  path = site.in_dest_dir(rel)
344
373
  data = JSON.parse(File.read(path)) if File.exist? path
374
+ data ||= { 'id' => status(path)['id'] }
345
375
 
346
- PseudoObject.new(url: rel, site: site, relative_path: rel, data: data || {})
376
+ PseudoObject.new(url: rel, site: site, relative_path: rel, data: data)
347
377
  end
348
378
  end
379
+
380
+ def abort_if_missing(key_name, value, file = '_data/activity_pub.yml')
381
+ return unless value.nil? || value.empty?
382
+
383
+ Jekyll.logger.abort_with 'ActivityPub:', "Missing #{key_name} in #{file}"
384
+ end
349
385
  end
350
386
  end
351
387
  end
@@ -37,9 +37,23 @@ module Jekyll
37
37
  'rel' => 'self',
38
38
  'type' => 'application/activity+json',
39
39
  'href' => absolute_url(@actor.url)
40
+ },
41
+ {
42
+ 'rel' => 'http://webfinger.net/rel/profile-page',
43
+ 'type' => 'text/html',
44
+ 'href' => site.config['url']
40
45
  }
41
46
  ]
42
- }
47
+ }.tap do |data|
48
+ next unless @actor.data['icon']
49
+
50
+ data['links'] <<
51
+ {
52
+ 'rel' => 'http://webfinger.net/rel/avatar',
53
+ 'type' => @actor.data.dig('icon', 'mediaType'),
54
+ 'href' => @actor.data.dig('icon', 'url')
55
+ }
56
+ end
43
57
  end
44
58
 
45
59
  # The webfinger file is expected to be at this location always
@@ -10,6 +10,12 @@ module Jekyll
10
10
  def add_build_options(cmd)
11
11
  super
12
12
 
13
+ activity_pub_private_key(cmd)
14
+ end
15
+
16
+ private
17
+
18
+ def activity_pub_private_key(cmd)
13
19
  cmd.option 'activity_pub_private_key', '--key path/to/rsa.pem', String, 'Path to RSA private key in PEM format'
14
20
  end
15
21
  end
@@ -6,14 +6,14 @@ require 'distributed_press/v1/social/client'
6
6
  module Jekyll
7
7
  module Commands
8
8
  # Send Activity Pub notifications
9
- class Notify < Jekyll::Command
9
+ class GenerateKeys < Jekyll::Command
10
10
  class << self
11
11
  def init_with_program(prog)
12
12
  prog.command(:generate_keys) do |c|
13
13
  c.syntax 'generate_keys --key-size 2048 --key path/to/privkey.pem'
14
14
  c.description 'Generate an RSA keypair for ActivityPub'
15
15
 
16
- add_build_options(c)
16
+ activity_pub_private_key c
17
17
 
18
18
  c.option 'activity_pub_key_size', '--key-size 2048', Integer, 'RSA key size (2048 by default)'
19
19
 
@@ -34,7 +34,6 @@ module Jekyll
34
34
  raise Jekyll::Errors::FatalException, "Private key already exists: #{private_key_path}"
35
35
  end
36
36
 
37
- options = configuration_from_options(options)
38
37
  client = DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: key_size)
39
38
 
40
39
  File.open(private_key_path, 'w') do |f|
@@ -77,7 +77,11 @@ Jekyll::Hooks.register(:documents, :post_convert, priority: :high) do |doc|
77
77
  end
78
78
 
79
79
  create_or_update.new(site, actor, activity).tap do |action|
80
- Jekyll::ActivityPub::Notifier.public_send action.data['type'].downcase.to_sym, activity.destination(site.dest)
80
+ method = action.data['type'].downcase.to_sym
81
+ path = activity.destination(site.dest)
82
+ id = activity.data['id']
83
+
84
+ Jekyll::ActivityPub::Notifier.public_send(method, path, id: id)
81
85
 
82
86
  outbox.data['totalItems'] += 1
83
87
  outbox.data['orderedItems'] << action
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-activity-pub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0rc12
4
+ version: 0.1.0rc14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sutty
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-07 00:00:00.000000000 Z
11
+ date: 2023-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: distributed-press-api-client
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: marcel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: pry
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -129,7 +143,6 @@ files:
129
143
  - lib/jekyll/activity_pub.rb
130
144
  - lib/jekyll/activity_pub/activity.rb
131
145
  - lib/jekyll/activity_pub/actor.rb
132
- - lib/jekyll/activity_pub/cache.rb
133
146
  - lib/jekyll/activity_pub/commands.rb
134
147
  - lib/jekyll/activity_pub/create.rb
135
148
  - lib/jekyll/activity_pub/delete.rb
@@ -144,7 +157,6 @@ files:
144
157
  - lib/jekyll/activity_pub/outbox.rb
145
158
  - lib/jekyll/activity_pub/property_value.rb
146
159
  - lib/jekyll/activity_pub/public_key.rb
147
- - lib/jekyll/activity_pub/tomsbtone.rb
148
160
  - lib/jekyll/activity_pub/update.rb
149
161
  - lib/jekyll/activity_pub/webfinger.rb
150
162
  - lib/jekyll/command_extension.rb
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'jekyll/cache'
4
-
5
- module Jekyll
6
- module ActivityPub
7
- class Cache < Jekyll::Cache
8
- end
9
- end
10
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'jekyll/page'
4
- require_relative 'helper'
5
-
6
- module Jekyll
7
- module ActivityPub
8
- # Represents a removed activity
9
- class Tombstone < Jekyll::Page
10
- include Helper
11
-
12
- # @param :object_id [String]
13
- attr_reader :object_id
14
-
15
- # Initialize with default data
16
- #
17
- # @param :site [Jekyll::Site]
18
- # @param :object_id [String]
19
- def initialize(site, object_id, )
20
- @context = StubContext.new(registers: { site: site })
21
- @object_id = object_id
22
-
23
- dest = Pathname.new(object.destination(site.dest)).relative_path_from(site.dest)
24
- name = 'tombstone.jsonld'
25
- dir = File.join(
26
- File.dirname(dest),
27
- File.basename(dest, '.jsonld')
28
- )
29
-
30
- super(site, '', dir, name)
31
-
32
- trigger_hooks :post_init
33
- end
34
-
35
- def read_yaml(*)
36
- @data = {
37
- '@context' => 'https://www.w3.org/ns/activitystreams',
38
- 'type' => 'Tomsbtone',
39
- 'id' => absolute_url(object_id),
40
- 'published' => object.data[DATE_ATTRIBUTE],
41
- 'to' => object.data['to'],
42
- 'cc' => object.data['cc'],
43
- 'object' => object.data,
44
- 'inReplyTo' => object.data['in_reply_to']
45
- }
46
- end
47
-
48
- # @return [Time]
49
- def date
50
- @date ||= Time.parse(object.data[DATE_ATTRIBUTE])
51
- end
52
-
53
- private
54
-
55
- def type
56
- @type ||= self.class.name.split('::').last
57
- end
58
- end
59
- end
60
- end
61
-