[WIP] Post Roulette feed where it sends you random posts by users you follow, unbiased by post age.

got most of followgraphseeder almost done?

+84 -29
+10
.vscode/settings.json
···
··· 1 + { 2 + "editor.formatOnSave": true, 3 + "[ruby]": { 4 + "editor.defaultFormatter": "Shopify.ruby-lsp" 5 + }, 6 + "rubyLsp.formatter": "standard", 7 + "rubyLsp.linters": [ 8 + "standard" 9 + ] 10 + }
+1 -1
Rakefile
··· 30 BlueFactory::Server.run! 31 end 32 task :firehose do 33 - firehose = PostRouletteFeed::Firehose.new ENV.fetch("REDIS_URL") 34 firehose.run 35 end 36 end
··· 30 BlueFactory::Server.run! 31 end 32 task :firehose do 33 + firehose = PostRouletteFeed::Firehose.new 34 firehose.run 35 end 36 end
+1 -1
bskypostroulettefeed.gemspec
··· 4 5 Gem::Specification.new do |spec| 6 spec.name = "bskypostroulettefeed" 7 - spec.version = Bskypostroulettefeed::VERSION 8 spec.authors = ["ansxor"] 9 spec.email = ["me@ansxor.ca"] 10
··· 4 5 Gem::Specification.new do |spec| 6 spec.name = "bskypostroulettefeed" 7 + spec.version = PostRouletteFeed::VERSION 8 spec.authors = ["ansxor"] 9 spec.email = ["me@ansxor.ca"] 10
+2 -2
db/migrate/001_create_sample_records.rb
··· 9 create_table(:users) do 10 primary_key :id 11 String :did, null: false 12 - backfill_status_enum :backfill_status, null: false, default: 'unwatched' 13 Boolean :is_feed_subscriber, null: false, default: false 14 15 index :backfill_status 16 index :is_feed_subscriber 17 end 18 19 - create_join_table({follower_id: :users, followed_id: :users}, options: { name: 'user_relationships' }) do 20 index %i[follower_id followed_id], unique: true 21 index :followed_id 22 end
··· 9 create_table(:users) do 10 primary_key :id 11 String :did, null: false 12 + backfill_status_enum :backfill_status, null: false, default: "unwatched" 13 Boolean :is_feed_subscriber, null: false, default: false 14 15 index :backfill_status 16 index :is_feed_subscriber 17 end 18 19 + create_join_table({follower_id: :users, followed_id: :users}, options: {name: "user_relationships"}) do 20 index %i[follower_id followed_id], unique: true 21 index :followed_id 22 end
+1 -1
lib/bskypostroulettefeed.rb
··· 2 3 require_relative "bskypostroulettefeed/version" 4 5 - module Bskypostroulettefeed 6 class Error < StandardError; end 7 # Your code goes here... 8 end
··· 2 3 require_relative "bskypostroulettefeed/version" 4 5 + module PostRouletteFeed 6 class Error < StandardError; end 7 # Your code goes here... 8 end
+1 -1
lib/bskypostroulettefeed/version.rb
··· 1 # frozen_string_literal: true 2 3 - module Bskypostroulettefeed 4 VERSION = "0.1.0" 5 end
··· 1 # frozen_string_literal: true 2 3 + module PostRouletteFeed 4 VERSION = "0.1.0" 5 end
+2 -2
lib/feed/config.rb
··· 1 - require 'blue_factory' 2 require_relative "feed" 3 4 BlueFactory.set :publisher_did, ENV.fetch("FEED_PUBLISHER_DID") 5 BlueFactory.set :hostname, ENV.fetch("FEED_HOSTNAME") 6 7 - BlueFactory.add_feed "postroulette", PostRouletteFeed::Feed.new
··· 1 + require "blue_factory" 2 require_relative "feed" 3 4 BlueFactory.set :publisher_did, ENV.fetch("FEED_PUBLISHER_DID") 5 BlueFactory.set :hostname, ENV.fetch("FEED_HOSTNAME") 6 7 + BlueFactory.add_feed "postroulette", PostRouletteFeed::Feed.new
+4 -8
lib/feed/feed.rb
··· 2 class Feed 3 def get_posts(params, context) 4 p context.user 5 - { posts: ["at://did:plc:brka7yc4gssxdquiwpii22pr/app.bsky.feed.post/3mckyk6xwhc2y"] } 6 end 7 8 - def display_name 9 - "Post Roulette" 10 - end 11 12 - def description 13 - "This is my testing feed for now" 14 - end 15 end 16 - end
··· 2 class Feed 3 def get_posts(params, context) 4 p context.user 5 + {posts: ["at://did:plc:brka7yc4gssxdquiwpii22pr/app.bsky.feed.post/3mckyk6xwhc2y"]} 6 end 7 8 + def display_name = "Post Roulette" 9 10 + def description = "This is my testing feed for now" 11 end 12 + end
+5 -5
lib/firehose/firehose.rb
··· 1 - require 'skyfall' 2 - require 'redis' 3 4 module PostRouletteFeed 5 class Firehose 6 def initialize redis_url 7 @sky = Skyfall::Firehose.new("bsky.network", :subscribe_repos) ··· 10 11 def run 12 redis = Redis.new(url: @redis_url) 13 - 14 @sky.on_message do |msg| 15 - if redis.exists? "watching:#{msg.did}" 16 p "watching", msg 17 end 18 end ··· 20 end 21 end 22 end 23 -
··· 1 + require "skyfall" 2 + require "redis" 3 + require_relative "../shared/redis" 4 5 module PostRouletteFeed 6 + using PostRouletteFeedRedis 7 class Firehose 8 def initialize redis_url 9 @sky = Skyfall::Firehose.new("bsky.network", :subscribe_repos) ··· 12 13 def run 14 redis = Redis.new(url: @redis_url) 15 @sky.on_message do |msg| 16 + if redis.watching? msg.did 17 p "watching", msg 18 end 19 end ··· 21 end 22 end 23 end
+19
lib/shared/redis.rb
···
··· 1 + require "redis" 2 + 3 + module PostRouletteFeedRedis 4 + refine Redis do 5 + def watching?(did) 6 + exists? watching_did_key(did) 7 + end 8 + 9 + def watch(did) 10 + set watching_did_key(did), true 11 + end 12 + 13 + private 14 + 15 + def watching_did_key(did) 16 + "watching:#{did}" 17 + end 18 + end 19 + end
+38 -8
lib/workers/followgraphseeder.rb
··· 1 - require 'sidekiq' 2 - require 'minisky' 3 4 module PostRouletteFeed 5 class FollowGraphSeeder 6 include Sidekiq::Worker 7 8 - def perform(did) 9 - atproto = Minisky.new('public.api.bsky.app', nil) 10 11 dids = [] 12 cursor = nil 13 14 - while true do 15 - json = atproto.get_request('app.bsky.graph.getFollows', { 16 - actor: did, 17 limit: 100, 18 cursor: cursor 19 }) 20 cursor = json["cursor"] 21 break if cursor.nil? 22 end 23 p "done" 24 end 25 end 26 - end
··· 1 + require "sidekiq" 2 + require "minisky" 3 + require "sequel" 4 + require_relative "../shared/redis" 5 6 module PostRouletteFeed 7 class FollowGraphSeeder 8 include Sidekiq::Worker 9 10 + using PostRouletteFeedRedis 11 + 12 + def initialize 13 + @redis_url = ENV.fetch("REDIS_URL") 14 + @database_url = ENV.fetch("DATABASE_URL") 15 + end 16 + 17 + def perform(follower_did) 18 + atproto = Minisky.new("public.api.bsky.app", nil) 19 20 dids = [] 21 cursor = nil 22 23 + loop do 24 + json = atproto.get_request("app.bsky.graph.getFollows", { 25 + actor: follower_did, 26 limit: 100, 27 cursor: cursor 28 }) 29 + dids.concat(Array(json["follows"]).map { |follow| follow["did"] }) 30 cursor = json["cursor"] 31 break if cursor.nil? 32 end 33 + # signal to redis that we need to start watching these users 34 + redis = Redis.new(url: @redis_url) 35 + dids.each do |did| 36 + redis.watch did 37 + end 38 + ## create the users / connections in postgres 39 + db = Sequel.connect(@database_url) 40 + users_dataset = db[:users] 41 + user_relationships_dataset = db[:users_relationships] 42 + # create the user that is viewing the feed 43 + follower_id_rows = users_dataset.insert_conflict(target: :did, update: {}).returning(:id).insert(did: follower_did, is_feed_subscriber: true) 44 + # we add the chain in case the user was already created before, as may be the case 45 + # if someone followed them and used this feed in the past 46 + follower_id_rows.first&.fetch(:id) || users_dataset.where(did: follower_did).get(:id) 47 + # create all of the users that the user is following 48 + followed_users = dids.each { |did| {did: did} } 49 + followed_users_rows = users_dataset.insert_conflict(target: :did, update: {}).returning(:id).multi_insert(followed_users) 50 + # create the relationships that signify the follower is following all these users 51 + user_relationships_dataset.insert_conflict(target: :follower_id, update: {}).multi_insert(followed_users_rows.each { |row| }) 52 + p dids.length 53 p "done" 54 end 55 end 56 + end