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 = '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 end
27
28 before do
29 @authenticator = Authenticator.new(hostname: HOSTNAME)
30 end
31
32 get '/xrpc/blue.feeds.lycan.searchPosts' do
33 headers['access-control-allow-origin'] = '*'
34
35 if settings.development?
36 user = User.find_by(did: params[:user])
37 return json_error('UserNotFound', 'Missing "user" parameter') if user.nil?
38 else
39 begin
40 auth_header = env['HTTP_AUTHORIZATION']
41 did = @authenticator.decode_user_from_jwt(auth_header, 'blue.feeds.lycan.searchPosts')
42 rescue StandardError => e
43 p e
44 end
45
46 user = did && User.find_by(did: did)
47 return json_error('UserNotFound', 'Missing authentication header') if user.nil?
48 end
49
50 if params[:query].to_s.strip.empty?
51 return json_error('MissingParameter', 'Missing "query" parameter')
52 end
53
54 if params[:collection].to_s.strip.empty?
55 return json_error('MissingParameter', 'Missing "collection" parameter')
56 end
57
58 query = QueryParser.new(params[:query])
59
60 collection = case params[:collection]
61 when 'likes' then user.likes
62 when 'pins' then user.pins
63 when 'quotes' then user.quotes
64 when 'reposts' then user.reposts
65 else return json_error('InvalidParameter', 'Invalid search collection')
66 end
67
68 if query.terms.empty?
69 return json_response(terms: [], posts: [], cursor: nil)
70 end
71
72 items = collection
73 .joins(:post)
74 .includes(:post => :user)
75 .matching_terms(query.terms)
76 .excluding_terms(query.exclusions)
77 .reverse_chronologically
78 .after_cursor(params[:cursor])
79 .limit(PAGE_LIMIT)
80
81 post_uris = case params[:collection]
82 when 'quotes'
83 items.map { |x| "at://#{user.did}/app.bsky.feed.post/#{x.rkey}" }
84 else
85 items.map(&:post).map(&:at_uri)
86 end
87
88 json_response(terms: query.terms, posts: post_uris, cursor: items.last&.cursor)
89 end
90
91 get '/.well-known/did.json' do
92 json_response({
93 '@context': ['https://www.w3.org/ns/did/v1'],
94 id: "did:web:#{HOSTNAME}",
95 service: [
96 {
97 id: '#lycan',
98 type: 'LycanServer',
99 serviceEndpoint: "https://#{HOSTNAME}"
100 }
101 ]
102 })
103 end
104end