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 json_response(status: 'not_started')
161 when :end
162 json_response(status: 'finished')
163 else
164 progress = 1 - (until_date - user.registered_at) / (Time.now - user.registered_at)
165 json_response(status: 'in_progress', position: until_date.iso8601, progress: progress)
166 end
167 end
168
169 get '/.well-known/did.json' do
170 json_response({
171 '@context': ['https://www.w3.org/ns/did/v1'],
172 id: "did:web:#{HOSTNAME}",
173 service: [
174 {
175 id: '#lycan',
176 type: 'LycanServer',
177 serviceEndpoint: "https://#{HOSTNAME}"
178 }
179 ]
180 })
181 end
182end