blue_factory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 80b991e6702487fe63c56386b86d181093e88abbe3bc3c30360a4fca7977350f
4
+ data.tar.gz: d4fe4bfd4a4cb2dea7dd165eb6dbff45bd3e55eb72144b82669dd5bab212b1d8
5
+ SHA512:
6
+ metadata.gz: 3c591080e8fb94fb8ecbc8db0178b08a4d98b018ece55975ab672dd81ea05d5b7aaa18aebedd3b1514d7e134b2d77794e98026a9bcfcc06d73fe71bf67d74df0
7
+ data.tar.gz: 3c2dcb624de1429e6eee265727c5a2368ce4cbb999e74d07016ae485b568c34cfdea39c9cb5640d32730f597e548bac48255c7e04a8cf8359d360e9db4d88ef4
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ ## [Unreleased]
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The zlib License
2
+
3
+ Copyright (c) 2023 Jakub Suder
4
+
5
+ This software is provided 'as-is', without any express or implied
6
+ warranty. In no event will the authors be held liable for any damages
7
+ arising from the use of this software.
8
+
9
+ Permission is granted to anyone to use this software for any purpose,
10
+ including commercial applications, and to alter it and redistribute it
11
+ freely, subject to the following restrictions:
12
+
13
+ 1. The origin of this software must not be misrepresented; you must not
14
+ claim that you wrote the original software. If you use this software
15
+ in a product, an acknowledgment in the product documentation would be
16
+ appreciated but is not required.
17
+
18
+ 2. Altered source versions must be plainly marked as such, and must not be
19
+ misrepresented as being the original software.
20
+
21
+ 3. This notice may not be removed or altered from any source distribution.
22
+
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # BlueFactory
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/blue_factory`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/blue_factory.
@@ -0,0 +1,19 @@
1
+ require_relative 'modules/configurable'
2
+ require_relative 'modules/feeds'
3
+
4
+ module BlueFactory
5
+ extend Configurable
6
+ extend Feeds
7
+
8
+ def self.service_did
9
+ 'did:web:' + hostname
10
+ end
11
+
12
+ def self.environment
13
+ (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
14
+ end
15
+
16
+ configurable :publisher_did, :hostname, :validate_responses
17
+
18
+ set :validate_responses, (environment != :production)
19
+ end
@@ -0,0 +1,13 @@
1
+ module BlueFactory
2
+ class InvalidRequestError < StandardError
3
+ attr_reader :error_type
4
+
5
+ def initialize(message, error_type = nil)
6
+ super(message)
7
+ @error_type = error_type
8
+ end
9
+ end
10
+
11
+ class InvalidResponseError < StandardError
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module BlueFactory
2
+ module Configurable
3
+ def self.extended(target)
4
+ target.instance_variable_set('@properties', [])
5
+ end
6
+
7
+ def configurable(*properties)
8
+ @properties ||= []
9
+ @properties += properties.map(&:to_sym)
10
+ singleton_class.attr_reader(*properties)
11
+ end
12
+
13
+ def set(property, value)
14
+ if @properties.include?(property.to_sym)
15
+ self.instance_variable_set("@#{property}", value)
16
+ else
17
+ raise NoMethodError
18
+ end
19
+ end
20
+
21
+ private :configurable
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module BlueFactory
2
+ module Feeds
3
+ def self.extended(target)
4
+ target.instance_variable_set('@feeds', {})
5
+ end
6
+
7
+ def add_feed(key, feed_class)
8
+ @feeds[key.to_s] = feed_class
9
+ end
10
+
11
+ def all_feeds
12
+ @feeds.keys
13
+ end
14
+
15
+ def get_feed(key)
16
+ @feeds[key.to_s]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ load 'blue_factory/tasks/publish.rake'
@@ -0,0 +1,104 @@
1
+ require 'json'
2
+ require 'sinatra/base'
3
+
4
+ require_relative 'configuration'
5
+ require_relative 'errors'
6
+
7
+ module BlueFactory
8
+ class Server < Sinatra::Base
9
+ AT_URI_REGEXP = %r(^at://did:plc:[a-z0-9]+/app\.bsky\.feed\.post/[a-z0-9]+$)
10
+
11
+ configure do
12
+ disable :static
13
+ enable :quiet
14
+ enable :logging
15
+ set :default_content_type, 'application/json'
16
+ settings.add_charset << 'application/json'
17
+ end
18
+
19
+ helpers do
20
+ def config
21
+ BlueFactory
22
+ end
23
+
24
+ def feed_uri(key)
25
+ 'at://' + config.publisher_did + '/' + FEED_GENERATOR_TYPE + '/' + key
26
+ end
27
+
28
+ def json(data)
29
+ JSON.generate(data)
30
+ end
31
+
32
+ def json_error(name, message, status: 400)
33
+ [status, JSON.generate({ error: name, message: message })]
34
+ end
35
+
36
+ def validate_response(response)
37
+ cursor = response[:cursor]
38
+ raise InvalidResponseError, ":cursor key is missing" unless response.has_key?(:cursor)
39
+ raise InvalidResponseError, ":cursor should be a string or nil" unless cursor.nil? || cursor.is_a?(String)
40
+
41
+ posts = response[:posts]
42
+ raise InvalidResponseError, ":posts key is missing" unless response.has_key?(:posts)
43
+ raise InvalidResponseError, ":posts should be an array of strings" unless posts.is_a?(Array)
44
+ raise InvalidResponseError, ":posts should be an array of strings" unless posts.all? { |x| x.is_a?(String) }
45
+
46
+ if bad_uri = posts.detect { |x| x !~ AT_URI_REGEXP }
47
+ raise InvalidResponseError, "Invalid post URI: #{bad_uri}"
48
+ end
49
+ end
50
+ end
51
+
52
+ get '/xrpc/app.bsky.feed.getFeedSkeleton' do
53
+ if params[:feed].to_s.empty?
54
+ return json_error("InvalidRequest", "Error: Params must have the property \"feed\"")
55
+ end
56
+
57
+ if params[:feed] !~ %r(^at://[\w\-\.\:]+/[\w\.]+/[\w\.\-]+$)
58
+ return json_error("InvalidRequest", "Error: feed must be a valid at-uri")
59
+ end
60
+
61
+ feed_key = params[:feed].split('/').last
62
+ feed = config.get_feed(feed_key)
63
+
64
+ if feed.nil? || feed_uri(feed_key) != params[:feed]
65
+ return json_error("UnsupportedAlgorithm", "Unsupported algorithm")
66
+ end
67
+
68
+ begin
69
+ response = feed.get_posts(params.slice(:feed, :cursor, :limit))
70
+ validate_response(response) if config.validate_responses
71
+
72
+ return json({
73
+ cursor: response[:cursor],
74
+ feed: response[:posts].map { |s| { post: s }}
75
+ })
76
+ rescue InvalidRequestError => e
77
+ return json_error(e.error_type || "InvalidRequest", e.message)
78
+ rescue InvalidResponseError => e
79
+ return json_error("InvalidResponse", e.message)
80
+ end
81
+ end
82
+
83
+ get '/xrpc/app.bsky.feed.describeFeedGenerator' do
84
+ return json({
85
+ did: config.service_did,
86
+ feeds: config.all_feeds.map { |f| { uri: feed_uri(f) }}
87
+ })
88
+ end
89
+
90
+ get '/.well-known/did.json' do
91
+ return json({
92
+ '@context': ['https://www.w3.org/ns/did/v1'],
93
+ id: config.service_did,
94
+ service: [
95
+ {
96
+ id: '#bsky_fg',
97
+ type: 'BskyFeedGenerator',
98
+ serviceEndpoint: 'https://' + config.hostname
99
+ }
100
+ ]
101
+ })
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,91 @@
1
+ require 'blue_factory/configuration'
2
+ require 'blue_factory/tasks/support'
3
+ require 'io/console'
4
+
5
+ namespace :bluesky do
6
+ desc "Publish a feed"
7
+ task :publish do
8
+ if ENV['KEY'].to_s == ''
9
+ puts "Please specify feed key as KEY=feedname (the part of the feed's at:// URI after the last slash)"
10
+ exit 1
11
+ end
12
+
13
+ feed_key = ENV['KEY']
14
+
15
+ if BlueFactory.hostname.nil?
16
+ puts "Missing server hostname: please set the hostname with `BlueFactory.set :hostname, 'example.com'`"
17
+ exit 1
18
+ end
19
+
20
+ if BlueFactory.publisher_did.nil?
21
+ puts "Missing publisher DID: please set it with `BlueFactory.set :publisher_did, 'did:plc:youridentifier'`"
22
+ exit 1
23
+ end
24
+
25
+ feed = BlueFactory.get_feed(feed_key)
26
+
27
+ if feed.nil?
28
+ puts "No feed configured for key '#{feed_key}' - use `BlueFactory.add_feed '#{feed_key}', MyFeed.new`"
29
+ exit 1
30
+ end
31
+
32
+ if feed.respond_to?(:display_name) && feed.display_name.to_s.strip != ''
33
+ feed_display_name = feed.display_name
34
+ else
35
+ puts "The feed has no display name - implement a #display_name method."
36
+ exit 1
37
+ end
38
+
39
+ if feed.respond_to?(:description) && feed.description.to_s.strip != ''
40
+ feed_description = feed.description
41
+ end
42
+
43
+ if feed.respond_to?(:avatar_file) && feed.avatar_file.to_s.strip != ''
44
+ avatar_file = feed.avatar_file
45
+
46
+ if !File.exist?
47
+ puts "Avatar file #{avatar_file} not found."
48
+ exit 1
49
+ end
50
+
51
+ encoding = case avatar_file
52
+ when /\.png$/ then 'image/png'
53
+ when /\.jpe?g$/ then 'image/jpeg'
54
+ else
55
+ puts "The avatar must be either a PNG or a JPEG file."
56
+ exit 1
57
+ end
58
+
59
+ avatar_data = File.read(avatar_file)
60
+ end
61
+
62
+ server = ENV['SERVER_URL'] || "https://bsky.social"
63
+
64
+ print "Enter password for your publisher account (#{BlueFactory.publisher_did}): "
65
+ password = STDIN.noecho(&:gets).chomp
66
+ puts
67
+
68
+ json = BlueFactory::Net.post_request(server, 'com.atproto.server.createSession', {
69
+ identifier: BlueFactory.publisher_did,
70
+ password: password
71
+ })
72
+
73
+ access_token = json['accessJwt']
74
+
75
+ record = {
76
+ did: BlueFactory.service_did,
77
+ displayName: feed_display_name,
78
+ description: feed_description,
79
+ createdAt: Time.now.iso8601,
80
+ }
81
+
82
+ json = BlueFactory::Net.post_request(server, 'com.atproto.repo.putRecord', {
83
+ repo: BlueFactory.publisher_did,
84
+ collection: BlueFactory::FEED_GENERATOR_TYPE,
85
+ rkey: feed_key,
86
+ record: record
87
+ }, auth: access_token)
88
+
89
+ p json
90
+ end
91
+ end
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ module BlueFactory
5
+ module Net
6
+ class ResponseError < StandardError; end
7
+
8
+ def self.post_request(server, method, data, auth: nil, content_type: "application/json")
9
+ headers = {}
10
+ headers['Content-Type'] = content_type
11
+ headers['Authorization'] = "Bearer #{auth}" if auth
12
+
13
+ body = data.is_a?(String) ? data : data.to_json
14
+
15
+ puts body unless data.is_a?(String)
16
+
17
+ response = ::Net::HTTP.post(URI("#{server}/xrpc/#{method}"), body, headers)
18
+ raise ResponseError, "Invalid response: #{response.code} #{response.body}" if response.code.to_i / 100 != 2
19
+
20
+ JSON.parse(response.body)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module BlueFactory
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'blue_factory/configuration'
2
+ require_relative 'blue_factory/server'
3
+ require_relative 'blue_factory/version'
4
+
5
+ module BlueFactory
6
+ FEED_GENERATOR_TYPE = 'app.bsky.feed.generator'
7
+ end
@@ -0,0 +1,4 @@
1
+ module BlueFactory
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blue_factory
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kuba Suder
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description: Write a longer description or delete this line.
28
+ email:
29
+ - jakub.suder@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE.txt
36
+ - README.md
37
+ - lib/blue_factory.rb
38
+ - lib/blue_factory/configuration.rb
39
+ - lib/blue_factory/errors.rb
40
+ - lib/blue_factory/modules/configurable.rb
41
+ - lib/blue_factory/modules/feeds.rb
42
+ - lib/blue_factory/rake.rb
43
+ - lib/blue_factory/server.rb
44
+ - lib/blue_factory/tasks/publish.rake
45
+ - lib/blue_factory/tasks/support.rb
46
+ - lib/blue_factory/version.rb
47
+ - sig/blue_factory.rbs
48
+ homepage: https://github.com/mackuba/blue_factory
49
+ licenses:
50
+ - Zlib
51
+ metadata:
52
+ bug_tracker_uri: https://github.com/mackuba/blue_factory/issues
53
+ changelog_uri: https://github.com/mackuba/blue_factory/blob/master/CHANGELOG.md
54
+ source_code_uri: https://github.com/mackuba/blue_factory
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.6.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.4.10
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Write a short summary, because RubyGems requires one.
74
+ test_files: []