jekyll-activity-pub 0.1.0rc0
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 +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +6 -0
- data/lib/jekyll/activity_pub/activity.rb +99 -0
- data/lib/jekyll/activity_pub/actor.rb +109 -0
- data/lib/jekyll/activity_pub/cache.rb +10 -0
- data/lib/jekyll/activity_pub/commands.rb +4 -0
- data/lib/jekyll/activity_pub/create.rb +41 -0
- data/lib/jekyll/activity_pub/delete.rb +27 -0
- data/lib/jekyll/activity_pub/document.rb +23 -0
- data/lib/jekyll/activity_pub/errors.rb +25 -0
- data/lib/jekyll/activity_pub/followers.rb +25 -0
- data/lib/jekyll/activity_pub/following.rb +25 -0
- data/lib/jekyll/activity_pub/helper.rb +158 -0
- data/lib/jekyll/activity_pub/host_meta.rb +45 -0
- data/lib/jekyll/activity_pub/image.rb +32 -0
- data/lib/jekyll/activity_pub/notifier.rb +268 -0
- data/lib/jekyll/activity_pub/ordered_collection.rb +137 -0
- data/lib/jekyll/activity_pub/ordered_collection_page.rb +41 -0
- data/lib/jekyll/activity_pub/outbox.rb +25 -0
- data/lib/jekyll/activity_pub/property_value.rb +26 -0
- data/lib/jekyll/activity_pub/public_key.rb +50 -0
- data/lib/jekyll/activity_pub/update.rb +24 -0
- data/lib/jekyll/activity_pub/webfinger.rb +56 -0
- data/lib/jekyll/activity_pub.rb +18 -0
- data/lib/jekyll/command_extension.rb +18 -0
- data/lib/jekyll/commands/generate_keys.rb +52 -0
- data/lib/jekyll/commands/notify.rb +47 -0
- data/lib/jekyll-activity-pub.rb +93 -0
- metadata +189 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jekyll/hooks'
|
4
|
+
|
5
|
+
module Jekyll
|
6
|
+
module ActivityPub
|
7
|
+
# Container for common tools
|
8
|
+
module Helper
|
9
|
+
include Jekyll::Filters::URLFilters
|
10
|
+
|
11
|
+
# Some filters needs a Liquid-like context
|
12
|
+
StubContext = Struct.new(:registers, keyword_init: true)
|
13
|
+
|
14
|
+
# Renders the data hash as a stringified JSON
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def content
|
18
|
+
data.to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
# JSONify this object
|
22
|
+
def to_json(*args)
|
23
|
+
data.to_json(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
# There's no excerpt to be generated
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
29
|
+
def generate_excerpt?
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
# Trigger hooks
|
34
|
+
#
|
35
|
+
# @param :hook_name [Symbol]
|
36
|
+
# @param :args [any]
|
37
|
+
# @return [nil]
|
38
|
+
def trigger_hooks(hook_name, *args)
|
39
|
+
Jekyll::Hooks.trigger hook_owner, hook_name.to_sym, self, *args
|
40
|
+
end
|
41
|
+
|
42
|
+
# Simpler version of ActiveSupport::Inflector#underscore
|
43
|
+
#
|
44
|
+
# https://stackoverflow.com/a/1510078
|
45
|
+
#
|
46
|
+
# @see Jekyll::Convertible
|
47
|
+
# @return [Symbol]
|
48
|
+
def hook_owner
|
49
|
+
@hook_owner ||= self.class.name.split('::').last.gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
alias type hook_owner
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# Is Liquid on strict mode?
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def strict?
|
60
|
+
site.config.dig('liquid', 'strict_variables')
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns site name, for use as a username. "site" by default.
|
64
|
+
#
|
65
|
+
# @return [String]
|
66
|
+
def username
|
67
|
+
@username ||= site.config.dig('activity_pub', 'username')
|
68
|
+
@username ||= hostname&.split('.', 2)&.first
|
69
|
+
@username ||= 'site'
|
70
|
+
end
|
71
|
+
|
72
|
+
# Return hostname
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
def hostname
|
76
|
+
return @hostname if defined? @hostname
|
77
|
+
|
78
|
+
@hostname = site.config.dig('activity_pub', 'hostname')
|
79
|
+
@hostname ||= site.config['hostname']
|
80
|
+
@hostname ||= Addressable::URI.parse(site.config['url']).hostname if site.config['url']
|
81
|
+
@hostname ||= File.read(cname_file).strip if cname_file?
|
82
|
+
|
83
|
+
if @hostname.nil? || @hostname.empty?
|
84
|
+
raise ArgumentError, 'Site must have a hostname' if strict?
|
85
|
+
|
86
|
+
Jekyll.logger.warn 'ActivityPub:', 'Site must have a hostname'
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
@hostname
|
91
|
+
end
|
92
|
+
|
93
|
+
# Site uses CNAME file
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
def cname_file
|
97
|
+
@cname_file ||= site.in_source_dir('CNAME')
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Boolean]
|
101
|
+
def cname_file?
|
102
|
+
File.exist?(cname_file)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Detects locale
|
106
|
+
#
|
107
|
+
# @return [String]
|
108
|
+
def locale
|
109
|
+
return @locale if defined? @locale
|
110
|
+
|
111
|
+
@locale = site.config['locale']
|
112
|
+
@locale ||= ENV['LANG']&.split('.', 2)&.first
|
113
|
+
|
114
|
+
@locale = @locale&.tr('_', '-')
|
115
|
+
end
|
116
|
+
|
117
|
+
# Finds the value of a text field amongst options
|
118
|
+
#
|
119
|
+
# @params :hash [Hash] The haystack
|
120
|
+
# @param :args [String,Array] A combination of paths to find
|
121
|
+
# a value
|
122
|
+
# @return [Any, nil]
|
123
|
+
def find_best_value_for(hash, *args)
|
124
|
+
raise ArgumentError, 'First argument must be hash' unless hash.is_a? Hash
|
125
|
+
|
126
|
+
field = args.find do |f|
|
127
|
+
!hash.dig(*f).nil? && !hash.dig(*f).empty?
|
128
|
+
rescue TypeError
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
hash.dig(*field) if field
|
133
|
+
end
|
134
|
+
|
135
|
+
# Raise an exception if the value is required but empty
|
136
|
+
#
|
137
|
+
# @param :value [String,nil]
|
138
|
+
# @param :exception [Object]
|
139
|
+
def value_is_required!(value, exception)
|
140
|
+
raise exception if value.nil? || value.empty?
|
141
|
+
rescue exception => e
|
142
|
+
raise if strict?
|
143
|
+
|
144
|
+
Jekyll.logger.warn 'ActivityPub:', e.message
|
145
|
+
end
|
146
|
+
|
147
|
+
# Generates an absolute URL if not empty
|
148
|
+
#
|
149
|
+
# @param :url [String,nil]
|
150
|
+
# @return [nil,String]
|
151
|
+
def conditional_absolute_url(url)
|
152
|
+
return if url.nil? || url.empty?
|
153
|
+
|
154
|
+
absolute_url url
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jekyll/page_without_a_file'
|
4
|
+
require_relative 'helper'
|
5
|
+
|
6
|
+
module Jekyll
|
7
|
+
module ActivityPub
|
8
|
+
# Host meta
|
9
|
+
#
|
10
|
+
# https://www.rfc-editor.org/rfc/rfc6415
|
11
|
+
class HostMeta < Jekyll::PageWithoutAFile
|
12
|
+
include Helper
|
13
|
+
|
14
|
+
# Initialize with default data
|
15
|
+
#
|
16
|
+
# @param :site [Jekyll::Site]
|
17
|
+
# @param :base [String]
|
18
|
+
# @param :dir [String]
|
19
|
+
# @param :name [String]
|
20
|
+
def initialize(site, webfinger, base = '', dir = '.well-known', name = 'host-meta')
|
21
|
+
super(site, base, dir, name)
|
22
|
+
|
23
|
+
@webfinger = webfinger
|
24
|
+
@context = StubContext.new(registers: { site: site })
|
25
|
+
|
26
|
+
trigger_hooks :post_init
|
27
|
+
end
|
28
|
+
|
29
|
+
def permalink
|
30
|
+
'.well-known/host-meta'
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String]
|
34
|
+
def content
|
35
|
+
@content ||=
|
36
|
+
<<~CONTENT
|
37
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
38
|
+
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
39
|
+
<Link rel="lrdd" template="#{absolute_url @webfinger.url}"/>
|
40
|
+
</XRD>
|
41
|
+
CONTENT
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
|
5
|
+
module Jekyll
|
6
|
+
module ActivityPub
|
7
|
+
# Represents an Image
|
8
|
+
class Image
|
9
|
+
include Helper
|
10
|
+
|
11
|
+
attr_reader :data
|
12
|
+
|
13
|
+
# Initialize with default data
|
14
|
+
#
|
15
|
+
# @param :site [Jekyll::Site]
|
16
|
+
# @param :path [Path]
|
17
|
+
# @param :description [String]
|
18
|
+
def initialize(site, path, description = nil)
|
19
|
+
@context = StubContext.new(registers: { site: site })
|
20
|
+
|
21
|
+
@data = {
|
22
|
+
'type' => 'Image',
|
23
|
+
'mediaType' => "image/#{File.extname(path).sub('.', '')}",
|
24
|
+
'url' => absolute_url(path),
|
25
|
+
'name' => description.to_s
|
26
|
+
}
|
27
|
+
|
28
|
+
trigger_hooks :post_init
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'httparty'
|
5
|
+
require 'distributed_press/v1/social/client'
|
6
|
+
require_relative 'errors'
|
7
|
+
require_relative 'create'
|
8
|
+
require_relative 'update'
|
9
|
+
require_relative 'delete'
|
10
|
+
|
11
|
+
module Jekyll
|
12
|
+
module ActivityPub
|
13
|
+
# Long term store for notifications.
|
14
|
+
#
|
15
|
+
# Needs to be a singleton so we can use the same data across all of
|
16
|
+
# Jekyll's build process.
|
17
|
+
class Notifier
|
18
|
+
# An struct that responds to a #url method
|
19
|
+
PseudoObject = Struct.new(:url, keyword_init: true)
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# Set the site and initialize data
|
23
|
+
#
|
24
|
+
# @param :site [Jekyll::Site]
|
25
|
+
# @return [Jekyll::Site]
|
26
|
+
def site=(site)
|
27
|
+
@@site = site.tap do |s|
|
28
|
+
s.data['activity_pub'] ||= {}
|
29
|
+
s.data['activity_pub']['notifications'] ||= {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Save the public key URL for later
|
34
|
+
#
|
35
|
+
# @param :url [String]
|
36
|
+
def public_key_url=(url)
|
37
|
+
data['public_key_url'] = url
|
38
|
+
end
|
39
|
+
|
40
|
+
# Public key URL
|
41
|
+
#
|
42
|
+
# @return [String,nil]
|
43
|
+
def public_key_url
|
44
|
+
data['public_key_url']
|
45
|
+
end
|
46
|
+
|
47
|
+
def actor=(actor)
|
48
|
+
data['actor'] = actor
|
49
|
+
end
|
50
|
+
|
51
|
+
def actor
|
52
|
+
data['actor']
|
53
|
+
end
|
54
|
+
|
55
|
+
def actor_url=(url)
|
56
|
+
data['actor_url'] = url
|
57
|
+
end
|
58
|
+
|
59
|
+
def actor_url
|
60
|
+
data['actor_url']
|
61
|
+
end
|
62
|
+
|
63
|
+
# Send notifications
|
64
|
+
#
|
65
|
+
# 1. Wait for public key propagation
|
66
|
+
# 2. Create/update inbox
|
67
|
+
# 3. Send create, update, and delete
|
68
|
+
def notify!
|
69
|
+
# TODO: request several times with a timeout
|
70
|
+
response = HTTParty.get(public_key_url)
|
71
|
+
|
72
|
+
unless response.ok?
|
73
|
+
raise NotificationError,
|
74
|
+
"Could't fetch public key (#{response.code}: #{response.message})"
|
75
|
+
end
|
76
|
+
|
77
|
+
unless client.private_key.compare? OpenSSL::Pkey::RSA.new(response.body)
|
78
|
+
raise NotificationError, "Public key at #{public_key_url} differs from local version"
|
79
|
+
end
|
80
|
+
|
81
|
+
base_endpoint = "/v1/#{actor}/"
|
82
|
+
outbox_endpoint = "#{base_endpoint}/outbox/"
|
83
|
+
actor_object = PseudoObject.new(url: actor_url)
|
84
|
+
|
85
|
+
# Create inbox
|
86
|
+
# TODO: Send keypair
|
87
|
+
client.post(endpoint: base_endpoint, body: {})
|
88
|
+
|
89
|
+
# Remove notifications already performed and notify
|
90
|
+
data['notifications'].reject do |object_url, status|
|
91
|
+
done? object_url, status
|
92
|
+
end.each do |object_url, status|
|
93
|
+
process_object(outbox_endpoint, actor_object, PseudoObject.new(url: object_url), status)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Store everything for later
|
97
|
+
save
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Jekyll::Site]
|
101
|
+
def site
|
102
|
+
@@site
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return data
|
106
|
+
#
|
107
|
+
# @return [Hash]
|
108
|
+
def data
|
109
|
+
@@data ||= site.data['activity_pub']
|
110
|
+
end
|
111
|
+
|
112
|
+
# Removes an activity if it was previously created
|
113
|
+
#
|
114
|
+
# @param :path [String]
|
115
|
+
# @return [Hash]
|
116
|
+
def delete(path)
|
117
|
+
action(path, 'delete') if exist? path
|
118
|
+
end
|
119
|
+
|
120
|
+
# Updates an activity if it was previously created
|
121
|
+
#
|
122
|
+
# @param :path [String]
|
123
|
+
# @return [Hash]
|
124
|
+
def update(path)
|
125
|
+
action(path, 'update') if exist? path
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates an activity
|
129
|
+
#
|
130
|
+
# @param :path [String]
|
131
|
+
# @return [Hash]
|
132
|
+
def create(path)
|
133
|
+
action(path, 'create')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check if activity existed
|
137
|
+
#
|
138
|
+
# @param :path [String]
|
139
|
+
# @return [Boolean]
|
140
|
+
def exist?(path)
|
141
|
+
data['notifications'].key? path_relative_to_dest(path)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Stores data back to a file and optionally commits it
|
145
|
+
#
|
146
|
+
# @return [nil]
|
147
|
+
def save
|
148
|
+
# TODO: Send warning if CI is detected
|
149
|
+
Jekyll.logger.info 'ActivityPub:', "Saving data to #{relative_path}"
|
150
|
+
|
151
|
+
FileUtils.mkdir_p(File.dirname(path))
|
152
|
+
|
153
|
+
File.open(path, 'w') do |f|
|
154
|
+
f.flock(File::LOCK_EX)
|
155
|
+
f.rewind
|
156
|
+
f.write(YAML.dump(data))
|
157
|
+
f.flush
|
158
|
+
f.truncate(f.pos)
|
159
|
+
end
|
160
|
+
|
161
|
+
if ENV['JEKYLL_ENV'] == 'production' && site.respond_to?(:repository)
|
162
|
+
site.staged_files << relative_path
|
163
|
+
site.repository.commit 'ActivityPub'
|
164
|
+
end
|
165
|
+
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the path for the storage
|
170
|
+
#
|
171
|
+
# @return [String]
|
172
|
+
def path
|
173
|
+
@@path ||= site.in_source_dir(site.config['data_dir'], 'activity_pub.yml')
|
174
|
+
end
|
175
|
+
|
176
|
+
# Storage path relative to site source
|
177
|
+
#
|
178
|
+
# @return [String]
|
179
|
+
def relative_path
|
180
|
+
@@relative_path ||= Pathname.new(path).relative_path_from(site.source).to_s
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
# Finds the private key path on config
|
186
|
+
#
|
187
|
+
# @return [String, nil]
|
188
|
+
def private_key_path
|
189
|
+
@@private_key_path ||= site.config['activity_pub_private_key']
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns the private key
|
193
|
+
#
|
194
|
+
# @return [String, nil]
|
195
|
+
def private_key
|
196
|
+
@@private_key ||= File.read private_key_path
|
197
|
+
rescue StandardError
|
198
|
+
Jekyll.logger.warn 'ActivityPub:', 'There\'s an issue with your private key'
|
199
|
+
raise
|
200
|
+
end
|
201
|
+
|
202
|
+
# @return [Hash]
|
203
|
+
def config
|
204
|
+
@@config ||= site.config['activity_pub'] || {}
|
205
|
+
end
|
206
|
+
|
207
|
+
def client
|
208
|
+
@@client ||= DistributedPress::V1::Social::Client.new(
|
209
|
+
private_key_pem: private_key,
|
210
|
+
url: config['url'],
|
211
|
+
public_key_url: public_key_url
|
212
|
+
)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Run action
|
216
|
+
#
|
217
|
+
# @param :path [String]
|
218
|
+
# @param :action [String]
|
219
|
+
# @return [Hash]
|
220
|
+
def action(path, action)
|
221
|
+
path = path_relative_to_dest(path)
|
222
|
+
|
223
|
+
data['notifications'][path] ||= {}
|
224
|
+
data['notifications'][path]['action'] = action.to_s
|
225
|
+
end
|
226
|
+
|
227
|
+
# Paths are relative to site destination
|
228
|
+
#
|
229
|
+
# @param :path [String]
|
230
|
+
# @return [String]
|
231
|
+
def path_relative_to_dest(path)
|
232
|
+
Pathname.new(site.in_dest_dir(path)).relative_path_from(site.dest).to_s
|
233
|
+
end
|
234
|
+
|
235
|
+
# Detects if an action was already done
|
236
|
+
def done?(url, status)
|
237
|
+
(status['action'] == 'done').tap do |done|
|
238
|
+
Jekyll.logger.debug('ActivityPub:', "Skipping notification for #{url}") if done
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Turns an object into an activity and notifies outbox
|
243
|
+
#
|
244
|
+
# @param :endpoint [String]
|
245
|
+
# @param :actor [PseudoObject]
|
246
|
+
# @param :object [PseudoObject]
|
247
|
+
# @param :status [Hash]
|
248
|
+
# @return [nil]
|
249
|
+
def process_object(endpoint, actor, object, status)
|
250
|
+
action = status['action']
|
251
|
+
activity = Object.const_get(action.capitalize).new(site, actor, object)
|
252
|
+
|
253
|
+
if (response = client.post(endpoint: endpoint, body: activity)).ok?
|
254
|
+
status['action'] = 'done'
|
255
|
+
status["#{action}d_at"] = Time.now.to_i
|
256
|
+
else
|
257
|
+
Jekyll.logger.warn 'ActivityPub:',
|
258
|
+
"Couldn't perform #{action} for #{object_url} (#{response.code}: #{response.message})"
|
259
|
+
end
|
260
|
+
|
261
|
+
nil
|
262
|
+
rescue NameError
|
263
|
+
Jekyll.logger.warn 'ActivityPub:', "Action \"#{action}\" for #{url} unrecognized, ignoring."
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jekyll/page'
|
4
|
+
require_relative 'helper'
|
5
|
+
require_relative 'ordered_collection_page'
|
6
|
+
|
7
|
+
module Jekyll
|
8
|
+
module ActivityPub
|
9
|
+
# An ordered collection of activities
|
10
|
+
class OrderedCollection < Jekyll::Page
|
11
|
+
include Helper
|
12
|
+
|
13
|
+
# Initialize with default data
|
14
|
+
#
|
15
|
+
# @param :site [Jekyll::Site]
|
16
|
+
# @param :base [String]
|
17
|
+
# @param :dir [String]
|
18
|
+
# @param :name [String]
|
19
|
+
def initialize(site, base = '', dir = '', name = 'ordered_collection.jsonld')
|
20
|
+
@context = StubContext.new(registers: { site: site })
|
21
|
+
|
22
|
+
super
|
23
|
+
|
24
|
+
trigger_hooks :post_init
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_yaml(*)
|
28
|
+
self.data = {
|
29
|
+
'@context' => 'https://www.w3.org/ns/activitystreams',
|
30
|
+
'id' => absolute_url(url),
|
31
|
+
'type' => 'OrderedCollection',
|
32
|
+
'totalItems' => 0,
|
33
|
+
'orderedItems' => []
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
alias original_content content
|
38
|
+
|
39
|
+
# Paginates the collection if it has too many activities
|
40
|
+
#
|
41
|
+
# @return [String]
|
42
|
+
def content
|
43
|
+
@content ||=
|
44
|
+
begin
|
45
|
+
order_items!
|
46
|
+
|
47
|
+
return original_content unless always_paginate? || paginable?
|
48
|
+
return original_content if data['orderedItems'].empty?
|
49
|
+
|
50
|
+
paged_data = data.dup
|
51
|
+
pages = paginate(paged_data.delete('orderedItems'))
|
52
|
+
|
53
|
+
assign_links! pages
|
54
|
+
|
55
|
+
paged_data['first'] = absolute_url(pages.first.url)
|
56
|
+
paged_data['last'] = absolute_url(pages.last.url)
|
57
|
+
|
58
|
+
paged_data.to_json
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Items per page
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
def items_per_page
|
68
|
+
@items_per_page ||=
|
69
|
+
begin
|
70
|
+
from_config = site.config.dig('activity_pub', 'items_per_page').to_i
|
71
|
+
if from_config.positive?
|
72
|
+
from_config
|
73
|
+
else
|
74
|
+
Jekyll.logger.warn 'ActivityPub:', 'Items per page option empty or not valid, using 20 items per page'
|
75
|
+
20
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Force pagination
|
81
|
+
#
|
82
|
+
# @return [Boolean]
|
83
|
+
def always_paginate?
|
84
|
+
!!site.config.dig('activity_pub', 'always_paginate')
|
85
|
+
end
|
86
|
+
|
87
|
+
# Item count is higher than items per page
|
88
|
+
#
|
89
|
+
# @return [Boolean]
|
90
|
+
def paginable?
|
91
|
+
data['orderedItems'].size > items_per_page
|
92
|
+
end
|
93
|
+
|
94
|
+
# Generate pages
|
95
|
+
#
|
96
|
+
# @param :items [Array]
|
97
|
+
# @return [Array]
|
98
|
+
def paginate(items)
|
99
|
+
[].tap do |pages|
|
100
|
+
total_pages = items.count / items_per_page
|
101
|
+
|
102
|
+
items.each_slice(items_per_page).each_with_index do |paged_items, i|
|
103
|
+
OrderedCollectionPage.new(site, self, nil, basename, "#{total_pages - i}.jsonld").tap do |page|
|
104
|
+
page.data['orderedItems'] = paged_items
|
105
|
+
site.pages << page
|
106
|
+
pages << page
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Assign previous and next links between pages
|
113
|
+
#
|
114
|
+
# @param :pages [Array]
|
115
|
+
# @return [nil]
|
116
|
+
def assign_links!(pages)
|
117
|
+
pages.reduce do |previous, current|
|
118
|
+
current.data['prev'] = absolute_url(previous.url)
|
119
|
+
previous.data['next'] = absolute_url(current.url)
|
120
|
+
|
121
|
+
current
|
122
|
+
end
|
123
|
+
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
# Sort items
|
128
|
+
#
|
129
|
+
# @return [nil]
|
130
|
+
def order_items!
|
131
|
+
data['orderedItems'].sort! do |a, b|
|
132
|
+
b.data['published'] <=> a.data['published']
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jekyll/page'
|
4
|
+
require_relative 'helper'
|
5
|
+
|
6
|
+
module Jekyll
|
7
|
+
module ActivityPub
|
8
|
+
# A collection of activities
|
9
|
+
class OrderedCollectionPage < Jekyll::Page
|
10
|
+
include Helper
|
11
|
+
|
12
|
+
attr_reader :outbox
|
13
|
+
|
14
|
+
# Initialize with default data
|
15
|
+
#
|
16
|
+
# @param :site [Jekyll::Site]
|
17
|
+
# @param :outbox [Jekyll::ActivityPub::Outbox]
|
18
|
+
# @param :base [String]
|
19
|
+
# @param :dir [String]
|
20
|
+
# @param :name [String]
|
21
|
+
def initialize(site, outbox, base = '', dir = 'outbox', name = 'page.jsonld')
|
22
|
+
@context = StubContext.new(registers: { site: site })
|
23
|
+
@outbox = outbox
|
24
|
+
|
25
|
+
super(site, base, dir, name)
|
26
|
+
|
27
|
+
trigger_hooks :post_init
|
28
|
+
end
|
29
|
+
|
30
|
+
def read_yaml(*)
|
31
|
+
self.data = {
|
32
|
+
'@context' => 'https://www.w3.org/ns/activitystreams',
|
33
|
+
'id' => absolute_url(url),
|
34
|
+
'type' => 'OrderedCollectionPage',
|
35
|
+
'partOf' => absolute_url(outbox.url),
|
36
|
+
'orderedItems' => []
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'ordered_collection'
|
4
|
+
|
5
|
+
module Jekyll
|
6
|
+
module ActivityPub
|
7
|
+
# A collection of activities
|
8
|
+
class Outbox < OrderedCollection
|
9
|
+
# Initialize with default data
|
10
|
+
#
|
11
|
+
# @param :site [Jekyll::Site]
|
12
|
+
# @param :actor [Jekyll::ActivityPub::Actor]
|
13
|
+
# @param :base [String]
|
14
|
+
# @param :dir [String]
|
15
|
+
# @param :name [String]
|
16
|
+
def initialize(site, actor, base = '', dir = '', name = 'outbox.jsonld')
|
17
|
+
super(site, base, dir, name)
|
18
|
+
|
19
|
+
actor.data['outbox'] = absolute_url(url)
|
20
|
+
|
21
|
+
trigger_hooks :post_init
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|