1
1
package app.revanced.api.configuration.repository
2
2
3
- import app.revanced.api.configuration.schema.APIAnnouncement
4
- import app.revanced.api.configuration.schema.APIResponseAnnouncement
5
- import app.revanced.api.configuration.schema.APIResponseAnnouncementId
3
+ import app.revanced.api.configuration.schema.ApiAnnouncement
4
+ import app.revanced.api.configuration.schema.ApiAnnouncementTag
5
+ import app.revanced.api.configuration.schema.ApiResponseAnnouncement
6
+ import app.revanced.api.configuration.schema.ApiResponseAnnouncementId
6
7
import kotlinx.coroutines.Dispatchers
7
- import kotlinx.coroutines.awaitAll
8
8
import kotlinx.coroutines.runBlocking
9
9
import kotlinx.datetime.*
10
10
import org.jetbrains.exposed.dao.IntEntity
@@ -15,126 +15,175 @@ import org.jetbrains.exposed.sql.*
15
15
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
16
16
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
17
17
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
18
- import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
18
+ import java.time.LocalDateTime
19
19
20
20
internal class AnnouncementRepository {
21
- // This is better than doing a maxByOrNull { it.id }.
21
+ // This is better than doing a maxByOrNull { it.id } on every request .
22
22
private var latestAnnouncement: Announcement ? = null
23
- private val latestAnnouncementByChannel = mutableMapOf<String , Announcement >()
24
-
25
- private fun updateLatestAnnouncement (new : Announcement ) {
26
- if (latestAnnouncement?.id?.value == new.id.value) {
27
- latestAnnouncement = new
28
- latestAnnouncementByChannel[new.channel ? : return ] = new
29
- }
30
- }
23
+ private val latestAnnouncementByTag = mutableMapOf<Int , Announcement >()
31
24
32
25
init {
33
26
runBlocking {
34
27
transaction {
35
- SchemaUtils .create(Announcements , Attachments )
28
+ SchemaUtils .create(
29
+ Announcements ,
30
+ Attachments ,
31
+ Tags ,
32
+ AnnouncementTags ,
33
+ )
36
34
37
- // Initialize the latest announcement.
38
- latestAnnouncement = Announcement .all().onEach {
39
- latestAnnouncementByChannel[it.channel ? : return @onEach] = it
40
- }.maxByOrNull { it.id } ? : return @transaction
35
+ initializeLatestAnnouncements()
41
36
}
42
37
}
43
38
}
44
39
45
- suspend fun all () = transaction {
46
- Announcement .all().map { it.toApi() }
40
+ private fun initializeLatestAnnouncements () {
41
+ latestAnnouncement = Announcement .all().orderBy(Announcements .id to SortOrder .DESC ).firstOrNull()
42
+
43
+ Tag .all().map { it.id.value }.forEach(::updateLatestAnnouncementForTag)
47
44
}
48
45
49
- suspend fun all (channel : String ) = transaction {
50
- Announcement .find { Announcements .channel eq channel }.map { it.toApi() }
46
+ private fun updateLatestAnnouncement (new : Announcement ) {
47
+ if (latestAnnouncement == null || latestAnnouncement!! .id.value <= new.id.value) {
48
+ latestAnnouncement = new
49
+ new.tags.forEach { tag -> latestAnnouncementByTag[tag.id.value] = new }
50
+ }
51
51
}
52
52
53
- suspend fun delete (id : Int ) = transaction {
54
- val announcement = Announcement .findById(id) ? : return @transaction
53
+ private fun updateLatestAnnouncementForTag (tag : Int ) {
54
+ val latestAnnouncementForTag = AnnouncementTags .select(AnnouncementTags .announcement)
55
+ .where { AnnouncementTags .tag eq tag }
56
+ .map { it[AnnouncementTags .announcement] }
57
+ .mapNotNull { Announcement .findById(it) }
58
+ .maxByOrNull { it.id }
55
59
56
- announcement.delete()
60
+ latestAnnouncementForTag?.let { latestAnnouncementByTag[tag] = it }
61
+ }
57
62
58
- // In case the latest announcement was deleted, query the new latest announcement again.
59
- if ( latestAnnouncement?.id?.value == id) {
60
- latestAnnouncement = Announcement .all().maxByOrNull { it.id }
63
+ suspend fun latest () = transaction {
64
+ latestAnnouncement.toApiResponseAnnouncement()
65
+ }
61
66
62
- // If no latest announcement was found, remove it from the channel map.
63
- if (latestAnnouncement == null ) {
64
- latestAnnouncementByChannel.remove(announcement.channel)
65
- } else {
66
- latestAnnouncementByChannel[latestAnnouncement!! .channel ? : return @transaction] = latestAnnouncement!!
67
- }
68
- }
67
+ suspend fun latest (tags : Set <Int >) = transaction {
68
+ tags.mapNotNull { tag -> latestAnnouncementByTag[tag] }.toApiAnnouncement()
69
69
}
70
70
71
- fun latest () = latestAnnouncement?.toApi ()
71
+ fun latestId () = latestAnnouncement?.id?.value.toApiResponseAnnouncementId ()
72
72
73
- fun latest (channel : String ) = latestAnnouncementByChannel[channel]?.toApi()
73
+ fun latestId (tags : Set <Int >) =
74
+ tags.map { tag -> latestAnnouncementByTag[tag]?.id?.value }.toApiResponseAnnouncementId()
74
75
75
- fun latestId () = latest()?.id?.toApi()
76
+ suspend fun paged (cursor : Int , count : Int , tags : Set <Int >? , archived : Boolean ) = transaction {
77
+ Announcement .find {
78
+ fun idLessEq () = Announcements .id lessEq cursor
79
+ fun archivedAtIsNull () = Announcements .archivedAt.isNull()
80
+ fun archivedAtGreaterNow () = Announcements .archivedAt greater LocalDateTime .now().toKotlinLocalDateTime()
81
+
82
+ if (tags == null ) {
83
+ if (archived) {
84
+ idLessEq()
85
+ } else {
86
+ idLessEq() and (archivedAtIsNull() or archivedAtGreaterNow())
87
+ }
88
+ } else {
89
+ fun archivedAtGreaterOrNullOrTrue () = if (archived) {
90
+ Op .TRUE
91
+ } else {
92
+ archivedAtIsNull() or archivedAtGreaterNow()
93
+ }
76
94
77
- fun latestId (channel : String ) = latest(channel)?.id?.toApi()
95
+ fun hasTags () = tags.mapNotNull { Tag .findById(it)?.id }.let { tags ->
96
+ Announcements .id inSubQuery Announcements .leftJoin(AnnouncementTags )
97
+ .select(AnnouncementTags .announcement)
98
+ .where { AnnouncementTags .tag inList tags }
99
+ .withDistinct()
100
+ }
78
101
79
- suspend fun archive (
80
- id : Int ,
81
- archivedAt : LocalDateTime ? ,
82
- ) = transaction {
83
- Announcement .findByIdAndUpdate(id) {
84
- it.archivedAt = archivedAt ? : java.time.LocalDateTime .now().toKotlinLocalDateTime()
85
- }?.also (::updateLatestAnnouncement)
102
+ idLessEq() and archivedAtGreaterOrNullOrTrue() and hasTags()
103
+ }
104
+ }.orderBy(Announcements .id to SortOrder .DESC ).limit(count).toApiAnnouncement()
86
105
}
87
106
88
- suspend fun unarchive (id : Int ) = transaction {
89
- Announcement .findByIdAndUpdate(id) {
90
- it.archivedAt = null
91
- }?.also (::updateLatestAnnouncement)
107
+ suspend fun get (id : Int ) = transaction {
108
+ Announcement .findById(id).toApiResponseAnnouncement()
92
109
}
93
110
94
- suspend fun new (new : APIAnnouncement ) = transaction {
111
+ suspend fun new (new : ApiAnnouncement ) = transaction {
95
112
Announcement .new {
96
113
author = new.author
97
114
title = new.title
98
115
content = new.content
99
- channel = new.channel
100
116
archivedAt = new.archivedAt
101
117
level = new.level
102
- }.also { newAnnouncement ->
103
- new.attachmentUrls.map { newUrl ->
104
- suspendedTransactionAsync {
105
- Attachment .new {
106
- url = newUrl
107
- announcement = newAnnouncement
108
- }
118
+ tags = SizedCollection (
119
+ new.tags.map { tag -> Tag .find { Tags .name eq tag }.firstOrNull() ? : Tag .new { name = tag } },
120
+ )
121
+ }.apply {
122
+ new.attachments.map { attachmentUrl ->
123
+ Attachment .new {
124
+ url = attachmentUrl
125
+ announcement = this @apply
109
126
}
110
- }.awaitAll()
111
- }.also (::updateLatestAnnouncement)
127
+ }
128
+ }.let (::updateLatestAnnouncement)
112
129
}
113
130
114
- suspend fun update (id : Int , new : APIAnnouncement ) = transaction {
131
+ suspend fun update (id : Int , new : ApiAnnouncement ) = transaction {
115
132
Announcement .findByIdAndUpdate(id) {
116
133
it.author = new.author
117
134
it.title = new.title
118
135
it.content = new.content
119
- it.channel = new.channel
120
136
it.archivedAt = new.archivedAt
121
137
it.level = new.level
122
- }?.also { newAnnouncement ->
123
- newAnnouncement.attachments.map {
124
- suspendedTransactionAsync {
125
- it.delete()
126
- }
127
- }.awaitAll()
128
-
129
- new.attachmentUrls.map { newUrl ->
130
- suspendedTransactionAsync {
131
- Attachment .new {
132
- url = newUrl
133
- announcement = newAnnouncement
134
- }
138
+
139
+ // Get the old tags, create new tags if they don't exist,
140
+ // and delete tags that are not in the new tags, after updating the announcement.
141
+ val oldTags = it.tags.toList()
142
+ val updatedTags = new.tags.map { name ->
143
+ Tag .find { Tags .name eq name }.firstOrNull() ? : Tag .new { this .name = name }
144
+ }
145
+ it.tags = SizedCollection (updatedTags)
146
+ oldTags.forEach { tag ->
147
+ if (tag in updatedTags || ! tag.announcements.empty()) return @forEach
148
+ tag.delete()
149
+ }
150
+
151
+ // Delete old attachments and create new attachments.
152
+ it.attachments.forEach { attachment -> attachment.delete() }
153
+ new.attachments.map { attachment ->
154
+ Attachment .new {
155
+ url = attachment
156
+ announcement = it
135
157
}
136
- }.awaitAll()
137
- }?.also (::updateLatestAnnouncement)
158
+ }
159
+ }?.let (::updateLatestAnnouncement) ? : Unit
160
+ }
161
+
162
+ suspend fun delete (id : Int ) = transaction {
163
+ val announcement = Announcement .findById(id) ? : return @transaction
164
+
165
+ // Delete the tag if no other announcements are referencing it.
166
+ // One count means that the announcement is the only one referencing the tag.
167
+ announcement.tags.filter { tag -> tag.announcements.count() == 1L }.forEach { tag ->
168
+ latestAnnouncementByTag - = tag.id.value
169
+ tag.delete()
170
+ }
171
+
172
+ announcement.delete()
173
+
174
+ // If the deleted announcement is the latest announcement, set the new latest announcement.
175
+ if (latestAnnouncement?.id?.value == id) {
176
+ latestAnnouncement = Announcement .all().orderBy(Announcements .id to SortOrder .DESC ).firstOrNull()
177
+ }
178
+
179
+ // The new announcement may be the latest for a specific tag. Set the new latest announcement for that tag.
180
+ latestAnnouncementByTag.keys.forEach { tag ->
181
+ updateLatestAnnouncementForTag(tag)
182
+ }
183
+ }
184
+
185
+ suspend fun tags () = transaction {
186
+ Tag .all().toList().toApiTag()
138
187
}
139
188
140
189
private suspend fun <T > transaction (statement : suspend Transaction .() -> T ) =
@@ -144,7 +193,6 @@ internal class AnnouncementRepository {
144
193
val author = varchar(" author" , 32 ).nullable()
145
194
val title = varchar(" title" , 64 )
146
195
val content = text(" content" ).nullable()
147
- val channel = varchar(" channel" , 16 ).nullable()
148
196
val createdAt = datetime(" createdAt" ).defaultExpression(CurrentDateTime )
149
197
val archivedAt = datetime(" archivedAt" ).nullable()
150
198
val level = integer(" level" )
@@ -155,14 +203,27 @@ internal class AnnouncementRepository {
155
203
val announcement = reference(" announcement" , Announcements , onDelete = ReferenceOption .CASCADE )
156
204
}
157
205
206
+ private object Tags : IntIdTable() {
207
+ val name = varchar(" name" , 16 ).uniqueIndex()
208
+ }
209
+
210
+ private object AnnouncementTags : Table() {
211
+ val tag = reference(" tag" , Tags , onDelete = ReferenceOption .CASCADE )
212
+ val announcement = reference(" announcement" , Announcements , onDelete = ReferenceOption .CASCADE )
213
+
214
+ init {
215
+ uniqueIndex(tag, announcement)
216
+ }
217
+ }
218
+
158
219
class Announcement (id : EntityID <Int >) : IntEntity(id) {
159
220
companion object : IntEntityClass <Announcement >(Announcements )
160
221
161
222
var author by Announcements .author
162
223
var title by Announcements .title
163
224
var content by Announcements .content
164
225
val attachments by Attachment referrersOn Attachments .announcement
165
- var channel by Announcements .channel
226
+ var tags by Tag via AnnouncementTags
166
227
var createdAt by Announcements .createdAt
167
228
var archivedAt by Announcements .archivedAt
168
229
var level by Announcements .level
@@ -175,17 +236,32 @@ internal class AnnouncementRepository {
175
236
var announcement by Announcement referencedOn Attachments .announcement
176
237
}
177
238
178
- private fun Announcement.toApi () = APIResponseAnnouncement (
179
- id.value,
180
- author,
181
- title,
182
- content,
183
- attachments.map { it.url },
184
- channel,
185
- createdAt,
186
- archivedAt,
187
- level,
188
- )
239
+ class Tag (id : EntityID <Int >) : IntEntity(id) {
240
+ companion object : IntEntityClass <Tag >(Tags )
241
+
242
+ var name by Tags .name
243
+ var announcements by Announcement via AnnouncementTags
244
+ }
245
+
246
+ private fun Announcement?.toApiResponseAnnouncement () = this ?.let {
247
+ ApiResponseAnnouncement (
248
+ id.value,
249
+ author,
250
+ title,
251
+ content,
252
+ attachments.map { it.url },
253
+ tags.map { it.id.value },
254
+ createdAt,
255
+ archivedAt,
256
+ level,
257
+ )
258
+ }
259
+
260
+ private fun Iterable<Announcement>.toApiAnnouncement () = map { it.toApiResponseAnnouncement()!! }
261
+
262
+ private fun Iterable<Tag>.toApiTag () = map { ApiAnnouncementTag (it.id.value, it.name) }
263
+
264
+ private fun Int?.toApiResponseAnnouncementId () = this ?.let { ApiResponseAnnouncementId (this ) }
189
265
190
- private fun Int. toApi () = APIResponseAnnouncementId ( this )
266
+ private fun Iterable< Int?>. toApiResponseAnnouncementId () = map { it.toApiResponseAnnouncementId() }
191
267
}
0 commit comments