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