+6
-6
_meta.lua
+6
-6
_meta.lua
···
1
-
local _ = require("gettext")
2
-
return {
3
-
name = "paperbnd",
4
-
fullname = _("Paperbnd"),
5
-
description = _([[Synchronize reading progress to Paperbnd]]),
6
-
}
1
+
local _ = require("gettext")
2
+
return {
3
+
name = "paperbnd",
4
+
fullname = _("Paperbnd"),
5
+
description = _([[Synchronize reading progress to Paperbnd]]),
6
+
}
+400
-413
main.lua
+400
-413
main.lua
···
9
9
10
10
local XRPCClient = require("xrpc")
11
11
12
-
local Paperbnd = WidgetContainer:extend {
13
-
name = "paperbnd",
14
-
is_doc_only = false,
15
-
}
12
+
local Paperbnd = WidgetContainer:extend({
13
+
name = "paperbnd",
14
+
is_doc_only = false,
15
+
})
16
16
17
17
function Paperbnd:init()
18
-
self.ui.menu:registerToMainMenu(self)
18
+
self.ui.menu:registerToMainMenu(self)
19
19
20
-
-- Load settings
21
-
self.settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/paperbnd.lua")
22
-
self.handle = self.settings:readSetting("handle")
23
-
self.app_password = self.settings:readSetting("app_password")
24
-
self.pds_url = self.settings:readSetting("pds_url")
25
-
self.access_token = self.settings:readSetting("access_token")
26
-
self.refresh_token = self.settings:readSetting("refresh_token")
27
-
self.did = self.settings:readSetting("did")
28
-
self.document_mappings = self.settings:readSetting("document_mappings") or {}
29
-
self.auto_sync_enabled = self.settings:readSetting("auto_sync_enabled")
30
-
if self.auto_sync_enabled == nil then
31
-
self.auto_sync_enabled = false -- Default OFF for safety
32
-
end
20
+
-- Load settings
21
+
self.settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/paperbnd.lua")
22
+
self.handle = self.settings:readSetting("handle")
23
+
self.app_password = self.settings:readSetting("app_password")
24
+
self.pds_url = self.settings:readSetting("pds_url")
25
+
self.access_token = self.settings:readSetting("access_token")
26
+
self.refresh_token = self.settings:readSetting("refresh_token")
27
+
self.did = self.settings:readSetting("did")
28
+
self.document_mappings = self.settings:readSetting("document_mappings") or {}
29
+
self.auto_sync_enabled = self.settings:readSetting("auto_sync_enabled")
30
+
if self.auto_sync_enabled == nil then
31
+
self.auto_sync_enabled = false -- Default OFF for safety
32
+
end
33
33
34
-
-- Auto-sync state tracking
35
-
self.auto_sync_task = nil -- Scheduled sync task reference
36
-
self.last_synced_page = nil -- Prevent duplicate syncs
37
-
self.auto_sync_delay = 10.0 -- 10 second debounce delay
34
+
-- Auto-sync state tracking
35
+
self.auto_sync_task = nil -- Scheduled sync task reference
36
+
self.last_synced_page = nil -- Prevent duplicate syncs
37
+
self.auto_sync_delay = 10.0 -- 10 second debounce delay
38
38
39
-
-- Initialize XRPC client
40
-
self.xrpc = XRPCClient:new()
41
-
if self.pds_url then
42
-
self.xrpc:setPDS(self.pds_url)
43
-
end
44
-
if self.access_token then
45
-
self.xrpc:setAuth(self.access_token, self.refresh_token)
46
-
end
47
-
if self.handle and self.app_password then
48
-
self.xrpc:setCredentials(self.handle, self.app_password)
49
-
end
39
+
-- Initialize XRPC client
40
+
self.xrpc = XRPCClient:new()
41
+
if self.pds_url then
42
+
self.xrpc:setPDS(self.pds_url)
43
+
end
44
+
if self.access_token then
45
+
self.xrpc:setAuth(self.access_token, self.refresh_token)
46
+
end
47
+
if self.handle and self.app_password then
48
+
self.xrpc:setCredentials(self.handle, self.app_password)
49
+
end
50
50
51
-
-- Set up callback for credential updates
52
-
self.xrpc:setCredentialsCallback(function(access_token, refresh_token)
53
-
self:onTokenRefresh(access_token, refresh_token)
54
-
end)
51
+
-- Set up callback for credential updates
52
+
self.xrpc:setCredentialsCallback(function(access_token, refresh_token)
53
+
self:onTokenRefresh(access_token, refresh_token)
54
+
end)
55
55
56
-
-- Validate session at startup if we have credentials
57
-
if self:isAuthenticated() then
58
-
self:validateSessionAtStartup()
59
-
end
56
+
-- Validate session at startup if we have credentials
57
+
if self:isAuthenticated() then
58
+
self:validateSessionAtStartup()
59
+
end
60
60
end
61
61
62
62
function Paperbnd:saveSettings()
63
-
self.settings:saveSetting("handle", self.handle)
64
-
self.settings:saveSetting("app_password", self.app_password)
65
-
self.settings:saveSetting("pds_url", self.pds_url)
66
-
self.settings:saveSetting("access_token", self.access_token)
67
-
self.settings:saveSetting("refresh_token", self.refresh_token)
68
-
self.settings:saveSetting("did", self.did)
69
-
self.settings:saveSetting("document_mappings", self.document_mappings)
70
-
self.settings:saveSetting("auto_sync_enabled", self.auto_sync_enabled)
71
-
self.settings:flush()
63
+
self.settings:saveSetting("handle", self.handle)
64
+
self.settings:saveSetting("app_password", self.app_password)
65
+
self.settings:saveSetting("pds_url", self.pds_url)
66
+
self.settings:saveSetting("access_token", self.access_token)
67
+
self.settings:saveSetting("refresh_token", self.refresh_token)
68
+
self.settings:saveSetting("did", self.did)
69
+
self.settings:saveSetting("document_mappings", self.document_mappings)
70
+
self.settings:saveSetting("auto_sync_enabled", self.auto_sync_enabled)
71
+
self.settings:flush()
72
72
end
73
73
74
74
function Paperbnd:onTokenRefresh(access_token, refresh_token)
75
-
self.access_token = access_token
76
-
self.refresh_token = refresh_token
77
-
self:saveSettings()
75
+
self.access_token = access_token
76
+
self.refresh_token = refresh_token
77
+
self:saveSettings()
78
78
end
79
79
80
80
function Paperbnd:validateSessionAtStartup()
81
-
-- Validate session asynchronously to avoid blocking plugin startup
82
-
-- This will trigger automatic renewal if the session is expired
83
-
-- Session validation errors are intentionally ignored at startup
84
-
-- renewSession will be called automatically on the next authenticated request
85
-
self.xrpc:validateSession()
81
+
-- Validate session asynchronously to avoid blocking plugin startup
82
+
-- This will trigger automatic renewal if the session is expired
83
+
-- Session validation errors are intentionally ignored at startup
84
+
-- renewSession will be called automatically on the next authenticated request
85
+
self.xrpc:validateSession()
86
86
end
87
87
88
88
function Paperbnd:addToMainMenu(menu_items)
89
-
menu_items.paperbnd = {
90
-
text = _("Paperbnd"),
91
-
sub_item_table = {
92
-
{
93
-
text = _("Set credentials"),
94
-
keep_menu_open = true,
95
-
callback = function()
96
-
self:setCredentials()
97
-
end,
98
-
},
99
-
{
100
-
text = _("Link current book"),
101
-
enabled_func = function()
102
-
return self:isAuthenticated() and self.ui.document ~= nil
103
-
end,
104
-
callback = function()
105
-
self:linkCurrentBook()
106
-
end,
107
-
},
108
-
{
109
-
text = _("Sync progress now"),
110
-
enabled_func = function()
111
-
return self:isAuthenticated() and self:isCurrentBookLinked()
112
-
end,
113
-
callback = function()
114
-
self:syncProgress()
115
-
end,
116
-
},
117
-
{
118
-
text = _("Auto-sync on page turn"),
119
-
checked_func = function()
120
-
return self.auto_sync_enabled
121
-
end,
122
-
callback = function()
123
-
self.auto_sync_enabled = not self.auto_sync_enabled
124
-
self:saveSettings()
89
+
menu_items.paperbnd = {
90
+
text = _("Paperbnd"),
91
+
sub_item_table = {
92
+
{
93
+
text = _("Set credentials"),
94
+
keep_menu_open = true,
95
+
callback = function()
96
+
self:setCredentials()
97
+
end,
98
+
},
99
+
{
100
+
text = _("Link current book"),
101
+
enabled_func = function()
102
+
return self:isAuthenticated() and self.ui.document ~= nil
103
+
end,
104
+
callback = function()
105
+
self:linkCurrentBook()
106
+
end,
107
+
},
108
+
{
109
+
text = _("Sync progress now"),
110
+
enabled_func = function()
111
+
return self:isAuthenticated() and self:isCurrentBookLinked()
112
+
end,
113
+
callback = function()
114
+
self:syncProgress()
115
+
end,
116
+
},
117
+
{
118
+
text = _("Auto-sync on page turn"),
119
+
checked_func = function()
120
+
return self.auto_sync_enabled
121
+
end,
122
+
callback = function()
123
+
self.auto_sync_enabled = not self.auto_sync_enabled
124
+
self:saveSettings()
125
125
126
-
-- Cancel pending sync when disabling
127
-
if not self.auto_sync_enabled and self.auto_sync_task then
128
-
UIManager:unschedule(self.auto_sync_task)
129
-
self.auto_sync_task = nil
130
-
self.last_synced_page = nil
131
-
end
126
+
-- Cancel pending sync when disabling
127
+
if not self.auto_sync_enabled and self.auto_sync_task then
128
+
UIManager:unschedule(self.auto_sync_task)
129
+
self.auto_sync_task = nil
130
+
self.last_synced_page = nil
131
+
end
132
132
133
-
local status = self.auto_sync_enabled and _("enabled") or _("disabled")
134
-
UIManager:show(InfoMessage:new {
135
-
text = T(_("Auto-sync %1"), status),
136
-
timeout = 2,
137
-
})
138
-
end,
139
-
},
140
-
{
141
-
text = _("Unlink current book"),
142
-
enabled_func = function()
143
-
return self:isCurrentBookLinked()
144
-
end,
145
-
callback = function()
146
-
self:unlinkCurrentBook()
147
-
end,
148
-
},
149
-
},
150
-
}
133
+
local status = self.auto_sync_enabled and _("enabled") or _("disabled")
134
+
UIManager:show(InfoMessage:new({
135
+
text = T(_("Auto-sync %1"), status),
136
+
timeout = 2,
137
+
}))
138
+
end,
139
+
},
140
+
{
141
+
text = _("Unlink current book"),
142
+
enabled_func = function()
143
+
return self:isCurrentBookLinked()
144
+
end,
145
+
callback = function()
146
+
self:unlinkCurrentBook()
147
+
end,
148
+
},
149
+
},
150
+
}
151
151
end
152
152
153
153
function Paperbnd:isAuthenticated()
154
-
return self.handle ~= nil and self.access_token ~= nil and self.did ~= nil
154
+
return self.handle ~= nil and self.access_token ~= nil and self.did ~= nil
155
155
end
156
156
157
157
function Paperbnd:getCurrentDocumentPath()
158
-
if self.ui.document then
159
-
return self.ui.document.file
160
-
end
161
-
return nil
158
+
if self.ui.document then
159
+
return self.ui.document.file
160
+
end
161
+
return nil
162
162
end
163
163
164
164
function Paperbnd:isCurrentBookLinked()
165
-
local doc_path = self:getCurrentDocumentPath()
166
-
return doc_path ~= nil and self.document_mappings[doc_path] ~= nil
165
+
local doc_path = self:getCurrentDocumentPath()
166
+
return doc_path ~= nil and self.document_mappings[doc_path] ~= nil
167
167
end
168
168
169
169
function Paperbnd:setCredentials()
170
-
local handle_input
171
-
handle_input = InputDialog:new {
172
-
title = _("Enter your handle"),
173
-
input = self.handle or "",
174
-
buttons = {
175
-
{
176
-
{
177
-
text = _("Cancel"),
178
-
callback = function()
179
-
UIManager:close(handle_input)
180
-
end,
181
-
},
182
-
{
183
-
text = _("Next"),
184
-
is_enter_default = true,
185
-
callback = function()
186
-
local handle = handle_input:getInputText()
187
-
UIManager:close(handle_input)
170
+
local handle_input
171
+
handle_input = InputDialog:new({
172
+
title = _("Enter your handle"),
173
+
input = self.handle or "",
174
+
buttons = {
175
+
{
176
+
{
177
+
text = _("Cancel"),
178
+
callback = function()
179
+
UIManager:close(handle_input)
180
+
end,
181
+
},
182
+
{
183
+
text = _("Next"),
184
+
is_enter_default = true,
185
+
callback = function()
186
+
local handle = handle_input:getInputText()
187
+
UIManager:close(handle_input)
188
188
189
-
if handle and handle ~= "" then
190
-
self:setAppPassword(handle)
191
-
end
192
-
end,
193
-
},
194
-
},
195
-
},
196
-
}
197
-
UIManager:show(handle_input)
198
-
handle_input:onShowKeyboard()
189
+
if handle and handle ~= "" then
190
+
self:setAppPassword(handle)
191
+
end
192
+
end,
193
+
},
194
+
},
195
+
},
196
+
})
197
+
UIManager:show(handle_input)
198
+
handle_input:onShowKeyboard()
199
199
end
200
200
201
201
function Paperbnd:setAppPassword(handle)
202
-
local password_input
203
-
password_input = InputDialog:new {
204
-
title = _("Enter your app password"),
205
-
input = self.app_password or "",
206
-
text_type = "password",
207
-
buttons = {
208
-
{
209
-
{
210
-
text = _("Cancel"),
211
-
callback = function()
212
-
UIManager:close(password_input)
213
-
end,
214
-
},
215
-
{
216
-
text = _("Login"),
217
-
is_enter_default = true,
218
-
callback = function()
219
-
local password = password_input:getInputText()
220
-
UIManager:close(password_input)
202
+
local password_input
203
+
password_input = InputDialog:new({
204
+
title = _("Enter your app password"),
205
+
input = self.app_password or "",
206
+
text_type = "password",
207
+
buttons = {
208
+
{
209
+
{
210
+
text = _("Cancel"),
211
+
callback = function()
212
+
UIManager:close(password_input)
213
+
end,
214
+
},
215
+
{
216
+
text = _("Login"),
217
+
is_enter_default = true,
218
+
callback = function()
219
+
local password = password_input:getInputText()
220
+
UIManager:close(password_input)
221
221
222
-
if password and password ~= "" then
223
-
self:authenticate(handle, password)
224
-
end
225
-
end,
226
-
},
227
-
},
228
-
},
229
-
}
230
-
UIManager:show(password_input)
231
-
password_input:onShowKeyboard()
222
+
if password and password ~= "" then
223
+
self:authenticate(handle, password)
224
+
end
225
+
end,
226
+
},
227
+
},
228
+
},
229
+
})
230
+
UIManager:show(password_input)
231
+
password_input:onShowKeyboard()
232
232
end
233
233
234
234
function Paperbnd:authenticate(handle, password)
235
-
UIManager:show(InfoMessage:new {
236
-
text = _("Authenticating..."),
237
-
timeout = 1,
238
-
})
235
+
UIManager:show(InfoMessage:new({
236
+
text = _("Authenticating..."),
237
+
timeout = 1,
238
+
}))
239
239
240
-
-- Resolve PDS
241
-
local pds_url, pds_err = self.xrpc:resolvePDS(handle)
242
-
if pds_err then
243
-
UIManager:show(InfoMessage:new {
244
-
text = T(_("Failed to resolve PDS: %1"), pds_err),
245
-
})
246
-
return
247
-
end
240
+
-- Resolve PDS
241
+
local pds_url, pds_err = self.xrpc:resolvePDS(handle)
242
+
if pds_err then
243
+
UIManager:show(InfoMessage:new({
244
+
text = T(_("Failed to resolve PDS: %1"), pds_err),
245
+
}))
246
+
return
247
+
end
248
248
249
-
self.pds_url = pds_url
250
-
self.xrpc:setPDS(pds_url)
249
+
self.pds_url = pds_url
250
+
self.xrpc:setPDS(pds_url)
251
251
252
-
-- Create session
253
-
local session, session_err = self.xrpc:createSession(handle, password)
254
-
if session_err then
255
-
UIManager:show(InfoMessage:new {
256
-
text = T(_("Authentication failed: %1"), session_err),
257
-
})
258
-
return
259
-
end
252
+
-- Create session
253
+
local session, session_err = self.xrpc:createSession(handle, password)
254
+
if session_err then
255
+
UIManager:show(InfoMessage:new({
256
+
text = T(_("Authentication failed: %1"), session_err),
257
+
}))
258
+
return
259
+
end
260
260
261
-
self.handle = handle
262
-
self.app_password = password
263
-
self.access_token = session.accessJwt
264
-
self.refresh_token = session.refreshJwt
265
-
self.did = session.did
261
+
self.handle = handle
262
+
self.app_password = password
263
+
self.access_token = session.accessJwt
264
+
self.refresh_token = session.refreshJwt
265
+
self.did = session.did
266
266
267
-
-- Store credentials in XRPC client for automatic session renewal
268
-
self.xrpc:setCredentials(handle, password)
267
+
-- Store credentials in XRPC client for automatic session renewal
268
+
self.xrpc:setCredentials(handle, password)
269
269
270
-
self:saveSettings()
270
+
self:saveSettings()
271
271
272
-
UIManager:show(InfoMessage:new {
273
-
text = _("Authentication successful!"),
274
-
})
272
+
UIManager:show(InfoMessage:new({
273
+
text = _("Authentication successful!"),
274
+
}))
275
275
end
276
276
277
277
function Paperbnd:linkCurrentBook()
278
-
if not self:isAuthenticated() then
279
-
UIManager:show(InfoMessage:new {
280
-
text = _("Please set credentials first"),
281
-
})
282
-
return
283
-
end
278
+
if not self:isAuthenticated() then
279
+
UIManager:show(InfoMessage:new({
280
+
text = _("Please set credentials first"),
281
+
}))
282
+
return
283
+
end
284
284
285
-
UIManager:show(InfoMessage:new {
286
-
text = _("Fetching your books..."),
287
-
timeout = 1,
288
-
})
285
+
UIManager:show(InfoMessage:new({
286
+
text = _("Fetching your books..."),
287
+
timeout = 1,
288
+
}))
289
289
290
-
-- Fetch list items
291
-
local response, err = self.xrpc:listRecords(
292
-
self.did,
293
-
"social.popfeed.feed.listItem",
294
-
100,
295
-
nil
296
-
)
290
+
-- Fetch list items
291
+
local response, err = self.xrpc:listRecords(self.did, "social.popfeed.feed.listItem", 100, nil)
297
292
298
-
if err then
299
-
UIManager:show(InfoMessage:new {
300
-
text = T(_("Failed to fetch books: %1"), err),
301
-
})
302
-
return
303
-
end
293
+
if err then
294
+
UIManager:show(InfoMessage:new({
295
+
text = T(_("Failed to fetch books: %1"), err),
296
+
}))
297
+
return
298
+
end
304
299
300
+
if not response.records or #response.records == 0 then
301
+
UIManager:show(InfoMessage:new({
302
+
text = _("No books found in your account"),
303
+
}))
304
+
return
305
+
end
305
306
306
-
if not response.records or #response.records == 0 then
307
-
UIManager:show(InfoMessage:new {
308
-
text = _("No books found in your account"),
309
-
})
310
-
return
311
-
end
307
+
-- Filter for books only
308
+
local books = {}
309
+
for _, record in ipairs(response.records) do
310
+
if
311
+
record.value
312
+
and record.value.creativeWorkType == "book"
313
+
and record.value.listType == "currently_reading_books"
314
+
then
315
+
table.insert(books, {
316
+
title = record.value.title or "Unknown",
317
+
author = record.value.mainCredit or "Unknown",
318
+
rkey = record.uri:match("([^/]+)$"),
319
+
record = record.value,
320
+
})
321
+
end
322
+
end
312
323
313
-
-- Filter for books only
314
-
local books = {}
315
-
for _, record in ipairs(response.records) do
316
-
if record.value and record.value.creativeWorkType == "book"
317
-
and record.value.listType == "currently_reading_books" then
318
-
table.insert(books, {
319
-
title = record.value.title or "Unknown",
320
-
author = record.value.mainCredit or "Unknown",
321
-
rkey = record.uri:match("([^/]+)$"),
322
-
record = record.value,
323
-
})
324
-
end
325
-
end
324
+
if #books == 0 then
325
+
UIManager:show(InfoMessage:new({
326
+
text = _("No books found in your account"),
327
+
}))
328
+
return
329
+
end
326
330
327
-
328
-
if #books == 0 then
329
-
UIManager:show(InfoMessage:new {
330
-
text = _("No books found in your account"),
331
-
})
332
-
return
333
-
end
334
-
335
-
-- Show selection dialog
336
-
self:showBookSelectionDialog(books)
331
+
-- Show selection dialog
332
+
self:showBookSelectionDialog(books)
337
333
end
338
334
339
335
function Paperbnd:showBookSelectionDialog(books)
340
-
local Menu = require("ui/widget/menu")
341
-
local Screen = require("device").screen
336
+
local Menu = require("ui/widget/menu")
337
+
local Screen = require("device").screen
342
338
343
-
local items = {}
344
-
for _, book in ipairs(books) do
345
-
table.insert(items, {
346
-
text = book.title,
347
-
subtitle = book.author,
348
-
book = book,
349
-
})
350
-
end
339
+
local items = {}
340
+
for _, book in ipairs(books) do
341
+
table.insert(items, {
342
+
text = book.title,
343
+
subtitle = book.author,
344
+
book = book,
345
+
})
346
+
end
351
347
352
-
local book_menu = Menu:new {
353
-
title = _("Select a book"),
354
-
item_table = items,
355
-
width = Screen:getWidth() - Screen:scaleBySize(100),
356
-
height = Screen:getHeight() - Screen:scaleBySize(100),
357
-
fullscreen = true,
358
-
single_line = false,
359
-
onMenuSelect = function(_, item)
360
-
self:confirmLinkBook(item.book)
361
-
end,
362
-
}
348
+
local book_menu = Menu:new({
349
+
title = _("Select a book"),
350
+
item_table = items,
351
+
width = Screen:getWidth() - Screen:scaleBySize(100),
352
+
height = Screen:getHeight() - Screen:scaleBySize(100),
353
+
fullscreen = true,
354
+
single_line = false,
355
+
onMenuSelect = function(_, item)
356
+
self:confirmLinkBook(item.book)
357
+
end,
358
+
})
363
359
364
-
UIManager:show(book_menu)
360
+
UIManager:show(book_menu)
365
361
end
366
362
367
363
function Paperbnd:confirmLinkBook(book)
368
-
local doc_path = self:getCurrentDocumentPath()
369
-
if not doc_path then
370
-
return
371
-
end
364
+
local doc_path = self:getCurrentDocumentPath()
365
+
if not doc_path then
366
+
return
367
+
end
372
368
373
-
self.document_mappings[doc_path] = {
374
-
rkey = book.rkey,
375
-
title = book.title,
376
-
author = book.author,
377
-
}
369
+
self.document_mappings[doc_path] = {
370
+
rkey = book.rkey,
371
+
title = book.title,
372
+
author = book.author,
373
+
}
378
374
379
-
self:saveSettings()
375
+
self:saveSettings()
380
376
381
-
UIManager:show(InfoMessage:new {
382
-
text = T(_("Linked to: %1"), book.title),
383
-
})
377
+
UIManager:show(InfoMessage:new({
378
+
text = T(_("Linked to: %1"), book.title),
379
+
}))
384
380
end
385
381
386
382
function Paperbnd:unlinkCurrentBook()
387
-
local doc_path = self:getCurrentDocumentPath()
388
-
if doc_path and self.document_mappings[doc_path] then
389
-
local book_title = self.document_mappings[doc_path].title
390
-
self.document_mappings[doc_path] = nil
391
-
self:saveSettings()
383
+
local doc_path = self:getCurrentDocumentPath()
384
+
if doc_path and self.document_mappings[doc_path] then
385
+
local book_title = self.document_mappings[doc_path].title
386
+
self.document_mappings[doc_path] = nil
387
+
self:saveSettings()
392
388
393
-
UIManager:show(InfoMessage:new {
394
-
text = T(_("Unlinked: %1"), book_title),
395
-
})
396
-
end
389
+
UIManager:show(InfoMessage:new({
390
+
text = T(_("Unlinked: %1"), book_title),
391
+
}))
392
+
end
397
393
end
398
394
399
395
function Paperbnd:syncProgress()
400
-
if not self:isAuthenticated() then
401
-
return
402
-
end
396
+
if not self:isAuthenticated() then
397
+
return
398
+
end
403
399
404
-
local doc_path = self:getCurrentDocumentPath()
405
-
if not doc_path or not self.document_mappings[doc_path] then
406
-
return
407
-
end
400
+
local doc_path = self:getCurrentDocumentPath()
401
+
if not doc_path or not self.document_mappings[doc_path] then
402
+
return
403
+
end
408
404
409
-
local mapping = self.document_mappings[doc_path]
405
+
local mapping = self.document_mappings[doc_path]
410
406
411
-
-- Get document statistics
412
-
local pages = self.ui.document:getPageCount()
413
-
local current_page = self.ui.paging and self.ui.paging.current_page or 1
414
-
local percent = math.floor((current_page / pages) * 100)
407
+
-- Get document statistics
408
+
local pages = self.ui.document:getPageCount()
409
+
local current_page = self.ui.paging and self.ui.paging.current_page or 1
410
+
local percent = math.floor((current_page / pages) * 100)
415
411
416
-
-- Fetch current record
417
-
local record_response, err = self.xrpc:getRecord(
418
-
self.did,
419
-
"social.popfeed.feed.listItem",
420
-
mapping.rkey
421
-
)
412
+
-- Fetch current record
413
+
local record_response, err = self.xrpc:getRecord(self.did, "social.popfeed.feed.listItem", mapping.rkey)
422
414
423
-
if err then
424
-
UIManager:show(InfoMessage:new {
425
-
text = T(_("Failed to fetch record: %1"), err),
426
-
})
427
-
return
428
-
end
415
+
if err then
416
+
UIManager:show(InfoMessage:new({
417
+
text = T(_("Failed to fetch record: %1"), err),
418
+
}))
419
+
return
420
+
end
429
421
430
-
local record = record_response.value
422
+
local record = record_response.value
431
423
432
-
-- Update bookProgress
433
-
record.bookProgress = {
434
-
status = "in_progress",
435
-
percent = percent,
436
-
currentPage = current_page,
437
-
totalPages = pages,
438
-
updatedAt = os.date("!%Y-%m-%dT%H:%M:%S.000Z"),
439
-
}
424
+
-- Update bookProgress
425
+
record.bookProgress = {
426
+
status = "in_progress",
427
+
percent = percent,
428
+
currentPage = current_page,
429
+
totalPages = pages,
430
+
updatedAt = os.date("!%Y-%m-%dT%H:%M:%S.000Z"),
431
+
}
440
432
441
-
-- If not already in currently_reading, update listType
442
-
-- if record.listType ~= "currently_reading_books" then
443
-
-- record.listType = "currently_reading_books"
444
-
-- TODO: Also update the listUri to point to currently_reading list
445
-
-- end
433
+
-- If not already in currently_reading, update listType
434
+
-- if record.listType ~= "currently_reading_books" then
435
+
-- record.listType = "currently_reading_books"
436
+
-- TODO: Also update the listUri to point to currently_reading list
437
+
-- end
446
438
447
-
-- Put updated record
448
-
local _, put_err = self.xrpc:putRecord(
449
-
self.did,
450
-
"social.popfeed.feed.listItem",
451
-
mapping.rkey,
452
-
record
453
-
)
439
+
-- Put updated record
440
+
local _, put_err = self.xrpc:putRecord(self.did, "social.popfeed.feed.listItem", mapping.rkey, record)
454
441
455
-
if put_err then
456
-
UIManager:show(InfoMessage:new {
457
-
text = T(_("Failed to sync progress: %1"), put_err),
458
-
})
459
-
return
460
-
end
442
+
if put_err then
443
+
UIManager:show(InfoMessage:new({
444
+
text = T(_("Failed to sync progress: %1"), put_err),
445
+
}))
446
+
return
447
+
end
461
448
462
-
UIManager:show(InfoMessage:new {
463
-
text = T(_("Synced: %1% (%2/%3)"), percent, current_page, pages),
464
-
timeout = 2,
465
-
})
449
+
UIManager:show(InfoMessage:new({
450
+
text = T(_("Synced: %1% (%2/%3)"), percent, current_page, pages),
451
+
timeout = 2,
452
+
}))
466
453
end
467
454
468
455
function Paperbnd:scheduleDebouncedSync(pageno)
469
-
-- Only schedule if auto-sync enabled and authenticated/linked
470
-
if not self.auto_sync_enabled then
471
-
return
472
-
end
456
+
-- Only schedule if auto-sync enabled and authenticated/linked
457
+
if not self.auto_sync_enabled then
458
+
return
459
+
end
473
460
474
-
if not self:isAuthenticated() or not self:isCurrentBookLinked() then
475
-
return
476
-
end
461
+
if not self:isAuthenticated() or not self:isCurrentBookLinked() then
462
+
return
463
+
end
477
464
478
-
-- Cancel any existing scheduled sync
479
-
if self.auto_sync_task then
480
-
UIManager:unschedule(self.auto_sync_task)
481
-
self.auto_sync_task = nil
482
-
end
465
+
-- Cancel any existing scheduled sync
466
+
if self.auto_sync_task then
467
+
UIManager:unschedule(self.auto_sync_task)
468
+
self.auto_sync_task = nil
469
+
end
483
470
484
-
-- Create closure-based task for this scheduling
485
-
self.auto_sync_task = function()
486
-
self:performAutoSync(pageno)
487
-
end
471
+
-- Create closure-based task for this scheduling
472
+
self.auto_sync_task = function()
473
+
self:performAutoSync(pageno)
474
+
end
488
475
489
-
-- Schedule sync after debounce delay
490
-
UIManager:scheduleIn(self.auto_sync_delay, self.auto_sync_task)
476
+
-- Schedule sync after debounce delay
477
+
UIManager:scheduleIn(self.auto_sync_delay, self.auto_sync_task)
491
478
end
492
479
493
480
function Paperbnd:performAutoSync(pageno)
494
-
-- Clear task reference
495
-
self.auto_sync_task = nil
481
+
-- Clear task reference
482
+
self.auto_sync_task = nil
496
483
497
-
-- Check if page actually changed since last sync
498
-
if self.last_synced_page == pageno then
499
-
return
500
-
end
484
+
-- Check if page actually changed since last sync
485
+
if self.last_synced_page == pageno then
486
+
return
487
+
end
501
488
502
-
-- Double-check auth and linking
503
-
if not self:isAuthenticated() or not self:isCurrentBookLinked() then
504
-
return
505
-
end
489
+
-- Double-check auth and linking
490
+
if not self:isAuthenticated() or not self:isCurrentBookLinked() then
491
+
return
492
+
end
506
493
507
-
-- Perform sync (reuses existing logic)
508
-
self:syncProgress()
494
+
-- Perform sync (reuses existing logic)
495
+
self:syncProgress()
509
496
510
-
-- Track synced page
511
-
self.last_synced_page = pageno
497
+
-- Track synced page
498
+
self.last_synced_page = pageno
512
499
end
513
500
514
501
function Paperbnd:onPageUpdate(pageno)
515
-
-- Fires on every page turn - debounce to avoid excessive syncing
516
-
self:scheduleDebouncedSync(pageno)
502
+
-- Fires on every page turn - debounce to avoid excessive syncing
503
+
self:scheduleDebouncedSync(pageno)
517
504
518
-
-- Return false to allow event propagation
519
-
return false
505
+
-- Return false to allow event propagation
506
+
return false
520
507
end
521
508
522
509
-- Hook into document close to sync progress
523
510
function Paperbnd:onCloseDocument()
524
-
-- Cancel any pending auto-sync
525
-
if self.auto_sync_task then
526
-
UIManager:unschedule(self.auto_sync_task)
527
-
self.auto_sync_task = nil
528
-
end
511
+
-- Cancel any pending auto-sync
512
+
if self.auto_sync_task then
513
+
UIManager:unschedule(self.auto_sync_task)
514
+
self.auto_sync_task = nil
515
+
end
529
516
530
-
-- Clear page tracking for next document
531
-
self.last_synced_page = nil
517
+
-- Clear page tracking for next document
518
+
self.last_synced_page = nil
532
519
533
-
-- Always sync on close (original behavior)
534
-
if self:isAuthenticated() and self:isCurrentBookLinked() then
535
-
self:syncProgress()
536
-
end
520
+
-- Always sync on close (original behavior)
521
+
if self:isAuthenticated() and self:isCurrentBookLinked() then
522
+
self:syncProgress()
523
+
end
537
524
end
538
525
539
526
return Paperbnd
+241
-241
xrpc.lua
+241
-241
xrpc.lua
···
6
6
local rapidjson = require("rapidjson")
7
7
8
8
local XRPCClient = {
9
-
pds_url = nil,
10
-
handle = nil,
11
-
app_password = nil,
12
-
access_token = nil,
13
-
refresh_token = nil,
14
-
timeout = 30,
15
-
on_credentials_updated = nil, -- Callback for when credentials are refreshed
9
+
pds_url = nil,
10
+
handle = nil,
11
+
app_password = nil,
12
+
access_token = nil,
13
+
refresh_token = nil,
14
+
timeout = 30,
15
+
on_credentials_updated = nil, -- Callback for when credentials are refreshed
16
16
}
17
17
18
18
function XRPCClient:new(o)
19
-
o = o or {}
20
-
setmetatable(o, self)
21
-
self.__index = self
22
-
return o
19
+
o = o or {}
20
+
setmetatable(o, self)
21
+
self.__index = self
22
+
return o
23
23
end
24
24
25
25
function XRPCClient:setPDS(pds_url)
26
-
self.pds_url = pds_url
26
+
self.pds_url = pds_url
27
27
end
28
28
29
29
function XRPCClient:setAuth(access_token, refresh_token)
30
-
self.access_token = access_token
31
-
if refresh_token then
32
-
self.refresh_token = refresh_token
33
-
end
30
+
self.access_token = access_token
31
+
if refresh_token then
32
+
self.refresh_token = refresh_token
33
+
end
34
34
end
35
35
36
36
function XRPCClient:setCredentials(handle, app_password)
37
-
self.handle = handle
38
-
self.app_password = app_password
37
+
self.handle = handle
38
+
self.app_password = app_password
39
39
end
40
40
41
41
function XRPCClient:setCredentialsCallback(callback)
42
-
self.on_credentials_updated = callback
42
+
self.on_credentials_updated = callback
43
43
end
44
44
45
45
-- Notify that credentials were updated
46
46
function XRPCClient:notifyCredentialsUpdated(access_token, refresh_token)
47
-
if self.on_credentials_updated then
48
-
self.on_credentials_updated(access_token, refresh_token)
49
-
end
47
+
if self.on_credentials_updated then
48
+
self.on_credentials_updated(access_token, refresh_token)
49
+
end
50
50
end
51
51
52
52
function XRPCClient:call(opts)
53
-
opts = opts or {}
54
-
local method = opts.method
55
-
local params = opts.params
56
-
local body = opts.body
57
-
local skip_renewal = opts.skip_renewal or false
58
-
local pds = opts.pds or self.pds_url
53
+
opts = opts or {}
54
+
local method = opts.method
55
+
local params = opts.params
56
+
local body = opts.body
57
+
local skip_renewal = opts.skip_renewal or false
58
+
local pds = opts.pds or self.pds_url
59
59
60
-
if not pds then
61
-
return nil, "PDS URL not set"
62
-
end
60
+
if not pds then
61
+
return nil, "PDS URL not set"
62
+
end
63
63
64
-
local endpoint = pds .. "/xrpc/" .. method
64
+
local endpoint = pds .. "/xrpc/" .. method
65
65
66
-
-- Add query parameters if provided
67
-
if params and next(params) then
68
-
local query_parts = {}
69
-
for k, v in pairs(params) do
70
-
table.insert(query_parts, k .. "=" .. url.escape(tostring(v)))
71
-
end
72
-
endpoint = endpoint .. "?" .. table.concat(query_parts, "&")
73
-
end
66
+
-- Add query parameters if provided
67
+
if params and next(params) then
68
+
local query_parts = {}
69
+
for k, v in pairs(params) do
70
+
table.insert(query_parts, k .. "=" .. url.escape(tostring(v)))
71
+
end
72
+
endpoint = endpoint .. "?" .. table.concat(query_parts, "&")
73
+
end
74
74
75
-
local request_body = nil
76
-
local source = nil
77
-
local is_post = body ~= nil
78
-
local headers = {
79
-
["Accept"] = "application/json",
80
-
}
75
+
local request_body = nil
76
+
local source = nil
77
+
local is_post = body ~= nil
78
+
local headers = {
79
+
["Accept"] = "application/json",
80
+
}
81
81
82
-
-- Handle request body (POST only)
83
-
if is_post then
84
-
headers["Content-Type"] = "application/json"
82
+
-- Handle request body (POST only)
83
+
if is_post then
84
+
headers["Content-Type"] = "application/json"
85
85
86
-
-- Add authorization header for POST requests if token is set
87
-
if self.access_token then
88
-
headers["Authorization"] = "Bearer " .. self.access_token
89
-
end
86
+
-- Add authorization header for POST requests if token is set
87
+
if self.access_token then
88
+
headers["Authorization"] = "Bearer " .. self.access_token
89
+
end
90
90
91
-
local encoded, err = rapidjson.encode(body)
92
-
if err then
93
-
return nil, "Failed to encode JSON: " .. err
94
-
end
95
-
request_body = encoded
96
-
headers["Content-Length"] = tostring(#request_body)
97
-
source = ltn12.source.string(request_body)
98
-
end
91
+
local encoded, err = rapidjson.encode(body)
92
+
if err then
93
+
return nil, "Failed to encode JSON: " .. err
94
+
end
95
+
request_body = encoded
96
+
headers["Content-Length"] = tostring(#request_body)
97
+
source = ltn12.source.string(request_body)
98
+
end
99
99
100
-
local sink = {}
101
-
local request = {
102
-
url = endpoint,
103
-
method = is_post and "POST" or "GET",
104
-
sink = ltn12.sink.table(sink),
105
-
source = source,
106
-
headers = headers,
107
-
}
100
+
local sink = {}
101
+
local request = {
102
+
url = endpoint,
103
+
method = is_post and "POST" or "GET",
104
+
sink = ltn12.sink.table(sink),
105
+
source = source,
106
+
headers = headers,
107
+
}
108
108
109
-
socketutil:set_timeout(self.timeout, self.timeout)
110
-
local code, response_headers = socket.skip(1, http.request(request))
111
-
socketutil:reset_timeout()
109
+
socketutil:set_timeout(self.timeout, self.timeout)
110
+
local code, response_headers = socket.skip(1, http.request(request))
111
+
socketutil:reset_timeout()
112
112
113
-
if not code then
114
-
return nil, "HTTP request failed: " .. tostring(response_headers)
115
-
end
113
+
if not code then
114
+
return nil, "HTTP request failed: " .. tostring(response_headers)
115
+
end
116
116
117
-
local response_body = table.concat(sink)
117
+
local response_body = table.concat(sink)
118
118
119
-
-- Handle authentication errors with automatic session renewal
120
-
local is_auth_error = code == 401 or
121
-
(code == 400 and response_body and string.find(string.lower(response_body), "expired"))
119
+
-- Handle authentication errors with automatic session renewal
120
+
local is_auth_error = code == 401
121
+
or (code == 400 and response_body and string.find(string.lower(response_body), "expired"))
122
122
123
-
if is_auth_error and not skip_renewal then
124
-
-- Try to renew session (refresh token -> app password fallback)
125
-
local renewed, renew_err = self:renewSession()
123
+
if is_auth_error and not skip_renewal then
124
+
-- Try to renew session (refresh token -> app password fallback)
125
+
local renewed, renew_err = self:renewSession()
126
126
127
-
if renewed then
128
-
-- Session renewed successfully, retry the original request
129
-
-- Rebuild request with new authorization header
130
-
if is_post and self.access_token then
131
-
headers["Authorization"] = "Bearer " .. self.access_token
132
-
end
127
+
if renewed then
128
+
-- Session renewed successfully, retry the original request
129
+
-- Rebuild request with new authorization header
130
+
if is_post and self.access_token then
131
+
headers["Authorization"] = "Bearer " .. self.access_token
132
+
end
133
133
134
-
sink = {}
135
-
request.sink = ltn12.sink.table(sink)
136
-
if is_post then
137
-
request.source = ltn12.source.string(request_body)
138
-
end
139
-
request.headers = headers
134
+
sink = {}
135
+
request.sink = ltn12.sink.table(sink)
136
+
if is_post then
137
+
request.source = ltn12.source.string(request_body)
138
+
end
139
+
request.headers = headers
140
140
141
-
socketutil:set_timeout(self.timeout, self.timeout)
142
-
code, response_headers = socket.skip(1, http.request(request))
143
-
socketutil:reset_timeout()
141
+
socketutil:set_timeout(self.timeout, self.timeout)
142
+
code, response_headers = socket.skip(1, http.request(request))
143
+
socketutil:reset_timeout()
144
144
145
-
if not code then
146
-
return nil, "HTTP request failed after session renewal: " .. tostring(response_headers)
147
-
end
145
+
if not code then
146
+
return nil, "HTTP request failed after session renewal: " .. tostring(response_headers)
147
+
end
148
148
149
-
response_body = table.concat(sink)
150
-
else
151
-
-- Could not renew session
152
-
return nil, "Session expired and renewal failed: " .. (renew_err or "unknown error")
153
-
end
154
-
end
149
+
response_body = table.concat(sink)
150
+
else
151
+
-- Could not renew session
152
+
return nil, "Session expired and renewal failed: " .. (renew_err or "unknown error")
153
+
end
154
+
end
155
155
156
-
-- Handle non-200 responses
157
-
if code ~= 200 then
158
-
local error_msg
159
-
if response_body and response_body ~= "" then
160
-
local error_data = rapidjson.decode(response_body)
161
-
if error_data and error_data.message then
162
-
error_msg = error_data.message
163
-
else
164
-
error_msg = response_body
165
-
end
166
-
else
167
-
error_msg = "HTTP " .. code
168
-
end
156
+
-- Handle non-200 responses
157
+
if code ~= 200 then
158
+
local error_msg
159
+
if response_body and response_body ~= "" then
160
+
local error_data = rapidjson.decode(response_body)
161
+
if error_data and error_data.message then
162
+
error_msg = error_data.message
163
+
else
164
+
error_msg = response_body
165
+
end
166
+
else
167
+
error_msg = "HTTP " .. code
168
+
end
169
169
170
-
return nil, error_msg
171
-
end
170
+
return nil, error_msg
171
+
end
172
172
173
-
-- Decode response
174
-
if response_body and response_body ~= "" then
175
-
local decoded, err = rapidjson.decode(response_body)
176
-
if err then
177
-
return nil, "Failed to decode JSON response: " .. err
178
-
end
179
-
return decoded, nil
180
-
end
173
+
-- Decode response
174
+
if response_body and response_body ~= "" then
175
+
local decoded, err = rapidjson.decode(response_body)
176
+
if err then
177
+
return nil, "Failed to decode JSON response: " .. err
178
+
end
179
+
return decoded, nil
180
+
end
181
181
182
-
return {}, nil
182
+
return {}, nil
183
183
end
184
184
185
185
-- Resolve PDS from handle using Slingshot service
186
186
function XRPCClient:resolvePDS(handle)
187
-
local response, err = self:call({
188
-
method = "com.bad-example.identity.resolveMiniDoc",
189
-
params = { identifier = handle },
190
-
pds = "https://slingshot.microcosm.blue",
191
-
})
187
+
local response, err = self:call({
188
+
method = "com.bad-example.identity.resolveMiniDoc",
189
+
params = { identifier = handle },
190
+
pds = "https://slingshot.microcosm.blue",
191
+
})
192
192
193
-
if err or not response then
194
-
return nil, "Failed to resolve PDS for handle: " .. err
195
-
end
193
+
if err or not response then
194
+
return nil, "Failed to resolve PDS for handle: " .. err
195
+
end
196
196
197
-
if not response.pds then
198
-
return nil, "Invalid response from Slingshot service"
199
-
end
197
+
if not response.pds then
198
+
return nil, "Invalid response from Slingshot service"
199
+
end
200
200
201
-
return response.pds, nil
201
+
return response.pds, nil
202
202
end
203
203
204
204
-- Create session (login)
205
205
function XRPCClient:createSession(identifier, password)
206
-
local response, err = self:call({
207
-
method = "com.atproto.server.createSession",
208
-
body = {
209
-
identifier = identifier,
210
-
password = password,
211
-
},
212
-
skip_renewal = true, -- Don't auto-renew during initial login
213
-
})
206
+
local response, err = self:call({
207
+
method = "com.atproto.server.createSession",
208
+
body = {
209
+
identifier = identifier,
210
+
password = password,
211
+
},
212
+
skip_renewal = true, -- Don't auto-renew during initial login
213
+
})
214
214
215
-
if err or not response then
216
-
return nil, err
217
-
end
215
+
if err or not response then
216
+
return nil, err
217
+
end
218
218
219
-
if response.accessJwt then
220
-
self:setAuth(response.accessJwt, response.refreshJwt)
221
-
-- Note: We don't call notifyCredentialsUpdated here since the caller
222
-
-- of createSession is expected to handle storing credentials
223
-
end
219
+
if response.accessJwt then
220
+
self:setAuth(response.accessJwt, response.refreshJwt)
221
+
-- Note: We don't call notifyCredentialsUpdated here since the caller
222
+
-- of createSession is expected to handle storing credentials
223
+
end
224
224
225
-
return response, nil
225
+
return response, nil
226
226
end
227
227
228
228
-- Validate current session by making a test request
229
229
function XRPCClient:validateSession()
230
-
if not self.access_token then
231
-
return false, "No access token"
232
-
end
230
+
if not self.access_token then
231
+
return false, "No access token"
232
+
end
233
233
234
-
-- Make a lightweight request to check if session is valid
235
-
local response, err = self:call({
236
-
method = "com.atproto.server.getSession",
237
-
body = {},
238
-
skip_renewal = true, -- Don't auto-renew during validation
239
-
})
234
+
-- Make a lightweight request to check if session is valid
235
+
local response, err = self:call({
236
+
method = "com.atproto.server.getSession",
237
+
body = {},
238
+
skip_renewal = true, -- Don't auto-renew during validation
239
+
})
240
240
241
-
return response ~= nil, err
241
+
return response ~= nil, err
242
242
end
243
243
244
244
-- Refresh session using refresh token
245
245
function XRPCClient:refreshSession()
246
-
if not self.refresh_token then
247
-
return nil, "No refresh token available"
248
-
end
246
+
if not self.refresh_token then
247
+
return nil, "No refresh token available"
248
+
end
249
249
250
-
-- Temporarily store the current access token
251
-
local old_access_token = self.access_token
250
+
-- Temporarily store the current access token
251
+
local old_access_token = self.access_token
252
252
253
-
-- Use refresh token for this request
254
-
self.access_token = self.refresh_token
253
+
-- Use refresh token for this request
254
+
self.access_token = self.refresh_token
255
255
256
-
local response, err = self:call({
257
-
method = "com.atproto.server.refreshSession",
258
-
body = {},
259
-
skip_renewal = true, -- Don't recurse during refresh
260
-
})
256
+
local response, err = self:call({
257
+
method = "com.atproto.server.refreshSession",
258
+
body = {},
259
+
skip_renewal = true, -- Don't recurse during refresh
260
+
})
261
261
262
-
if err or not response then
263
-
-- Restore old access token on failure
264
-
self.access_token = old_access_token
265
-
return nil, err
266
-
end
262
+
if err or not response then
263
+
-- Restore old access token on failure
264
+
self.access_token = old_access_token
265
+
return nil, err
266
+
end
267
267
268
-
-- Update tokens with new values
269
-
if response.accessJwt then
270
-
self:setAuth(response.accessJwt, response.refreshJwt)
271
-
self:notifyCredentialsUpdated(response.accessJwt, response.refreshJwt)
272
-
end
268
+
-- Update tokens with new values
269
+
if response.accessJwt then
270
+
self:setAuth(response.accessJwt, response.refreshJwt)
271
+
self:notifyCredentialsUpdated(response.accessJwt, response.refreshJwt)
272
+
end
273
273
274
-
return response, nil
274
+
return response, nil
275
275
end
276
276
277
277
-- Renew session with multi-level fallback
278
278
-- 1. Try to use refresh token
279
279
-- 2. If refresh token expired, create new session with app password
280
280
function XRPCClient:renewSession()
281
-
-- First, try to refresh with refresh token
282
-
local response, err = self:refreshSession()
281
+
-- First, try to refresh with refresh token
282
+
local response, err = self:refreshSession()
283
283
284
-
if response then
285
-
return true, nil
286
-
end
284
+
if response then
285
+
return true, nil
286
+
end
287
287
288
-
-- If refresh failed and we have app password, try to create new session
289
-
if self.handle and self.app_password then
290
-
local session, session_err = self:createSession(self.handle, self.app_password)
288
+
-- If refresh failed and we have app password, try to create new session
289
+
if self.handle and self.app_password then
290
+
local session, session_err = self:createSession(self.handle, self.app_password)
291
291
292
-
if session then
293
-
-- New session created successfully, tokens already set by createSession
294
-
self:notifyCredentialsUpdated(session.accessJwt, session.refreshJwt)
295
-
return true, nil
296
-
end
292
+
if session then
293
+
-- New session created successfully, tokens already set by createSession
294
+
self:notifyCredentialsUpdated(session.accessJwt, session.refreshJwt)
295
+
return true, nil
296
+
end
297
297
298
-
return false, "Failed to refresh session and create new session: " .. (session_err or "unknown error")
299
-
end
298
+
return false, "Failed to refresh session and create new session: " .. (session_err or "unknown error")
299
+
end
300
300
301
-
return false,
302
-
"Failed to refresh session: " .. (err or "unknown error") .. ", and no app password available for fallback"
301
+
return false,
302
+
"Failed to refresh session: " .. (err or "unknown error") .. ", and no app password available for fallback"
303
303
end
304
304
305
305
-- List records from a collection
306
306
function XRPCClient:listRecords(repo, collection, limit, cursor)
307
-
local params = {
308
-
repo = repo,
309
-
collection = collection,
310
-
}
307
+
local params = {
308
+
repo = repo,
309
+
collection = collection,
310
+
}
311
311
312
-
if limit then
313
-
params.limit = limit
314
-
end
312
+
if limit then
313
+
params.limit = limit
314
+
end
315
315
316
-
if cursor then
317
-
params.cursor = cursor
318
-
end
316
+
if cursor then
317
+
params.cursor = cursor
318
+
end
319
319
320
-
return self:call({
321
-
method = "com.atproto.repo.listRecords",
322
-
params = params,
323
-
})
320
+
return self:call({
321
+
method = "com.atproto.repo.listRecords",
322
+
params = params,
323
+
})
324
324
end
325
325
326
326
-- Get a single record
327
327
function XRPCClient:getRecord(repo, collection, rkey)
328
-
local params = {
329
-
repo = repo,
330
-
collection = collection,
331
-
rkey = rkey,
332
-
}
328
+
local params = {
329
+
repo = repo,
330
+
collection = collection,
331
+
rkey = rkey,
332
+
}
333
333
334
-
return self:call({
335
-
method = "com.atproto.repo.getRecord",
336
-
params = params,
337
-
})
334
+
return self:call({
335
+
method = "com.atproto.repo.getRecord",
336
+
params = params,
337
+
})
338
338
end
339
339
340
340
-- Put (create or update) a record
341
341
function XRPCClient:putRecord(repo, collection, rkey, record)
342
-
return self:call({
343
-
method = "com.atproto.repo.putRecord",
344
-
body = {
345
-
repo = repo,
346
-
collection = collection,
347
-
rkey = rkey,
348
-
record = record,
349
-
},
350
-
})
342
+
return self:call({
343
+
method = "com.atproto.repo.putRecord",
344
+
body = {
345
+
repo = repo,
346
+
collection = collection,
347
+
rkey = rkey,
348
+
record = record,
349
+
},
350
+
})
351
351
end
352
352
353
353
return XRPCClient