Template of a custom feed generator service for the Bluesky network in Ruby
at master 3.6 kB view raw
1require 'blue_factory/errors' 2require 'rainbow' 3require 'time' 4 5require_relative '../models/feed_post' 6 7class Feed 8 DEFAULT_LIMIT = 50 9 MAX_LIMIT = 100 10 11 # any unique number to use as a key in the database 12 def feed_id 13 raise NotImplementedError 14 end 15 16 def post_matches?(post) 17 raise NotImplementedError 18 end 19 20 # name of your feed, e.g. "What's Hot" 21 def display_name 22 raise NotImplementedError 23 end 24 25 # (optional) description of the feed, e.g. "Top trending content from the whole network" 26 def description 27 nil 28 end 29 30 # (optional) path of the feed avatar file 31 def avatar_file 32 nil 33 end 34 35 # (optional) special feed type (return :video for video feeds) 36 def content_mode 37 nil 38 end 39 40 # (optional) should posts be added to the feed from the firehose? 41 def is_updating? 42 true 43 end 44 45 # if the feed matches posts using keywords/regexps, highlight these keywords in the passed text 46 def colored_text(text) 47 text 48 end 49 50 def get_posts(params) 51 limit = check_query_limit(params) 52 query = FeedPost.where(feed_id: feed_id).joins(:post).select('posts.repo, posts.rkey, feed_posts.time, post_id') 53 .order('feed_posts.time DESC, post_id DESC').limit(limit) 54 55 if params[:cursor].to_s != "" 56 time, last_id = parse_cursor(params) 57 query = query.where("feed_posts.time < ? OR (feed_posts.time = ? AND post_id < ?)", time, time, last_id) 58 end 59 60 posts = query.to_a 61 last = posts.last 62 63 cursor = last && sprintf('%.06f', last.time.to_f) + ':' + last.post_id.to_s 64 65 { cursor: cursor, posts: posts.map { |p| 'at://' + p.repo + '/app.bsky.feed.post/' + p.rkey }} 66 end 67 68 # Use this version of the method when enable_unsafe_auth is set to true in app/config.rb. 69 # This method is called with the DID of the user viewing the feed decoded from the auth header. 70 # 71 # Important: BlueFactory does not verify auth signatures of the JWT token at the moment, 72 # which means that anyone can easily spoof the DID in the header - do not use for anything critical! 73 # 74 # def get_posts(params, visitor_did) 75 # # You can use this DID to e.g.: 76 # # 1) Provide a personalized feed: 77 # 78 # user = User.find_by(did: visitor_did) 79 # raise BlueFactory::AuthorizationError unless user 80 # build_feed_for_user(user) 81 # 82 # # 2) Only allow whitelisted users to view the feed and show it as empty to others: 83 # if visitor_did == BlueFactory.publisher_did || ALLOWED_USERS.include?(visitor_did) 84 # get_feed_posts(params) 85 # else 86 # { posts: [] } 87 # end 88 # 89 # # 3) Ban some users from accessing the feed: 90 # # (note: you can return a custom error message that will be shown in the error banner in the app) 91 # if BANNED_USERS.include?(visitor_did) 92 # raise BlueFactory::AuthorizationError, "You shall not pass!" 93 # else 94 # get_feed_posts(params) 95 # end 96 # 97 # # 4) Collect feed analytics (please take the privacy of your users into account) 98 # Stats.count_visit(visitor_did) 99 # end 100 101 102 private 103 104 def check_query_limit(params) 105 if params[:limit] 106 limit = params[:limit].to_i 107 (limit < 0) ? 0 : [limit, MAX_LIMIT].min 108 else 109 DEFAULT_LIMIT 110 end 111 end 112 113 def parse_cursor(params) 114 parts = params[:cursor].split(':') 115 116 if parts.length != 2 || parts[0] !~ /^\d+(\.\d+)?$/ || parts[1] !~ /^\d+$/ 117 raise BlueFactory::InvalidRequestError.new("Malformed cursor") 118 end 119 120 sec = parts[0].to_i 121 usec = (parts[0].to_f * 1_000_000).to_i % 1_000_000 122 time = Time.at(sec, usec) 123 124 last_id = parts[1].to_i 125 126 [time, last_id] 127 end 128end