blue_factory 0.2.1 → 0.3.0
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +1 -1
- data/README.md +7 -5
- data/lib/blue_factory/configuration.rb +24 -17
- data/lib/blue_factory/errors.rb +49 -3
- data/lib/blue_factory/feed_handler.rb +153 -0
- data/lib/blue_factory/interaction.rb +58 -1
- data/lib/blue_factory/modules/configurable.rb +17 -0
- data/lib/blue_factory/modules/feeds.rb +65 -6
- data/lib/blue_factory/modules/interactions.rb +39 -1
- data/lib/blue_factory/output_generator.rb +5 -0
- data/lib/blue_factory/request_context.rb +30 -1
- data/lib/blue_factory/server.rb +69 -12
- data/lib/blue_factory/tasks/publish.rake +15 -6
- data/lib/blue_factory/tasks/support.rb +20 -3
- data/lib/blue_factory/user_info.rb +33 -1
- data/lib/blue_factory/version.rb +1 -1
- data/lib/blue_factory.rb +30 -0
- metadata +21 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 137dfe791cea0735d1e273b584e5e9c0bc7bd475f55fa83480e6401b20d4f074
|
|
4
|
+
data.tar.gz: c191f1baf6c008233796c97eb67f6489b3cf43dcea2a99c5354bb51373410c53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9af6495afeae4b5698621433232432ee5deaaedcf55bab7090ab43ab37bdb0c12b47f5b0888d7fea747423f468ecf5378878a270187421c1f20b3fe345a05c2b
|
|
7
|
+
data.tar.gz: b6aa3624fb8c84a479611f143459dd4bfea1f835c59907fbf53ae23f2c8671f0605149fd835162d26940243d4e7975a3cc57cf638d293d93196e40484cf6dda0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [0.3.0] - 2026-02-15
|
|
2
|
+
|
|
3
|
+
- added YARD documentation
|
|
4
|
+
- added explicit `base64` dependency in gemspec
|
|
5
|
+
- added `MAX_LIMIT` (100) and `DEFAULT_LIMIT` (50) constants
|
|
6
|
+
- removed the length limit on feed rkeys, since it isn't actually limited to 15
|
|
7
|
+
- removed deprecated options `enable_unsafe_auth` and `validate_responses`
|
|
8
|
+
- fixed `rake:publish` not working with feed `description` is nil (thx @jthigpen)
|
|
9
|
+
- `rake:publish` shows a better error message when `createSession` requires a 2FA token
|
|
10
|
+
- added various additional checks:
|
|
11
|
+
* `Server` checks if `BlueFactory.hostname` and `BlueFactory.publisher_did` are set before launching
|
|
12
|
+
* `add_feed` checks if the key contains only valid characters
|
|
13
|
+
* `add_feed` checks if the feed class has a `#get_posts` method
|
|
14
|
+
* `getFeedSkeleton` ensures that the limit param is between 1 and 100 (so you don't need to check that)
|
|
15
|
+
* `raw_did` checks if the extracted payload contains an `iss` key
|
|
16
|
+
|
|
1
17
|
## [0.2.1] - 2025-12-07
|
|
2
18
|
|
|
3
19
|
- updated gemspec so the gem can be used with Sinatra 4.x
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
A Ruby gem for hosting custom feeds for Bluesky.
|
|
4
4
|
|
|
5
5
|
> [!NOTE]
|
|
6
|
-
> ATProto Ruby
|
|
6
|
+
> Part of ATProto Ruby SDK: [ruby.sdk.blue](https://ruby.sdk.blue)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
## What does it do
|
|
10
10
|
|
|
11
11
|
BlueFactory is a Ruby library which helps you build a web service that hosts custom feeds a.k.a. "[feed generators](https://github.com/bluesky-social/feed-generator)" for the Bluesky social network. It implements a simple HTTP server based on [Sinatra](https://sinatrarb.com) which provides the required endpoints for the feed generator interface. You need to provide the content for the feed by making a query to your preferred local database.
|
|
12
12
|
|
|
13
|
-
A feed server will usually be run together with a second piece of code that streams posts from the Bluesky "firehose" stream, runs them through some kind of filter and saves some or all of them to the database. To build that part, you can use my other Ruby gem [Skyfall](https://tangled.org
|
|
13
|
+
A feed server will usually be run together with a second piece of code that streams posts from the Bluesky "firehose" stream, runs them through some kind of filter and saves some or all of them to the database. To build that part, you can use my other Ruby gem [Skyfall](https://tangled.org/mackuba.eu/skyfall).
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
BlueFactory should run on any somewhat recent version of Ruby (3.x/4.x), although it's recommended to use one that's still getting maintenance updates, ideally the latest one. In production, it's also recommended to install it with [YJIT support](https://shopify.engineering/ruby-yjit-is-production-ready) and with [jemalloc](https://scalingo.com/blog/improve-ruby-application-memory-jemalloc). A compatible version should be preinstalled on many Linux systems, otherwise you can install one using tools such as [RVM](https://rvm.io), [asdf](https://asdf-vm.com), [ruby-install](https://github.com/postmodern/ruby-install) or [ruby-build](https://github.com/rbenv/ruby-build), or `rpm` or `apt-get` on Linux (see more installation options on [ruby-lang.org](https://www.ruby-lang.org/en/downloads/)).
|
|
19
|
+
|
|
20
|
+
To use it in your app, add this to your `Gemfile`:
|
|
19
21
|
|
|
20
22
|
gem 'blue_factory', '~> 0.2'
|
|
21
23
|
|
|
@@ -27,7 +29,7 @@ The server is configured through the `BlueFactory` module. The two required sett
|
|
|
27
29
|
- `publisher_did` – DID identifier of the account that you will publish the feed on (the string that starts with `did:plc:...`)
|
|
28
30
|
- `hostname` – the hostname on which the feed service will be run
|
|
29
31
|
|
|
30
|
-
You also need to configure at least one feed by passing a feed key and a feed object. The key is the identifier (rkey) that will appear at the end of the feed URI – it must only contain characters that are valid in URLs (preferably all lowercase) and it
|
|
32
|
+
You also need to configure at least one feed by passing a feed key and a feed object. The key is the identifier (rkey) that will appear at the end of the feed URI – it must only contain characters that are valid in URLs (preferably all lowercase) and it should be rather short. The feed object is anything that implements the single required method `get_posts` (could be a class, a module or an instance).
|
|
31
33
|
|
|
32
34
|
So a simple setup could look like this:
|
|
33
35
|
|
|
@@ -335,7 +337,7 @@ You also need to republish the feed by running the same task again any time you
|
|
|
335
337
|
|
|
336
338
|
## Credits
|
|
337
339
|
|
|
338
|
-
Copyright ©
|
|
340
|
+
Copyright © 2026 Kuba Suder ([@mackuba.eu](https://bsky.app/profile/did:plc:oio4hkxaop4ao4wz2pp3f4cr)).
|
|
339
341
|
|
|
340
342
|
The code is available under the terms of the [zlib license](https://choosealicense.com/licenses/zlib/) (permissive, similar to MIT).
|
|
341
343
|
|
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
require_relative 'modules/configurable'
|
|
2
2
|
require_relative 'modules/interactions'
|
|
3
3
|
require_relative 'modules/feeds'
|
|
4
|
+
require_relative 'errors'
|
|
4
5
|
|
|
5
6
|
module BlueFactory
|
|
6
7
|
extend Configurable
|
|
7
8
|
extend Feeds
|
|
8
9
|
extend Interactions
|
|
9
10
|
|
|
11
|
+
#
|
|
12
|
+
# The server's `did:web:` service DID built from the configured hostname.
|
|
13
|
+
# @return [String]
|
|
14
|
+
# @raise [ConfigurationError] if the hostname is not set
|
|
15
|
+
#
|
|
10
16
|
def self.service_did
|
|
17
|
+
if hostname.nil?
|
|
18
|
+
raise ConfigurationError, "The `hostname` property is not set. Set it with: BlueFactory.set(:hostname, 'example.com')"
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
'did:web:' + hostname
|
|
12
22
|
end
|
|
13
23
|
|
|
24
|
+
#
|
|
25
|
+
# Current application environment, usually `:development` or `:production`. It's read from the
|
|
26
|
+
# env variables `APP_ENV` or `RACK_ENV`, or `:development` by default.
|
|
27
|
+
#
|
|
28
|
+
# @return [Symbol]
|
|
29
|
+
#
|
|
14
30
|
def self.environment
|
|
15
31
|
(ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
|
|
16
32
|
end
|
|
17
33
|
|
|
18
|
-
|
|
34
|
+
# @!method self.hostname
|
|
35
|
+
# The hostname on which the feed server runs. Configured through {#set}.
|
|
36
|
+
# @return [String, nil]
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
puts "==="
|
|
26
|
-
@enable_unsafe_auth = value
|
|
27
|
-
elsif property.to_sym == :validate_responses
|
|
28
|
-
puts "==="
|
|
29
|
-
puts "WARNING: option :validate_responses is deprecated and will be removed in version 0.3. " +
|
|
30
|
-
"Responses are now always validated, also in production."
|
|
31
|
-
puts "==="
|
|
32
|
-
else
|
|
33
|
-
super
|
|
34
|
-
end
|
|
35
|
-
end
|
|
38
|
+
# @!method self.publisher_did
|
|
39
|
+
# The DID of the account publishing the feeds. Configured through {#set}.
|
|
40
|
+
# @return [String, nil]
|
|
41
|
+
|
|
42
|
+
configurable :publisher_did, :hostname
|
|
36
43
|
end
|
data/lib/blue_factory/errors.rb
CHANGED
|
@@ -1,31 +1,77 @@
|
|
|
1
1
|
module BlueFactory
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Raised during request processing if the authorization token can't be parsed; it can also be
|
|
5
|
+
# thrown from the user's {FeedHandler#get_posts} method to indicate that the given user is not
|
|
6
|
+
# authorized to access the requested feed, or that the feed requires authentication and it wasn't
|
|
7
|
+
# provided.
|
|
8
|
+
#
|
|
9
|
+
# The server intercepts this exception and returns a "401 Unauthorized" response to the AppView,
|
|
10
|
+
# which should result in the app displaying an error banner (along with the provided error
|
|
11
|
+
# message) in the feed view.
|
|
12
|
+
#
|
|
13
|
+
|
|
2
14
|
class AuthorizationError < StandardError
|
|
15
|
+
|
|
16
|
+
# @return [String, nil] machine-readable error identifier
|
|
3
17
|
attr_reader :error_type
|
|
4
18
|
|
|
5
|
-
|
|
19
|
+
# @param message [String] human-readable error message
|
|
20
|
+
# @param error_type [String] machine-readable error code
|
|
21
|
+
#
|
|
22
|
+
def initialize(message = "Authentication required", error_type = "AuthenticationRequired")
|
|
6
23
|
super(message)
|
|
7
24
|
@error_type = error_type
|
|
8
25
|
end
|
|
9
26
|
end
|
|
10
27
|
|
|
11
|
-
|
|
28
|
+
#
|
|
29
|
+
# Raised when some required configuration is missing or invalid.
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
class ConfigurationError < StandardError
|
|
12
33
|
end
|
|
13
34
|
|
|
35
|
+
#
|
|
36
|
+
# Raised during request processing if some of the parameters are invalid. The error is turned
|
|
37
|
+
# into a "400 Bad Request" response send to the AppView.
|
|
38
|
+
#
|
|
39
|
+
|
|
14
40
|
class InvalidRequestError < StandardError
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] machine-readable error identifier
|
|
15
43
|
attr_reader :error_type
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
# @param message [String] human-readable error message
|
|
46
|
+
# @param error_type [String] machine-readable error code
|
|
47
|
+
#
|
|
48
|
+
def initialize(message, error_type = "InvalidRequest")
|
|
18
49
|
super(message)
|
|
19
50
|
@error_type = error_type
|
|
20
51
|
end
|
|
21
52
|
end
|
|
22
53
|
|
|
54
|
+
#
|
|
55
|
+
# Raised during request processing if the response returned by the user's class from the
|
|
56
|
+
# {FeedHandler#get_posts} method doesn't fully match the expected format. The error is turned
|
|
57
|
+
# into a "500 Internal Server Error" response returned to the AppView.
|
|
58
|
+
#
|
|
59
|
+
|
|
23
60
|
class InvalidResponseError < StandardError
|
|
24
61
|
end
|
|
25
62
|
|
|
63
|
+
#
|
|
64
|
+
# Raised during request processing if the user's provided class does not have expected API.
|
|
65
|
+
#
|
|
66
|
+
|
|
26
67
|
class InvalidFeedClassError < StandardError
|
|
27
68
|
end
|
|
28
69
|
|
|
70
|
+
#
|
|
71
|
+
# Raised during request processing if the `:feed` parameter points to a feed generator URI
|
|
72
|
+
# which is not on the list of feeds configured in the app.
|
|
73
|
+
#
|
|
74
|
+
|
|
29
75
|
class UnsupportedAlgorithmError < StandardError
|
|
30
76
|
end
|
|
31
77
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module BlueFactory
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# @abstract This is not a real class, but it shows what *your* feed class should look like.
|
|
5
|
+
#
|
|
6
|
+
# Abstract interface describing the API that the feed class that will be handling the requests
|
|
7
|
+
# sent to `getFeedSkeleton`, which you pass to {BlueFactory::Feeds#add_feed}, is expected to have.
|
|
8
|
+
#
|
|
9
|
+
# All methods except {#get_posts} are only used briefly when submitting the feed as a record
|
|
10
|
+
# using the `bluesky:publish` rake task, so only {#get_posts} is really required during normal
|
|
11
|
+
# day to day operation.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
class FeedHandler
|
|
15
|
+
|
|
16
|
+
# The main method required to serve a feed. It accepts a hash of query params and optionally
|
|
17
|
+
# a request context object, and is expected to return a hash of data with URIs of posts to
|
|
18
|
+
# be displayed.
|
|
19
|
+
#
|
|
20
|
+
# The response hash should include:
|
|
21
|
+
#
|
|
22
|
+
# - `:posts` *(Array<String, Hash>)* — an array of posts (see below)
|
|
23
|
+
# - `:cursor` *(String)* — optionally, a cursor to be passed when loading the next page
|
|
24
|
+
# - `:req_id` *(String)* — optionally, a request ID that will be passed with "interactions"
|
|
25
|
+
#
|
|
26
|
+
# A post in `:posts` can be either a string with the AT URI of the post, or a hash with fields:
|
|
27
|
+
#
|
|
28
|
+
# - `:post` *(String)* — the AT URI of the post
|
|
29
|
+
# - `:context` *(String)* — optionally, a context string that will be passed with "interactions"
|
|
30
|
+
# - `:reason` *(Hash)* — optionally, a reason why a post appears in the feed:
|
|
31
|
+
# - `{ :repost => repost_uri }` — the post is displayed because someone reposted it (the uri points to a repost record)
|
|
32
|
+
# - `{ :pin => true }` — the post is pinned at the top of the feed
|
|
33
|
+
#
|
|
34
|
+
# @example Simple posts response
|
|
35
|
+
# {
|
|
36
|
+
# posts: [
|
|
37
|
+
# "at://.../app.bsky.feed.post/...",
|
|
38
|
+
# "at://.../app.bsky.feed.post/...",
|
|
39
|
+
# "at://.../app.bsky.feed.post/...",
|
|
40
|
+
# ...
|
|
41
|
+
# ],
|
|
42
|
+
# cursor: "1760639159"
|
|
43
|
+
# }
|
|
44
|
+
#
|
|
45
|
+
# @example More complex response
|
|
46
|
+
# {
|
|
47
|
+
# posts: [
|
|
48
|
+
# {
|
|
49
|
+
# post: "at://.../app.bsky.feed.post/...",
|
|
50
|
+
# reason: { pin: true }
|
|
51
|
+
# },
|
|
52
|
+
# "at://.../app.bsky.feed.post/...",
|
|
53
|
+
# "at://.../app.bsky.feed.post/...",
|
|
54
|
+
# "at://.../app.bsky.feed.post/...",
|
|
55
|
+
# {
|
|
56
|
+
# post: "at://.../app.bsky.feed.post/...",
|
|
57
|
+
# reason: { repost: "at://.../app.bsky.feed.repost/..." },
|
|
58
|
+
# context: 'qweqweqwe'
|
|
59
|
+
# },
|
|
60
|
+
# "at://.../app.bsky.feed.post/...",
|
|
61
|
+
# ...
|
|
62
|
+
# ],
|
|
63
|
+
# cursor: "1760639159",
|
|
64
|
+
# req_id: "req2048"
|
|
65
|
+
# }
|
|
66
|
+
#
|
|
67
|
+
# @example User authorization
|
|
68
|
+
# def get_posts(params, context)
|
|
69
|
+
# if AUTHORIZED_USERS.include?(context.user.raw_did)
|
|
70
|
+
# # ...
|
|
71
|
+
# else
|
|
72
|
+
# raise BlueFactory::AuthorizationError, "You shall not pass!"
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @overload get_posts(params)
|
|
77
|
+
# Use this version if you don't need the context object.
|
|
78
|
+
#
|
|
79
|
+
# @param params [Hash] the received query params
|
|
80
|
+
#
|
|
81
|
+
# @option params [String] :feed
|
|
82
|
+
# the AT URI of the feed record
|
|
83
|
+
# @option params [String, nil] :cursor
|
|
84
|
+
# the cursor returned from the last response (the format is chosen by you)
|
|
85
|
+
# @option params [Integer, nil] :limit
|
|
86
|
+
# requested number of posts (between 1 and 100)
|
|
87
|
+
#
|
|
88
|
+
# @raise [InvalidRequestError] if the request is invalid for some reason (e.g. bad cursor format)
|
|
89
|
+
# @return [Hash] post data
|
|
90
|
+
#
|
|
91
|
+
# @overload get_posts(params, context)
|
|
92
|
+
# Use this version if you want to be passed the context object.
|
|
93
|
+
#
|
|
94
|
+
# @param params [Hash] the received query params
|
|
95
|
+
# @param context [RequestContext] request context
|
|
96
|
+
#
|
|
97
|
+
# @option params [String] :feed
|
|
98
|
+
# the AT URI of the feed record
|
|
99
|
+
# @option params [String, nil] :cursor
|
|
100
|
+
# the cursor returned from the last response (the format is chosen by you)
|
|
101
|
+
# @option params [Integer, nil] :limit
|
|
102
|
+
# requested number of posts (between 1 and 100)
|
|
103
|
+
#
|
|
104
|
+
# @raise [AuthorizationError] if the requesting user is not authorized to see this feed
|
|
105
|
+
# @raise [InvalidRequestError] if the request is invalid for some reason (e.g. bad cursor format)
|
|
106
|
+
# @return [Hash] post data
|
|
107
|
+
|
|
108
|
+
def get_posts(params, context = nil)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns the name of the feed that will be shown to the user in the app.
|
|
112
|
+
#
|
|
113
|
+
# This name is displayed not only in the header and in link cards, but also in the pinned feeds
|
|
114
|
+
# tabs and the right sidebar on the desktop, so it should not be too long, ideally 1-3 words.
|
|
115
|
+
#
|
|
116
|
+
# @return [String]
|
|
117
|
+
|
|
118
|
+
def display_name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# (optional) Longer description of the feed, which explains how the feed works and what kind of
|
|
122
|
+
# content it serves.
|
|
123
|
+
#
|
|
124
|
+
# @return [String, nil]
|
|
125
|
+
|
|
126
|
+
def description
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# (optional) Path of the image file which will be used as the feed's avatar (PNG or JPG).
|
|
130
|
+
#
|
|
131
|
+
# @return [String, nil]
|
|
132
|
+
|
|
133
|
+
def avatar_file
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# (optional) Special feed type - return `:video` for video feeds.
|
|
137
|
+
#
|
|
138
|
+
# @return [String, nil]
|
|
139
|
+
|
|
140
|
+
def content_mode
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# (optional) Return true to opt-in to receiving interactions from users viewing the feed.
|
|
144
|
+
#
|
|
145
|
+
# @see Interaction
|
|
146
|
+
# @return [Boolean]
|
|
147
|
+
|
|
148
|
+
def accepts_interactions
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private_class_method :new
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -1,17 +1,74 @@
|
|
|
1
1
|
module BlueFactory
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Represents a single feed interaction event.
|
|
5
|
+
#
|
|
6
|
+
# Interactions are various kinds of events registered while a user is browsing the feed,
|
|
7
|
+
# either automatically in response to their browsing (e.g. "post seen", "post liked") or as
|
|
8
|
+
# explicit actions taken consciously by the user, with the intention of passing information back
|
|
9
|
+
# to the feed operator (clicking "show more like this" / "show less like this" in the post context
|
|
10
|
+
# menu). Interactions are submitted through the `app.bsky.feed.sendInteractions` endpoint and may
|
|
11
|
+
# be batched.
|
|
12
|
+
#
|
|
13
|
+
# When that endpoint is called, BlueFactory wraps the submitted interaction objects in
|
|
14
|
+
# instances of {Interaction} and passes them to the configured handler block, which is set up
|
|
15
|
+
# through {Interactions#on_interactions} or {Interactions#interactions_handler=} on {BlueFactory}.
|
|
16
|
+
#
|
|
17
|
+
# Note: the {Interaction} objects include info about what interaction has been triggered and
|
|
18
|
+
# on which specific post (post AT URI), but they don't include info in which *feed* it has been
|
|
19
|
+
# triggered. If you have multiple feeds configured and need to know which feed an interaction
|
|
20
|
+
# is from, you need to include either a `context` field (assigned to a specific post) or a
|
|
21
|
+
# `req_id` field (assigned to the whole request) in the data response returned from
|
|
22
|
+
# {FeedHandler#get_posts}.
|
|
23
|
+
#
|
|
24
|
+
|
|
2
25
|
class Interaction
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# Mapping of interaction identifiers in the protocol to short code symbols.
|
|
29
|
+
#
|
|
30
|
+
|
|
3
31
|
EVENTS = {
|
|
32
|
+
# user has liked a post in the feed
|
|
4
33
|
'app.bsky.feed.defs#interactionLike' => :like,
|
|
34
|
+
|
|
35
|
+
# user has quoted a post in the feed
|
|
5
36
|
'app.bsky.feed.defs#interactionQuote' => :quote,
|
|
37
|
+
|
|
38
|
+
# user has replied to a post in the feed
|
|
6
39
|
'app.bsky.feed.defs#interactionReply' => :reply,
|
|
40
|
+
|
|
41
|
+
# user has reposted a post in the feed
|
|
7
42
|
'app.bsky.feed.defs#interactionRepost' => :repost,
|
|
43
|
+
|
|
44
|
+
# user has scrolled down to the given post
|
|
8
45
|
'app.bsky.feed.defs#interactionSeen' => :seen,
|
|
46
|
+
|
|
47
|
+
# user has clicked "show less like this" on a post
|
|
9
48
|
'app.bsky.feed.defs#requestLess' => :request_less,
|
|
49
|
+
|
|
50
|
+
# user has clicked "show more like this" on a post
|
|
10
51
|
'app.bsky.feed.defs#requestMore' => :request_more
|
|
11
52
|
}
|
|
12
53
|
|
|
13
|
-
|
|
54
|
+
# @return [String] the URI of the post
|
|
55
|
+
attr_reader :item
|
|
56
|
+
|
|
57
|
+
# @return [String] the protocol identifier of the interaction type
|
|
58
|
+
attr_reader :event
|
|
59
|
+
|
|
60
|
+
# @return [Symbol, nil] short code symbol of the interaction type
|
|
61
|
+
attr_reader :type
|
|
62
|
+
|
|
63
|
+
# @return [String, nil] optional post context from the original response
|
|
64
|
+
attr_reader :context
|
|
65
|
+
|
|
66
|
+
# @return [String, nil] optional request identifier from the original response
|
|
67
|
+
attr_reader :req_id
|
|
14
68
|
|
|
69
|
+
#
|
|
70
|
+
# @param data [Hash] interaction JSON data
|
|
71
|
+
#
|
|
15
72
|
def initialize(data)
|
|
16
73
|
@item = data['item']
|
|
17
74
|
@event = data['event']
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
module BlueFactory
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# Adds a helper for configuring supported properties to {BlueFactory}.
|
|
7
|
+
# Use these APIs through the main {BlueFactory} module, not directly.
|
|
8
|
+
#
|
|
9
|
+
|
|
2
10
|
module Configurable
|
|
3
11
|
def self.extended(target)
|
|
4
12
|
target.instance_variable_set('@properties', [])
|
|
@@ -10,6 +18,14 @@ module BlueFactory
|
|
|
10
18
|
singleton_class.attr_reader(*properties)
|
|
11
19
|
end
|
|
12
20
|
|
|
21
|
+
#
|
|
22
|
+
# Sets a configurable property to the given value.
|
|
23
|
+
#
|
|
24
|
+
# @api public
|
|
25
|
+
# @param property [String, Symbol] configuration key
|
|
26
|
+
# @param value [Object] value to assign
|
|
27
|
+
# @raise [NoMethodError] if no such property is defined
|
|
28
|
+
#
|
|
13
29
|
def set(property, value)
|
|
14
30
|
if @properties.include?(property.to_sym)
|
|
15
31
|
self.instance_variable_set("@#{property}", value)
|
|
@@ -19,5 +35,6 @@ module BlueFactory
|
|
|
19
35
|
end
|
|
20
36
|
|
|
21
37
|
private :configurable
|
|
38
|
+
private_class_method :extended
|
|
22
39
|
end
|
|
23
40
|
end
|
|
@@ -1,33 +1,92 @@
|
|
|
1
|
+
require_relative '../errors'
|
|
2
|
+
|
|
1
3
|
module BlueFactory
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
#
|
|
8
|
+
# Adds configuration for feeds and provides lookup of configured feeds to {BlueFactory}.
|
|
9
|
+
# Use these APIs through the main {BlueFactory} module, not directly.
|
|
10
|
+
#
|
|
11
|
+
|
|
2
12
|
module Feeds
|
|
3
13
|
def self.extended(target)
|
|
4
14
|
target.instance_variable_set('@feeds', {})
|
|
5
15
|
end
|
|
6
16
|
|
|
7
|
-
|
|
17
|
+
RKEY_REGEXP = /\A[A-Za-z0-9\.\-_:~]+\z/
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# Registers a feed handler for a given rkey. The full AT URI of the feed generator, which is
|
|
21
|
+
# listed in `describeFeedGenerator` and expected in the `feed` parameter to `getFeedSkeleton`
|
|
22
|
+
# will be: `"at://#{publisher_did}/app.bsky.feed.generator/#{key}"`. The key should be a string
|
|
23
|
+
# no longer than 15 characters and should not contain slashes, preferably only lowercase letters,
|
|
24
|
+
# digits and hyphens.
|
|
25
|
+
#
|
|
26
|
+
# The feed handler is expected to be an object which has a `#get_posts` method which accepts
|
|
27
|
+
# requests routed from the BlueFactory server and returns post data in a specified format.
|
|
28
|
+
# See the abstract {FeedHandler} class for a description of the expected API.
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
# @param key [String] the feed rkey
|
|
32
|
+
# @param feed_handler [#get_posts] feed implementation object
|
|
33
|
+
# @raise [ConfigurationError] if the key has invalid format
|
|
34
|
+
#
|
|
35
|
+
def add_feed(key, feed_handler)
|
|
8
36
|
validate_key(key)
|
|
9
|
-
|
|
37
|
+
validate_feed(key, feed_handler)
|
|
38
|
+
|
|
39
|
+
@feeds[key.to_s] = feed_handler
|
|
10
40
|
end
|
|
11
41
|
|
|
42
|
+
#
|
|
43
|
+
# Lists all configured feed keys.
|
|
44
|
+
#
|
|
45
|
+
# @api public
|
|
46
|
+
# @return [Array<String>]
|
|
47
|
+
#
|
|
12
48
|
def feed_keys
|
|
13
49
|
@feeds.keys
|
|
14
50
|
end
|
|
15
51
|
|
|
52
|
+
#
|
|
53
|
+
# Returns a feed handler configured for a given rkey.
|
|
54
|
+
#
|
|
55
|
+
# @api public
|
|
56
|
+
# @param key [String] feed key
|
|
57
|
+
# @return [#get_posts, nil] feed object, if registered
|
|
58
|
+
#
|
|
16
59
|
def get_feed(key)
|
|
17
60
|
@feeds[key.to_s]
|
|
18
61
|
end
|
|
19
62
|
|
|
63
|
+
#
|
|
64
|
+
# Returns an array of all registered feed handler objects.
|
|
65
|
+
#
|
|
66
|
+
# @api public
|
|
67
|
+
# @return [Array<#get_posts>]
|
|
68
|
+
#
|
|
20
69
|
def all_feeds
|
|
21
70
|
@feeds.values
|
|
22
71
|
end
|
|
23
72
|
|
|
73
|
+
|
|
24
74
|
private
|
|
25
75
|
|
|
26
76
|
def validate_key(key)
|
|
27
|
-
raise
|
|
28
|
-
raise
|
|
29
|
-
raise
|
|
30
|
-
raise InvalidKeyError, "Key must not be longer than 15 characters" if key.length > 15
|
|
77
|
+
raise ConfigurationError, "Key must be a string (got: #{key.inspect})" unless key.is_a?(String)
|
|
78
|
+
raise ConfigurationError, "Key must not be empty" if key == ''
|
|
79
|
+
raise ConfigurationError, "Key #{key.inspect} contains invalid characters" if key !~ RKEY_REGEXP
|
|
31
80
|
end
|
|
81
|
+
|
|
82
|
+
def validate_feed(key, feed)
|
|
83
|
+
get_posts = feed.method(:get_posts) rescue nil
|
|
84
|
+
|
|
85
|
+
if get_posts.nil?
|
|
86
|
+
raise InvalidFeedClassError, "The feed object for '#{key}' is missing required method `get_posts`"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :extended
|
|
32
91
|
end
|
|
33
92
|
end
|
|
@@ -1,9 +1,47 @@
|
|
|
1
1
|
module BlueFactory
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# Adds configuration for an interactions handler to {BlueFactory}.
|
|
7
|
+
# Use these APIs through the main {BlueFactory} module, not directly.
|
|
8
|
+
#
|
|
9
|
+
|
|
2
10
|
module Interactions
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# Returns the currently configured feed interactions handler.
|
|
14
|
+
#
|
|
15
|
+
# @api public
|
|
16
|
+
# @see Interaction
|
|
17
|
+
# @yieldparam interactions [Array<Interaction>] one or more received interactions
|
|
18
|
+
# @yieldparam context [RequestContext] HTTP request context, including e.g. user auth info
|
|
19
|
+
#
|
|
3
20
|
def on_interactions(&block)
|
|
4
21
|
@interactions_handler = block
|
|
5
22
|
end
|
|
6
23
|
|
|
7
|
-
|
|
24
|
+
#
|
|
25
|
+
# Returns the currently configured feed interactions handler.
|
|
26
|
+
#
|
|
27
|
+
# @api public
|
|
28
|
+
# @see Interaction
|
|
29
|
+
# @see #on_interactions
|
|
30
|
+
# @return [Proc, nil]
|
|
31
|
+
#
|
|
32
|
+
def interactions_handler
|
|
33
|
+
@interactions_handler
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# Registers a callback for incoming feed interactions.
|
|
38
|
+
#
|
|
39
|
+
# @api public
|
|
40
|
+
# @see Interaction
|
|
41
|
+
# @see #on_interactions
|
|
42
|
+
#
|
|
43
|
+
def interactions_handler=(block)
|
|
44
|
+
@interactions_handler = block
|
|
45
|
+
end
|
|
8
46
|
end
|
|
9
47
|
end
|
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
require_relative 'user_info'
|
|
2
2
|
|
|
3
3
|
module BlueFactory
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# An object which provides some metadata about the `getFeedSkeleton` request being processed.
|
|
7
|
+
# The context is passed to the {FeedHandler#get_posts} method of the provided feed handler object,
|
|
8
|
+
# if the method is made to accept two parameters.
|
|
9
|
+
#
|
|
10
|
+
|
|
4
11
|
class RequestContext
|
|
5
|
-
attr_accessor :request
|
|
6
12
|
|
|
13
|
+
# Returns the underlying request object.
|
|
14
|
+
#
|
|
15
|
+
# @return [Sinatra::Request]
|
|
16
|
+
# see {https://www.rubydoc.info/gems/sinatra/Sinatra/Request Sinatra::Request}
|
|
17
|
+
#
|
|
18
|
+
attr_reader :request
|
|
19
|
+
|
|
20
|
+
# @param request
|
|
21
|
+
# ({https://www.rubydoc.info/gems/sinatra/Sinatra/Request Sinatra::Request})
|
|
22
|
+
# — the original Sinatra/Rack request object
|
|
23
|
+
#
|
|
7
24
|
def initialize(request)
|
|
8
25
|
@request = request
|
|
9
26
|
end
|
|
10
27
|
|
|
28
|
+
#
|
|
29
|
+
# The request environment hash.
|
|
30
|
+
# @return [Hash] Rack environment
|
|
31
|
+
#
|
|
11
32
|
def env
|
|
12
33
|
@request.env
|
|
13
34
|
end
|
|
14
35
|
|
|
36
|
+
#
|
|
37
|
+
# Parsed user information derived from the authorization header.
|
|
38
|
+
# @return [UserInfo]
|
|
39
|
+
#
|
|
15
40
|
def user
|
|
16
41
|
UserInfo.new(env['HTTP_AUTHORIZATION'])
|
|
17
42
|
end
|
|
18
43
|
|
|
44
|
+
#
|
|
45
|
+
# Indicates if an authorization header was provided at all or not.
|
|
46
|
+
# @return [Boolean] true when the authorization header is non-empty
|
|
47
|
+
#
|
|
19
48
|
def has_auth?
|
|
20
49
|
env['HTTP_AUTHORIZATION'] != nil
|
|
21
50
|
end
|
data/lib/blue_factory/server.rb
CHANGED
|
@@ -8,7 +8,36 @@ require_relative 'output_generator'
|
|
|
8
8
|
require_relative 'request_context'
|
|
9
9
|
|
|
10
10
|
module BlueFactory
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# Sinatra server implementing the required feed generator endpoints. Usually you will only
|
|
14
|
+
# interact with it in order to start the server in a startup script.
|
|
15
|
+
#
|
|
16
|
+
# @example Starting locally during development
|
|
17
|
+
# BlueFactory::Server.run!
|
|
18
|
+
#
|
|
19
|
+
# @example Starting from Rack config `config.ru` in production
|
|
20
|
+
# run BlueFactory::Server
|
|
21
|
+
#
|
|
22
|
+
|
|
11
23
|
class Server < Sinatra::Base
|
|
24
|
+
|
|
25
|
+
# @private
|
|
26
|
+
@@config_checked = false
|
|
27
|
+
|
|
28
|
+
# Run the Sinatra app as a self-hosted server.
|
|
29
|
+
# See {https://www.rubydoc.info/gems/sinatra/Sinatra/Base#run!-class_method Sinatra::Base.run!}.
|
|
30
|
+
# @raise [ConfigurationError] if the {BlueFactory.hostname} or {BlueFactory.publisher_did} is not set
|
|
31
|
+
|
|
32
|
+
def self.run!(options = {}, &block)
|
|
33
|
+
verify_config
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
before do
|
|
38
|
+
Server.send(:verify_config) unless @@config_checked
|
|
39
|
+
end
|
|
40
|
+
|
|
12
41
|
configure do
|
|
13
42
|
disable :static
|
|
14
43
|
enable :quiet
|
|
@@ -17,6 +46,10 @@ module BlueFactory
|
|
|
17
46
|
end
|
|
18
47
|
|
|
19
48
|
helpers do
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
#
|
|
52
|
+
|
|
20
53
|
def config
|
|
21
54
|
BlueFactory
|
|
22
55
|
end
|
|
@@ -43,7 +76,7 @@ module BlueFactory
|
|
|
43
76
|
end
|
|
44
77
|
|
|
45
78
|
if feed_uri !~ %r(^at://[\w\-\.\:]+/[\w\.]+/[\w\.\-]+$)
|
|
46
|
-
raise InvalidRequestError, "Error: feed must be a valid
|
|
79
|
+
raise InvalidRequestError, "Error: feed must be a valid AT URI"
|
|
47
80
|
end
|
|
48
81
|
|
|
49
82
|
feed_key = feed_uri.split('/').last
|
|
@@ -55,20 +88,26 @@ module BlueFactory
|
|
|
55
88
|
|
|
56
89
|
feed
|
|
57
90
|
end
|
|
91
|
+
|
|
92
|
+
def validate_limit(args)
|
|
93
|
+
if args[:limit]
|
|
94
|
+
args[:limit] = args[:limit].to_i.clamp(1, MAX_LIMIT)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
58
97
|
end
|
|
59
98
|
|
|
60
99
|
get '/xrpc/app.bsky.feed.getFeedSkeleton' do
|
|
61
100
|
begin
|
|
62
101
|
feed = get_feed(params[:feed])
|
|
63
|
-
get_posts = feed.method(:get_posts)
|
|
64
102
|
args = params.slice(:feed, :cursor, :limit)
|
|
103
|
+
validate_limit(args)
|
|
65
104
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
105
|
+
get_posts = feed.method(:get_posts)
|
|
106
|
+
|
|
107
|
+
case get_posts.arity
|
|
108
|
+
when 1
|
|
70
109
|
response = feed.get_posts(args)
|
|
71
|
-
|
|
110
|
+
when 2
|
|
72
111
|
context = RequestContext.new(request)
|
|
73
112
|
response = feed.get_posts(args, context)
|
|
74
113
|
else
|
|
@@ -78,17 +117,17 @@ module BlueFactory
|
|
|
78
117
|
json = OutputGenerator.new.generate(response)
|
|
79
118
|
return json_response(json)
|
|
80
119
|
rescue InvalidRequestError => e
|
|
81
|
-
return json_error(e.error_type
|
|
120
|
+
return json_error(e.error_type, e.message)
|
|
82
121
|
rescue AuthorizationError => e
|
|
83
|
-
return json_error(e.error_type
|
|
122
|
+
return json_error(e.error_type, e.message, status: 401)
|
|
84
123
|
rescue UnsupportedAlgorithmError => e
|
|
85
124
|
return json_error("UnsupportedAlgorithm", e.message)
|
|
86
125
|
rescue InvalidResponseError => e
|
|
87
|
-
if settings.
|
|
88
|
-
return json_error("InvalidResponse", e.message, status: 500)
|
|
89
|
-
else
|
|
126
|
+
if settings.production?
|
|
90
127
|
request.logger&.<< "#{e.class}: #{e.message}\n"
|
|
91
128
|
return json_error("InvalidResponse", "Feed response was invalid", status: 500)
|
|
129
|
+
else
|
|
130
|
+
return json_error("InvalidResponse", e.message, status: 500)
|
|
92
131
|
end
|
|
93
132
|
end
|
|
94
133
|
end
|
|
@@ -126,5 +165,23 @@ module BlueFactory
|
|
|
126
165
|
json_error('MethodNotImplemented', 'Method Not Implemented', status: 501)
|
|
127
166
|
end
|
|
128
167
|
end
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def self.verify_config
|
|
173
|
+
if BlueFactory.hostname.nil?
|
|
174
|
+
raise ConfigurationError, "The `hostname` property is not set. Set it with: BlueFactory.set(:hostname, 'example.com')"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if BlueFactory.publisher_did.nil?
|
|
178
|
+
raise ConfigurationError,
|
|
179
|
+
"The `publisher_did` property is not set. Set it with: BlueFactory.set(:publisher_did, 'did:plc:qweqweqwe')"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@@config_checked = true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private_class_method :verify_config
|
|
129
186
|
end
|
|
130
187
|
end
|
|
@@ -87,10 +87,19 @@ namespace :bluesky do
|
|
|
87
87
|
password = STDIN.noecho(&:gets).chomp
|
|
88
88
|
puts
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
begin
|
|
91
|
+
json = BlueFactory::Net.post_request(pds_host, 'com.atproto.server.createSession', {
|
|
92
|
+
identifier: publisher_did,
|
|
93
|
+
password: password
|
|
94
|
+
})
|
|
95
|
+
rescue BlueFactory::Net::ResponseError => e
|
|
96
|
+
if e.response.code.to_i == 401 && e.response.body.include?('"error":"AuthFactorTokenRequired"')
|
|
97
|
+
puts "Error: this Rake task doesn't support logging in with 2FA. Please use an app password here if you have 2FA enabled."
|
|
98
|
+
exit 1
|
|
99
|
+
else
|
|
100
|
+
raise
|
|
101
|
+
end
|
|
102
|
+
end
|
|
94
103
|
|
|
95
104
|
access_token = json['accessJwt']
|
|
96
105
|
|
|
@@ -104,15 +113,15 @@ namespace :bluesky do
|
|
|
104
113
|
record = {
|
|
105
114
|
did: BlueFactory.service_did,
|
|
106
115
|
displayName: feed_display_name,
|
|
107
|
-
description: feed_description,
|
|
108
116
|
createdAt: Time.now.iso8601,
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
record[:avatar] = avatar_ref if avatar_ref
|
|
120
|
+
record[:description] = feed_description if feed_description
|
|
112
121
|
record[:contentMode] = feed_content_mode if feed_content_mode
|
|
113
122
|
record[:acceptsInteractions] = true if feed.respond_to?(:accepts_interactions) && feed.accepts_interactions
|
|
114
123
|
|
|
115
|
-
|
|
124
|
+
BlueFactory::Net.post_request(pds_host, 'com.atproto.repo.putRecord', {
|
|
116
125
|
repo: publisher_did,
|
|
117
126
|
collection: BlueFactory::FEED_GENERATOR_TYPE,
|
|
118
127
|
rkey: feed_key,
|
|
@@ -3,8 +3,25 @@ require 'net/http'
|
|
|
3
3
|
require 'uri'
|
|
4
4
|
|
|
5
5
|
module BlueFactory
|
|
6
|
+
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
# HTTP request helpers.
|
|
10
|
+
#
|
|
11
|
+
|
|
6
12
|
module Net
|
|
7
|
-
|
|
13
|
+
|
|
14
|
+
# @api private
|
|
15
|
+
# Thrown when a HTTP response is not 200 OK.
|
|
16
|
+
#
|
|
17
|
+
class ResponseError < StandardError
|
|
18
|
+
attr_reader :response
|
|
19
|
+
|
|
20
|
+
def initialize(response, message)
|
|
21
|
+
@response = response
|
|
22
|
+
super(message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
8
25
|
|
|
9
26
|
def self.get_request(server, method = nil, params = nil, auth: nil)
|
|
10
27
|
headers = {}
|
|
@@ -17,7 +34,7 @@ module BlueFactory
|
|
|
17
34
|
end
|
|
18
35
|
|
|
19
36
|
response = ::Net::HTTP.get_response(url, headers)
|
|
20
|
-
raise ResponseError, "Invalid response: #{response.code} #{response.body}" if response.code.to_i / 100 != 2
|
|
37
|
+
raise ResponseError.new(response, "Invalid response: #{response.code} #{response.body}") if response.code.to_i / 100 != 2
|
|
21
38
|
|
|
22
39
|
JSON.parse(response.body)
|
|
23
40
|
end
|
|
@@ -30,7 +47,7 @@ module BlueFactory
|
|
|
30
47
|
body = data.is_a?(String) ? data : data.to_json
|
|
31
48
|
|
|
32
49
|
response = ::Net::HTTP.post(URI("#{server}/xrpc/#{method}"), body, headers)
|
|
33
|
-
raise ResponseError, "Invalid response: #{response.code} #{response.body}" if response.code.to_i / 100 != 2
|
|
50
|
+
raise ResponseError.new(response, "Invalid response: #{response.code} #{response.body}") if response.code.to_i / 100 != 2
|
|
34
51
|
|
|
35
52
|
JSON.parse(response.body)
|
|
36
53
|
end
|
|
@@ -4,11 +4,27 @@ require 'json'
|
|
|
4
4
|
require_relative 'errors'
|
|
5
5
|
|
|
6
6
|
module BlueFactory
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# An object which provides info about the user making the request, based on the
|
|
10
|
+
# included authorization header (if any). Accessed through {RequestContext#user}.
|
|
11
|
+
#
|
|
12
|
+
|
|
7
13
|
class UserInfo
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# @param auth_header [String, nil] value of the "Authorization" HTTP header
|
|
17
|
+
#
|
|
8
18
|
def initialize(auth_header)
|
|
9
19
|
@auth = auth_header
|
|
10
20
|
end
|
|
11
21
|
|
|
22
|
+
#
|
|
23
|
+
# The bearer token extracted from the authorization header.
|
|
24
|
+
#
|
|
25
|
+
# @return [String, nil]
|
|
26
|
+
# @raise [AuthorizationError] if the header is not a "Bearer" token
|
|
27
|
+
#
|
|
12
28
|
def token
|
|
13
29
|
@token ||= begin
|
|
14
30
|
if @auth.nil? || @auth.strip.empty?
|
|
@@ -21,6 +37,17 @@ module BlueFactory
|
|
|
21
37
|
end
|
|
22
38
|
end
|
|
23
39
|
|
|
40
|
+
#
|
|
41
|
+
# Returns the user's (unverified) DID decoded from the JWT payload of the bearer token.
|
|
42
|
+
#
|
|
43
|
+
# Important: this method does not verify the signature of the token, which means
|
|
44
|
+
# the token can be fairly easily forged to impersonate another user, and this method
|
|
45
|
+
# would not detect that. Do not rely on it for use cases where it's important to be
|
|
46
|
+
# certain of the requesting user's identity.
|
|
47
|
+
#
|
|
48
|
+
# @return [String, nil] user DID decoded from the token
|
|
49
|
+
# @raise [AuthorizationError] when the token format is invalid
|
|
50
|
+
#
|
|
24
51
|
def raw_did
|
|
25
52
|
return nil if token.nil?
|
|
26
53
|
|
|
@@ -29,10 +56,15 @@ module BlueFactory
|
|
|
29
56
|
|
|
30
57
|
begin
|
|
31
58
|
payload = JSON.parse(Base64.decode64(parts[1]))
|
|
32
|
-
payload['iss']
|
|
33
59
|
rescue StandardError => e
|
|
34
60
|
raise AuthorizationError.new("Invalid JWT format", "BadJwt")
|
|
35
61
|
end
|
|
62
|
+
|
|
63
|
+
if did = payload['iss']
|
|
64
|
+
did
|
|
65
|
+
else
|
|
66
|
+
raise AuthorizationError.new("Invalid JWT format", "BadJwt")
|
|
67
|
+
end
|
|
36
68
|
end
|
|
37
69
|
end
|
|
38
70
|
end
|
data/lib/blue_factory/version.rb
CHANGED
data/lib/blue_factory.rb
CHANGED
|
@@ -2,6 +2,36 @@ require_relative 'blue_factory/configuration'
|
|
|
2
2
|
require_relative 'blue_factory/server'
|
|
3
3
|
require_relative 'blue_factory/version'
|
|
4
4
|
|
|
5
|
+
#
|
|
6
|
+
# This is the main module of the library, through which you configure the feed service.
|
|
7
|
+
#
|
|
8
|
+
# @example Configuring the server and feeds
|
|
9
|
+
# require 'blue_factory'
|
|
10
|
+
#
|
|
11
|
+
# BlueFactory.set :publisher_did, 'did:plc:qwertyuiopasdf'
|
|
12
|
+
# BlueFactory.set :hostname, 'feeds.example.com'
|
|
13
|
+
#
|
|
14
|
+
# BlueFactory.add_feed 'photos', PhotographyFeed.new
|
|
15
|
+
#
|
|
16
|
+
# @example Handling interactions
|
|
17
|
+
# BlueFactory.on_interactions do |interactions, context|
|
|
18
|
+
# interactions.each do |i|
|
|
19
|
+
# unless i.type == :seen
|
|
20
|
+
# puts "[#{Time.now}] #{context.user.raw_did}: #{i.type} #{i.item}"
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
|
|
5
26
|
module BlueFactory
|
|
27
|
+
|
|
28
|
+
# The collection NSID of a Bluesky feed generator service.
|
|
6
29
|
FEED_GENERATOR_TYPE = 'app.bsky.feed.generator'
|
|
30
|
+
|
|
31
|
+
# Maximum allowed value for the limit parameter in `getFeedSkeleton`.
|
|
32
|
+
MAX_LIMIT = 100
|
|
33
|
+
|
|
34
|
+
# Default value for the limit parameter in `getFeedSkeleton`. This value isn't used by the library
|
|
35
|
+
# (if no limit parameter is passed, it isn't added), but you can use it as a fallback in your code.
|
|
36
|
+
DEFAULT_LIMIT = 50
|
|
7
37
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: blue_factory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kuba Suder
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: sinatra
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -46,6 +60,7 @@ files:
|
|
|
46
60
|
- lib/blue_factory.rb
|
|
47
61
|
- lib/blue_factory/configuration.rb
|
|
48
62
|
- lib/blue_factory/errors.rb
|
|
63
|
+
- lib/blue_factory/feed_handler.rb
|
|
49
64
|
- lib/blue_factory/interaction.rb
|
|
50
65
|
- lib/blue_factory/modules/configurable.rb
|
|
51
66
|
- lib/blue_factory/modules/feeds.rb
|
|
@@ -59,13 +74,13 @@ files:
|
|
|
59
74
|
- lib/blue_factory/user_info.rb
|
|
60
75
|
- lib/blue_factory/version.rb
|
|
61
76
|
- sig/blue_factory.rbs
|
|
62
|
-
homepage: https://
|
|
77
|
+
homepage: https://ruby.sdk.blue
|
|
63
78
|
licenses:
|
|
64
79
|
- Zlib
|
|
65
80
|
metadata:
|
|
66
|
-
bug_tracker_uri: https://
|
|
67
|
-
changelog_uri: https://
|
|
68
|
-
source_code_uri: https://
|
|
81
|
+
bug_tracker_uri: https://tangled.org/mackuba.eu/blue_factory/issues
|
|
82
|
+
changelog_uri: https://tangled.org/mackuba.eu/blue_factory/blob/master/CHANGELOG.md
|
|
83
|
+
source_code_uri: https://tangled.org/mackuba.eu/blue_factory
|
|
69
84
|
rdoc_options: []
|
|
70
85
|
require_paths:
|
|
71
86
|
- lib
|
|
@@ -80,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
80
95
|
- !ruby/object:Gem::Version
|
|
81
96
|
version: '0'
|
|
82
97
|
requirements: []
|
|
83
|
-
rubygems_version:
|
|
98
|
+
rubygems_version: 4.0.3
|
|
84
99
|
specification_version: 4
|
|
85
100
|
summary: A Ruby gem for hosting custom feeds for Bluesky
|
|
86
101
|
test_files: []
|