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