-
Notifications
You must be signed in to change notification settings - Fork 1
/
PostSender.kt
156 lines (146 loc) · 6.18 KB
/
PostSender.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package com.dluvian.voyage.data.interactor
import android.util.Log
import com.dluvian.nostr_kt.RelayUrl
import com.dluvian.nostr_kt.extractMentions
import com.dluvian.nostr_kt.getSubject
import com.dluvian.nostr_kt.secs
import com.dluvian.voyage.core.EventIdHex
import com.dluvian.voyage.core.MAX_TOPICS
import com.dluvian.voyage.core.PubkeyHex
import com.dluvian.voyage.core.SignerLauncher
import com.dluvian.voyage.core.Topic
import com.dluvian.voyage.core.createValidatedMainPost
import com.dluvian.voyage.core.extractCleanHashtags
import com.dluvian.voyage.core.getNormalizedTopics
import com.dluvian.voyage.data.event.ValidatedCrossPost
import com.dluvian.voyage.data.event.ValidatedReply
import com.dluvian.voyage.data.event.ValidatedRootPost
import com.dluvian.voyage.data.nostr.NostrService
import com.dluvian.voyage.data.provider.RelayProvider
import com.dluvian.voyage.data.room.dao.PostDao
import com.dluvian.voyage.data.room.dao.tx.PostInsertDao
import rust.nostr.protocol.Event
import rust.nostr.protocol.Nip19Profile
import rust.nostr.protocol.PublicKey
class PostSender(
private val nostrService: NostrService,
private val relayProvider: RelayProvider,
private val postInsertDao: PostInsertDao,
private val postDao: PostDao,
) {
private val tag = "PostSender"
suspend fun sendPost(
header: String,
body: String,
topics: List<Topic>,
signerLauncher: SignerLauncher
): Result<Event> {
val trimmedHeader = header.trim()
val trimmedBody = body.trim()
val concat = "$trimmedHeader $trimmedBody"
val mentions = extractMentionPubkeys(content = concat)
val allTopics = topics.toMutableList()
allTopics.addAll(extractCleanHashtags(content = concat))
return nostrService.publishPost(
subject = trimmedHeader,
content = trimmedBody,
topics = allTopics.distinct().take(MAX_TOPICS),
mentions = mentions,
relayUrls = relayProvider.getPublishRelays(publishTo = mentions),
signerLauncher = signerLauncher,
).onSuccess { event ->
val validatedPost = ValidatedRootPost(
id = event.id().toHex(),
pubkey = event.author().toHex(),
topics = event.getNormalizedTopics(limited = false),
subject = event.getSubject(),
content = event.content(),
createdAt = event.createdAt().secs(),
relayUrl = "", // We don't know which relay accepted this note
json = event.asJson(),
)
postInsertDao.insertRootPosts(posts = listOf(validatedPost))
}.onFailure {
Log.w(tag, "Failed to create post event", it)
}
}
suspend fun sendReply(
parentId: EventIdHex,
recipient: PubkeyHex,
body: String,
relayHint: RelayUrl,
signerLauncher: SignerLauncher,
): Result<Event> {
val trimmedBody = body.trim()
val mentions = (extractMentionPubkeys(content = trimmedBody) + recipient).distinct()
return nostrService.publishReply(
content = trimmedBody,
parentId = parentId,
mentions = mentions,
relayHint = relayHint,
relayUrls = relayProvider.getPublishRelays(publishTo = mentions),
signerLauncher = signerLauncher,
).onSuccess { event ->
val validatedReply = ValidatedReply(
id = event.id().toHex(),
pubkey = event.author().toHex(),
parentId = parentId,
content = event.content(),
createdAt = event.createdAt().secs(),
relayUrl = "", // We don't know which relay accepted this note
json = event.asJson(),
)
postInsertDao.insertReplies(replies = listOf(validatedReply))
}.onFailure {
Log.w(tag, "Failed to create reply event", it)
}
}
suspend fun sendCrossPost(
id: EventIdHex,
topics: List<Topic>,
signerLauncher: SignerLauncher
): Result<Event> {
val post = postDao.getPost(id = id)
?: return Result.failure(IllegalStateException("Post not found"))
val json = post.json
?: return Result.failure(IllegalStateException("Json not found"))
if (json.isEmpty()) return Result.failure(IllegalStateException("Json is empty"))
val crossPostedEvent = kotlin.runCatching { Event.fromJson(json) }.getOrNull()
?: return Result.failure(IllegalStateException("Json is not deserializable"))
if (post.crossPostedId != null) {
return Result.failure(IllegalStateException("Can't cross-post a cross-post"))
}
val validatedMainPost = createValidatedMainPost(
event = crossPostedEvent,
relayUrl = post.relayUrl
)
?: return Result.failure(IllegalStateException("Cross-posted event is invalid"))
return nostrService.publishCrossPost(
crossPostedEvent = crossPostedEvent,
topics = topics,
relayHint = post.relayUrl,
relayUrls = relayProvider.getPublishRelays(),
signerLauncher = signerLauncher,
).onSuccess { event ->
val validatedCrossPost = ValidatedCrossPost(
id = event.id().toHex(),
pubkey = event.author().toHex(),
topics = event.getNormalizedTopics(limited = false),
createdAt = event.createdAt().secs(),
relayUrl = "", // We don't know which relay accepted this note
crossPosted = validatedMainPost,
)
postInsertDao.insertCrossPosts(crossPosts = listOf(validatedCrossPost))
}.onFailure {
Log.w(tag, "Failed to create cross-post event", it)
}
}
private fun extractMentionPubkeys(content: String): List<PubkeyHex> {
return extractMentions(content = content)
.mapNotNull {
runCatching { PublicKey.fromBech32(it).toHex() }.getOrNull()
?: kotlin.runCatching { Nip19Profile.fromBech32(it).publicKey().toHex() }
.getOrNull()
}.distinct()
}
}