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 '/xrpc/blue.feeds.lycan.searchPosts' do
83 @user = load_user
84
85 if params[:query].to_s.strip.empty?
86 return json_error('MissingParameter', 'Missing "query" parameter')
87 end
88
89 if params[:collection].to_s.strip.empty?
90 return json_error('MissingParameter', 'Missing "collection" parameter')
91 end
92
93 query = QueryParser.new(params[:query])
94
95 collection = case params[:collection]
96 when 'likes' then @user.likes
97 when 'pins' then @user.pins
98 when 'quotes' then @user.quotes
99 when 'reposts' then @user.reposts
100 else return json_error('InvalidParameter', 'Invalid search collection')
101 end
102
103 if query.terms.empty?
104 return json_response(terms: [], posts: [], cursor: nil)
105 end
106
107 items = collection
108 .joins(:post)
109 .includes(:post => :user)
110 .matching_terms(query.terms)
111 .excluding_terms(query.exclusions)
112 .reverse_chronologically
113 .after_cursor(params[:cursor])
114 .limit(PAGE_LIMIT)
115
116 post_uris = case params[:collection]
117 when 'quotes'
118 items.map { |x| "at://#{@user.did}/app.bsky.feed.post/#{x.rkey}" }
119 else
120 items.map(&:post).map(&:at_uri)
121 end
122
123 json_response(terms: query.terms, posts: post_uris, cursor: items.last&.cursor)
124 end
125
126 if settings.development?
127 options '/xrpc/blue.feeds.lycan.startImport' do
128 headers['Access-Control-Allow-Origin'] = '*'
129 headers['Access-Control-Allow-Headers'] = 'content-type'
130 end
131 end
132
133 post '/xrpc/blue.feeds.lycan.startImport' do
134 did = get_user_did
135 user = User.find_or_create_by(did: did)
136
137 if !user.valid?
138 json_error('InvalidRequest', 'Invalid DID')
139 elsif user.import_job || user.active?
140 json_response(message: "Import has already started")
141 else
142 user.create_import_job!
143
144 status 202
145 json_response(message: "Import has been scheduled")
146 end
147 end
148
149 get '/xrpc/blue.feeds.lycan.getImportStatus' do
150 did = get_user_did
151 user = User.find_by(did: did)
152 until_date = user&.imported_until
153
154 case until_date
155 when nil
156 json_response(status: 'not_started')
157 when :end
158 json_response(status: 'finished')
159 else
160 progress = 1 - (until_date - user.registered_at) / (Time.now - user.registered_at)
161 json_response(status: 'in_progress', position: until_date.iso8601, progress: progress)
162 end
163 end
164
165 get '/.well-known/did.json' do
166 json_response({
167 '@context': ['https://www.w3.org/ns/did/v1'],
168 id: "did:web:#{HOSTNAME}",
169 service: [
170 {
171 id: '#lycan',
172 type: 'LycanServer',
173 serviceEndpoint: "https://#{HOSTNAME}"
174 }
175 ]
176 })
177 end
178end