+14
-1
darkfeed/src/main/kotlin/Main.kt
+14
-1
darkfeed/src/main/kotlin/Main.kt
···
17
17
import rs.averyrive.darkfeed.server.FeedServer
18
18
import java.time.Instant
19
19
import java.time.format.DateTimeFormatter
20
+
import kotlin.io.path.Path
20
21
21
22
val PACKAGE_NAME: String = object {}.javaClass.packageName
22
23
···
56
57
by option(help = "Description to use for the feed", envvar = "FEED_DESCRIPTION")
57
58
.default("Development version of the NSFW Likes feed. Likely will not work.")
58
59
60
+
private val storageDir: String
61
+
by option(help = "Directory to store data in", envvar = "STORAGE_DIR")
62
+
.default("/var/lib/darkfeed")
63
+
59
64
private val debug: Boolean
60
65
by option(help = "Log debug information")
61
66
.flag(default = false)
···
99
104
log.warn("Authorized requests (e.g. updating feed generator record) will not be available")
100
105
}
101
106
107
+
// Create auth manager if the owner's account was found.
108
+
val authManager = authAccount?.let {
109
+
AuthManager(
110
+
authAccount = authAccount,
111
+
tokensPath = Path(storageDir, "auth_tokens.json")
112
+
)
113
+
}
114
+
102
115
// Create the client used for Bluesky API calls.
103
116
val bskyApi = BskyApi(
104
-
authManager = authAccount?.let { AuthManager(it) },
117
+
authManager = authManager,
105
118
atpIdentityClient = atpIdentityClient,
106
119
)
107
120
+74
-7
darkfeed/src/main/kotlin/api/AuthManager.kt
+74
-7
darkfeed/src/main/kotlin/api/AuthManager.kt
···
9
9
import io.ktor.http.*
10
10
import io.ktor.serialization.kotlinx.json.*
11
11
import kotlinx.serialization.Serializable
12
+
import kotlinx.serialization.SerializationException
13
+
import kotlinx.serialization.encodeToString
12
14
import kotlinx.serialization.json.Json
13
15
import org.slf4j.Logger
14
16
import org.slf4j.LoggerFactory
17
+
import java.nio.file.Path
15
18
16
19
/**
17
20
* Stores auth tokens and handles all auth related requests.
18
21
*
19
22
* @param authAccount ATProto account to use for authorization.
20
23
* @param httpClient Client to use for requests.
24
+
* @param tokensPath Path to store tokens in.
21
25
*/
22
26
class AuthManager(
23
27
private val authAccount: AuthAccount,
···
31
35
32
36
install(Logging)
33
37
},
38
+
private val tokensPath: Path,
34
39
) {
40
+
/** Logger to use for this class. */
41
+
private val log: Logger = LoggerFactory.getLogger(this.javaClass)
42
+
43
+
init {
44
+
loadTokens()
45
+
}
46
+
35
47
/** The current bearer tokens. */
36
48
var authTokens: AuthTokens? = null
37
49
private set
···
40
52
val authAccountDid
41
53
get() = authAccount.username
42
54
43
-
/** Logger to use for this class. */
44
-
private val log: Logger = LoggerFactory.getLogger(this.javaClass)
45
55
46
56
/** Create a new auth session. */
47
57
suspend fun createSession() {
···
80
90
val authSession: AuthSession = response.body()
81
91
this.authTokens = authSession.into()
82
92
log.debug("New session created, updated auth tokens.")
93
+
this.saveTokens()
83
94
} catch (error: Exception) {
84
95
TODO("Handle deserialization errors")
85
96
}
···
94
105
/** Refresh the current auth session. */
95
106
suspend fun refreshSession() {
96
107
val refreshSessionUrl = buildUrl {
97
-
protocol = URLProtocol.HTTPS
98
-
host = this@AuthManager.authAccount.pdsHost
99
-
path("/xrpc/com.atproto.server.refreshSession")
108
+
takeFrom(this@AuthManager.authAccount.pdsHost)
109
+
appendPathSegments("xrpc", "com.atproto.server.refreshSession")
100
110
}
101
111
102
112
val refreshToken = this.authTokens?.refreshToken!!
···
116
126
val authSession: AuthSession = response.body()
117
127
this.authTokens = authSession.into()
118
128
log.debug("Session refreshed, updated auth tokens")
129
+
this.saveTokens()
119
130
} catch (error: Exception) {
120
131
TODO("Handle deserialization errors")
121
132
}
···
126
137
}
127
138
}
128
139
}
140
+
141
+
/**
142
+
* Load tokens from disk.
143
+
*/
144
+
private fun loadTokens() {
145
+
log.debug("Loading auth tokens from '{}'...", tokensPath)
146
+
147
+
val tokensFile = tokensPath.toFile()
148
+
149
+
// Do nothing if tokens file doesn't exist.
150
+
if (!tokensFile.isFile) {
151
+
log.debug("No tokens found at '{}'", tokensPath)
152
+
return
153
+
}
154
+
155
+
// Load auth tokens from file.
156
+
val authTokens = try {
157
+
Json.decodeFromString<AuthTokens>(tokensFile.readText())
158
+
} catch (error: SerializationException) {
159
+
log.warn("Failed to load auth tokens from '{}': {}", tokensPath, error.message)
160
+
return
161
+
}
162
+
163
+
// Don't use tokens if they are for a different account.
164
+
if (this.authAccountDid != authTokens.did) {
165
+
log.info("Refusing to use stored auth tokens for '{}'", authTokens.did)
166
+
return
167
+
}
168
+
169
+
// Update auth tokens.
170
+
this.authTokens = authTokens
171
+
172
+
log.debug("Loaded tokens")
173
+
}
174
+
175
+
/**
176
+
* Save tokens to disk.
177
+
*/
178
+
private fun saveTokens() {
179
+
log.debug("Saving auth tokens to '{}'...", tokensPath)
180
+
181
+
// Open file and create parent directory if necessary.
182
+
val tokensFile = tokensPath.toFile()
183
+
tokensFile.parentFile?.mkdirs()
184
+
185
+
// Serialize auth tokens and write to the tokens file.
186
+
val tokensData = Json.encodeToString(authTokens)
187
+
tokensFile.bufferedWriter().use { writer ->
188
+
writer.write(tokensData)
189
+
}
190
+
191
+
log.debug("Saved tokens")
192
+
}
129
193
}
130
194
131
195
/**
···
147
211
* @param accessToken Token used for normal authorized requests.
148
212
* @param refreshToken Token used for session refresh requests.
149
213
*/
214
+
@Serializable
150
215
data class AuthTokens(
151
216
val accessToken: String,
152
217
val refreshToken: String,
218
+
val did: String,
153
219
)
154
220
155
221
/**
···
161
227
@Serializable
162
228
data class AuthSession(
163
229
val accessJwt: String,
164
-
val refreshJwt: String
230
+
val refreshJwt: String,
231
+
val did: String,
165
232
) {
166
233
/** Create `AuthTokens` from an `AuthSession`. */
167
-
fun into(): AuthTokens = AuthTokens(this.accessJwt, this.refreshJwt)
234
+
fun into(): AuthTokens = AuthTokens(this.accessJwt, this.refreshJwt, this.did)
168
235
}
+2
-1
darkfeed/src/main/kotlin/api/AuthPlugin.kt
+2
-1
darkfeed/src/main/kotlin/api/AuthPlugin.kt
···
10
10
import org.slf4j.LoggerFactory
11
11
12
12
const val PLUGIN_NAME: String = "AuthPlugin"
13
+
val LOGGER_NAME: String = "${object {}.javaClass.packageName}.$PLUGIN_NAME"
13
14
14
15
val AuthPlugin = createClientPlugin(PLUGIN_NAME, ::AuthPluginConfig) {
15
16
val authManager = pluginConfig.authManager ?: throw AuthPluginConfigurationError("Auth manager is required")
16
17
val authMutex = Mutex()
17
-
val log = LoggerFactory.getLogger(PLUGIN_NAME)
18
+
val log = LoggerFactory.getLogger(LOGGER_NAME)
18
19
19
20
// Add authorization header to requests.
20
21
onRequest { request, _ ->