Don't forget to lycansubscribe
1require 'json' 2require 'sinatra/base' 3 4require_relative 'init' 5require_relative 'authenticator' 6require_relative 'models/user' 7require_relative 'query_parser' 8 9class Server < Sinatra::Application 10 register Sinatra::ActiveRecordExtension 11 set :port, 3000 12 13 PAGE_LIMIT = 25 14 HOSTNAME = ENV['SERVER_HOSTNAME'] || 'lycan.feeds.blue' 15 16 helpers do 17 def json_response(data) 18 content_type :json 19 JSON.generate(data) 20 end 21 22 def json_error(name, message, status: 400) 23 content_type :json 24 [status, JSON.generate({ error: name, message: message })] 25 end 26 27 def get_user_did 28 if settings.development? 29 did = if request.post? 30 json = JSON.parse(request.body.read) 31 request.body.rewind 32 json['user'] 33 else 34 params[:user] 35 end 36 37 if did 38 return did 39 else 40 halt json_error('AuthMissing', 'Missing "user" parameter', status: 401) 41 end 42 else 43 auth_header = env['HTTP_AUTHORIZATION'] 44 if auth_header.to_s.strip.empty? 45 halt json_error('AuthMissing', 'Missing authentication header', status: 401) 46 end 47 48 begin 49 method = request.path.split('/')[2] 50 did = @authenticator.decode_user_from_jwt(auth_header, method) 51 rescue StandardError => e 52 halt json_error('InvalidToken', e.message, status: 401) 53 end 54 55 if did 56 return did 57 else 58 halt json_error('InvalidToken', "Invalid JWT token", status: 401) 59 end 60 end 61 end 62 63 def load_user 64 did = get_user_did 65 66 if user = User.find_by(did: did) 67 return user 68 else 69 halt json_error('AccountNotFound', 'Account not found', status: 401) 70 end 71 end 72 end 73 74 before do 75 @authenticator = Authenticator.new(hostname: HOSTNAME) 76 77 if settings.development? 78 headers['Access-Control-Allow-Origin'] = '*' 79 end 80 end 81 82 get '/' do 83 "Awoo 🐺" 84 end 85 86 get '/xrpc/blue.feeds.lycan.searchPosts' do 87 @user = load_user 88 89 if params[:query].to_s.strip.empty? 90 return json_error('MissingParameter', 'Missing "query" parameter') 91 end 92 93 if params[:collection].to_s.strip.empty? 94 return json_error('MissingParameter', 'Missing "collection" parameter') 95 end 96 97 query = QueryParser.new(params[:query]) 98 99 collection = case params[:collection] 100 when 'likes' then @user.likes 101 when 'pins' then @user.pins 102 when 'quotes' then @user.quotes 103 when 'reposts' then @user.reposts 104 else return json_error('InvalidParameter', 'Invalid search collection') 105 end 106 107 if query.terms.empty? 108 return json_response(terms: [], posts: [], cursor: nil) 109 end 110 111 items = collection 112 .joins(:post) 113 .includes(:post => :user) 114 .matching_terms(query.terms) 115 .excluding_terms(query.exclusions) 116 .reverse_chronologically 117 .after_cursor(params[:cursor]) 118 .limit(PAGE_LIMIT) 119 120 post_uris = case params[:collection] 121 when 'quotes' 122 items.map { |x| "at://#{@user.did}/app.bsky.feed.post/#{x.rkey}" } 123 else 124 items.map(&:post).map(&:at_uri) 125 end 126 127 json_response(terms: query.terms, posts: post_uris, cursor: items.last&.cursor) 128 end 129 130 if settings.development? 131 options '/xrpc/blue.feeds.lycan.startImport' do 132 headers['Access-Control-Allow-Origin'] = '*' 133 headers['Access-Control-Allow-Headers'] = 'content-type' 134 end 135 end 136 137 post '/xrpc/blue.feeds.lycan.startImport' do 138 did = get_user_did 139 user = User.find_or_create_by(did: did) 140 141 if !user.valid? 142 json_error('InvalidRequest', 'Invalid DID') 143 elsif user.import_job || user.active? 144 json_response(message: "Import has already started") 145 else 146 user.create_import_job! 147 148 status 202 149 json_response(message: "Import has been scheduled") 150 end 151 end 152 153 get '/xrpc/blue.feeds.lycan.getImportStatus' do 154 did = get_user_did 155 user = User.find_by(did: did) 156 until_date = user&.imported_until 157 158 case until_date 159 when nil 160 if user.import_job || user.imports.exists? 161 json_response(status: 'scheduled') 162 else 163 json_response(status: 'not_started') 164 end 165 when :end 166 json_response(status: 'finished') 167 else 168 progress = 1 - (until_date - user.registered_at) / (Time.now - user.registered_at) 169 json_response(status: 'in_progress', position: until_date.iso8601, progress: progress) 170 end 171 end 172 173 get '/.well-known/did.json' do 174 json_response({ 175 '@context': ['https://www.w3.org/ns/did/v1'], 176 id: "did:web:#{HOSTNAME}", 177 service: [ 178 { 179 id: '#lycan', 180 type: 'LycanServer', 181 serviceEndpoint: "https://#{HOSTNAME}" 182 } 183 ] 184 }) 185 end 186end