jekyll-activity-pub 0.1.0rc12 → 0.1.0rc14

Sign up to get free protection for your applications and to get access to all the features.
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
-