OXYGENHUB
# db00632a61d6 oxygenhub-android / app / src / main / java / io / oxygenhub / android / api / Models.kt
5.0 KiB · last changed The hub's API and device sign-in, the token in the Keystore (refs #2) 11 days ago source blame history raw
1
package io.oxygenhub.android.api
2
3
import kotlinx.serialization.SerialName
4
import kotlinx.serialization.Serializable
5
6
// The hub's JSON, by hand from oxygenhub's crates/hub-api/src/lib.rs (and
7
// the device flow's answers in crates/hub/src/api/device.rs). Field names
8
// are the hub's own, snake case; times are milliseconds since the epoch.
9
// Every field has a default, so an added or missing field never breaks the
10
// app: the hub's types may still change before 1.0.
11
12
/** `GET /api/v1/user`. */
13
@Serializable
14
data class User(
15
    val username: String,
16
    @SerialName("display_name") val displayName: String = "",
17
    @SerialName("is_admin") val isAdmin: Boolean = false,
18
    val keys: List<String> = emptyList(),
19
    @SerialName("created_ms") val createdMs: Long = 0,
20
) {
21
    /** The name to greet: the display name, or the username without one. */
22
    val shownName: String get() = displayName.ifBlank { username }
23
}
24
25
@Serializable
26
enum class Visibility {
27
    @SerialName("public") Public,
28
    @SerialName("internal") Internal,
29
    @SerialName("private") Private,
30
}
31
32
/** A repository as `GET /api/v1/repos` lists it. */
33
@Serializable
34
data class Repository(
35
    val owner: String,
36
    val name: String,
37
    val description: String = "",
38
    @SerialName("default_bookmark") val defaultBookmark: String = "main",
39
    val visibility: Visibility = Visibility.Public,
40
    @SerialName("created_ms") val createdMs: Long = 0,
41
    @SerialName("updated_ms") val updatedMs: Long = 0,
42
    @SerialName("clone_url") val cloneUrl: String = "",
43
) {
44
    val slug: String get() = "$owner/$name"
45
}
46
47
@Serializable
48
data class Label(
49
    val name: String,
50
    /** `#rrggbb`. */
51
    val colour: String = "",
52
    val description: String = "",
53
)
54
55
/** `GET /api/v1/repos/{owner}/{name}/issues`. */
56
@Serializable
57
data class Issue(
58
    val number: Long,
59
    val title: String,
60
    val body: String = "",
61
    val author: String = "",
62
    /** `open` or `closed`. */
63
    val state: String = "open",
64
    /** Why it closed: `completed`, `not_planned`, … (empty while open). */
65
    val reason: String = "",
66
    val labels: List<Label> = emptyList(),
67
    val assignees: List<String> = emptyList(),
68
    val milestone: String? = null,
69
    val comments: Long = 0,
70
    val locked: Boolean = false,
71
    val pinned: Boolean = false,
72
    @SerialName("created_ms") val createdMs: Long = 0,
73
    @SerialName("updated_ms") val updatedMs: Long = 0,
74
    @SerialName("closed_ms") val closedMs: Long? = null,
75
    val url: String = "",
76
) {
77
    val isOpen: Boolean get() = state == "open"
78
}
79
80
/** Which issues to list: the hub's `?state=`. */
81
enum class IssueState(val query: String) { Open("open"), Closed("closed"), All("all") }
82
83
/** One entry in the caller's inbox. */
84
@Serializable
85
data class Notification(
86
    val id: Long,
87
    @SerialName("at_ms") val atMs: Long = 0,
88
    /** `issue.opened`, `proposal.comment`, `mention`, `review`, … */
89
    val kind: String = "",
90
    val title: String = "",
91
    /** The page on the hub, absolute. */
92
    val url: String = "",
93
    val actor: String = "",
94
    val read: Boolean = false,
95
)
96
97
/** `GET /api/v1/notifications`: the unread count and the newest entries, unread first. */
98
@Serializable
99
data class Inbox(
100
    val unread: Long = 0,
101
    val items: List<Notification> = emptyList(),
102
)
103
104
/** An RFC 7807 problem: what the API answers with on an error. */
105
@Serializable
106
data class Problem(
107
    val status: Int = 0,
108
    val title: String = "",
109
    val detail: String = "",
110
)
111
112
/** The device flow's errors, RFC 8628's shape: `{"error": …, "error_description": …}`. */
113
@Serializable
114
data class OAuthError(
115
    val error: String = "",
116
    @SerialName("error_description") val description: String = "",
117
)
118
119
/** `POST /api/v1/device/code`'s answer. */
120
@Serializable
121
data class DeviceCode(
122
    @SerialName("device_code") val deviceCode: String,
123
    @SerialName("user_code") val userCode: String,
124
    @SerialName("verification_uri") val verificationUri: String,
125
    @SerialName("verification_uri_complete") val verificationUriComplete: String = "",
126
    @SerialName("expires_in") val expiresIn: Long = 900,
127
    val interval: Long = 5,
128
) {
129
    /** The page to open: with the code filled in when the hub offers it. */
130
    val pageToOpen: String get() = verificationUriComplete.ifBlank { verificationUri }
131
132
    // The device code is a secret until it is used: never in logs.
133
    override fun toString(): String = "DeviceCode(userCode=$userCode, verificationUri=$verificationUri)"
134
}
135
136
/** `POST /api/v1/device/token`'s answer once the user approved. */
137
@Serializable
138
data class TokenGrant(
139
    @SerialName("access_token") val accessToken: String,
140
    @SerialName("token_type") val tokenType: String = "bearer",
141
    /** The scopes the user allowed, space separated (they may untick some). */
142
    val scope: String = "",
143
    val username: String = "",
144
    @SerialName("expires_in") val expiresIn: Long = 0,
145
) {
146
    val scopes: List<String> get() = scope.split(' ').filter { it.isNotBlank() }
147
148
    override fun toString(): String = "TokenGrant(username=$username, scope=$scope, accessToken=<redacted>)"
149
}