From aaf92caa70db6bffdf829babade4a6abf262bf92 Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 07:48:22 +0700
Subject: [PATCH 1/8] feat: Ty
---
.../com/aliucord/gradle/repo/Authors.kt | 1 +
plugins/ActionAlerts/README.md | 3 +
plugins/ActionAlerts/build.gradle.kts | 18 +
.../ActionAlerts/src/main/AndroidManifest.xml | 2 +
.../plugins/actionalerts/ActionAlerts.kt | 53 ++
plugins/CallTime/README.md | 3 +
plugins/CallTime/build.gradle.kts | 25 +
plugins/CallTime/src/main/AndroidManifest.xml | 2 +
.../tech/tyman/plugins/calltime/CallTime.kt | 83 ++++
plugins/EncryptDMs/README.md | 24 +
plugins/EncryptDMs/build.gradle.kts | 18 +
.../EncryptDMs/src/main/AndroidManifest.xml | 2 +
.../tyman/plugins/encryptdms/EncryptDMs.kt | 468 ++++++++++++++++++
.../tyman/plugins/encryptdms/Encryption.kt | 98 ++++
.../tech/tyman/plugins/encryptdms/Utils.kt | 37 ++
plugins/ReverseImageSearch/README.md | 3 +
plugins/ReverseImageSearch/build.gradle.kts | 24 +
.../src/main/AndroidManifest.xml | 2 +
.../plugins/reverseimagesearch/EngineSheet.kt | 40 ++
.../reverseimagesearch/ReverseImageSearch.kt | 87 ++++
.../reverseimagesearch/SearchSettings.kt | 52 ++
.../drawable/ic_baseline_image_search_24.xml | 5 +
plugins/RoleColorContrast/README.md | 11 +
plugins/RoleColorContrast/build.gradle.kts | 23 +
.../src/main/AndroidManifest.xml | 2 +
.../rolecolorcontrast/RoleColorContrast.kt | 131 +++++
.../plugins/rolecolorcontrast/Settings.kt | 29 ++
plugins/ShowNames/README.md | 3 +
plugins/ShowNames/build.gradle.kts | 30 ++
.../ShowNames/src/main/AndroidManifest.xml | 2 +
.../tech/tyman/plugins/shownames/ShowNames.kt | 49 ++
plugins/Texter/README.md | 3 +
plugins/Texter/build.gradle.kts | 25 +
plugins/Texter/src/main/AndroidManifest.xml | 2 +
.../kotlin/tech/tyman/plugins/texter/Maps.kt | 348 +++++++++++++
.../tech/tyman/plugins/texter/Texter.kt | 103 ++++
.../kotlin/tech/tyman/plugins/texter/Utils.kt | 16 +
plugins/Translate/README.md | 3 +
plugins/Translate/build.gradle.kts | 39 ++
.../Translate/src/main/AndroidManifest.xml | 2 +
.../tyman/plugins/translate/PluginSettings.kt | 55 ++
.../tech/tyman/plugins/translate/Translate.kt | 234 +++++++++
.../tech/tyman/plugins/translate/Utils.kt | 171 +++++++
43 files changed, 2331 insertions(+)
create mode 100644 plugins/ActionAlerts/README.md
create mode 100644 plugins/ActionAlerts/build.gradle.kts
create mode 100644 plugins/ActionAlerts/src/main/AndroidManifest.xml
create mode 100644 plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt
create mode 100644 plugins/CallTime/README.md
create mode 100644 plugins/CallTime/build.gradle.kts
create mode 100644 plugins/CallTime/src/main/AndroidManifest.xml
create mode 100644 plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt
create mode 100644 plugins/EncryptDMs/README.md
create mode 100644 plugins/EncryptDMs/build.gradle.kts
create mode 100644 plugins/EncryptDMs/src/main/AndroidManifest.xml
create mode 100644 plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt
create mode 100644 plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt
create mode 100644 plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt
create mode 100644 plugins/ReverseImageSearch/README.md
create mode 100644 plugins/ReverseImageSearch/build.gradle.kts
create mode 100644 plugins/ReverseImageSearch/src/main/AndroidManifest.xml
create mode 100644 plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt
create mode 100644 plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
create mode 100644 plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt
create mode 100644 plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml
create mode 100644 plugins/RoleColorContrast/README.md
create mode 100644 plugins/RoleColorContrast/build.gradle.kts
create mode 100644 plugins/RoleColorContrast/src/main/AndroidManifest.xml
create mode 100644 plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt
create mode 100644 plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt
create mode 100644 plugins/ShowNames/README.md
create mode 100644 plugins/ShowNames/build.gradle.kts
create mode 100644 plugins/ShowNames/src/main/AndroidManifest.xml
create mode 100644 plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt
create mode 100644 plugins/Texter/README.md
create mode 100644 plugins/Texter/build.gradle.kts
create mode 100644 plugins/Texter/src/main/AndroidManifest.xml
create mode 100644 plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt
create mode 100644 plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt
create mode 100644 plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt
create mode 100644 plugins/Translate/README.md
create mode 100644 plugins/Translate/build.gradle.kts
create mode 100644 plugins/Translate/src/main/AndroidManifest.xml
create mode 100644 plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt
create mode 100644 plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt
create mode 100644 plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt
diff --git a/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt b/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt
index 0e5716a..d77ca95 100644
--- a/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt
+++ b/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt
@@ -38,6 +38,7 @@ object Authors {
val oSumAtrIX = Author("oSumAtrIX", 737323631117598811L)
val RazerTexz = Author("RazerTexz", 633565155501801472L)
val LampDelivery = Author("LampDelivery", 650805815623680030L)
+ val Ty = author("Ty", 487443883127472129L)
}
/**
diff --git a/plugins/ActionAlerts/README.md b/plugins/ActionAlerts/README.md
new file mode 100644
index 0000000..2085659
--- /dev/null
+++ b/plugins/ActionAlerts/README.md
@@ -0,0 +1,3 @@
+# ShowNames
+
+A plugin that changes the color of usernames to stop them from blending into the background.
\ No newline at end of file
diff --git a/plugins/ActionAlerts/build.gradle.kts b/plugins/ActionAlerts/build.gradle.kts
new file mode 100644
index 0000000..ad0bf0a
--- /dev/null
+++ b/plugins/ActionAlerts/build.gradle.kts
@@ -0,0 +1,18 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.0.0"
+description = "A plugin that alerts you for certain actions (ie being banned/kicked from a server, friend removals)"
+
+aliucord {
+ changelog.set("""
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/ActionAlerts/src/main/AndroidManifest.xml b/plugins/ActionAlerts/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/ActionAlerts/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt b/plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt
new file mode 100644
index 0000000..3160f83
--- /dev/null
+++ b/plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt
@@ -0,0 +1,53 @@
+package tech.tyman.plugins.actionalerts
+
+import android.content.Context
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.api.NotificationsAPI
+import com.aliucord.entities.NotificationData
+import com.aliucord.entities.Plugin
+import com.aliucord.utils.RxUtils.await
+import com.aliucord.utils.RxUtils.subscribe
+import com.discord.gateway.GatewaySocket
+import com.discord.models.domain.ModelUserRelationship
+import com.discord.models.user.CoreUser
+import com.discord.restapi.RestAPIParams
+import com.discord.stores.StoreBans
+import com.discord.stores.StoreStream
+import com.discord.stores.StoreUserRelationships
+import com.discord.utilities.rest.RestAPI
+import rx.Subscription
+
+@AliucordPlugin
+class ActionAlerts : Plugin() {
+ private var relationshipCache = mutableMapOf()
+ private lateinit var relationshipListener: Subscription
+
+ override fun start(context: Context) {
+ relationshipListener = StoreStream.getUserRelationships().observe().subscribe {
+ if (relationshipCache.size > this.size) {
+ val removed = relationshipCache.entries.find {
+ !this.keys.contains(it.key)
+ } ?: return@subscribe
+ val user = StoreStream.getUsers().users[removed.key] ?: CoreUser(
+ RestAPI.getApi().userGet(removed.key).await().first ?: return@subscribe
+ )
+ if (removed.value != ModelUserRelationship.TYPE_FRIEND) {
+ NotificationsAPI.display(
+ NotificationData().apply {
+ title = "ActionAlerts"
+ body = "${user.username}#${String.format("%04d", user.discriminator)} removed you as a friend."
+ autoDismissPeriodSecs = 5
+ onClick = {}
+ }
+ )
+ }
+ }
+ relationshipCache = this
+ }
+ }
+
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ relationshipListener.unsubscribe()
+ }
+}
\ No newline at end of file
diff --git a/plugins/CallTime/README.md b/plugins/CallTime/README.md
new file mode 100644
index 0000000..79a6151
--- /dev/null
+++ b/plugins/CallTime/README.md
@@ -0,0 +1,3 @@
+# CallTime
+
+A plugin that shows the time you have been in a VC for
\ No newline at end of file
diff --git a/plugins/CallTime/build.gradle.kts b/plugins/CallTime/build.gradle.kts
new file mode 100644
index 0000000..d4d930d
--- /dev/null
+++ b/plugins/CallTime/build.gradle.kts
@@ -0,0 +1,25 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.0.3"
+description = "A plugin that shows how long you have been in a VC for."
+
+aliucord {
+ changelog.set("""
+ # Version 1.0.3
+ * Updated for new discord/aliucord version
+ # Version 1.0.2
+ * Fix a crash
+ * Fix seconds bugging out with long times
+ # Version 1.0.1
+ * Fix ending timer on mute/deafen
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/CallTime/src/main/AndroidManifest.xml b/plugins/CallTime/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/CallTime/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt b/plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt
new file mode 100644
index 0000000..ecf01f7
--- /dev/null
+++ b/plugins/CallTime/src/main/kotlin/tech/tyman/plugins/calltime/CallTime.kt
@@ -0,0 +1,83 @@
+package tech.tyman.plugins.calltime
+
+import android.content.Context
+import com.aliucord.Utils
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.api.PatcherAPI
+import com.aliucord.entities.Plugin
+import com.aliucord.patcher.Hook
+import com.discord.gateway.io.OutgoingPayload
+import com.discord.rtcconnection.RtcConnection
+import com.discord.stores.StoreRtcConnection
+import com.discord.utilities.streams.StreamContext
+import com.discord.utilities.time.ClockFactory
+import com.discord.utilities.voice.VoiceViewUtils
+import com.discord.widgets.status.WidgetGlobalStatusIndicator
+import com.discord.widgets.status.WidgetGlobalStatusIndicatorViewModel
+import java.util.*
+import java.util.concurrent.TimeUnit
+import kotlin.concurrent.schedule
+
+@AliucordPlugin
+class CallTime : Plugin() {
+ private var vcConnectedTime: Long? = null
+ private var cachedVoiceState: OutgoingPayload.VoiceStateUpdate? = null
+ private val timer = Timer()
+ private var timerTask: TimerTask? = null
+ private val clock = ClockFactory.get()
+ private val elapsedTime: String
+ get() = run {
+ val vcConnectedTime = vcConnectedTime ?: return "00:00:00"
+ val duration = clock.currentTimeMillis() - vcConnectedTime
+ return String.format(
+ "%02d:%02d:%02d",
+ TimeUnit.MILLISECONDS.toHours(duration),
+ TimeUnit.MILLISECONDS.toMinutes(duration) % TimeUnit.HOURS.toMinutes(1),
+ TimeUnit.MILLISECONDS.toSeconds(duration) % TimeUnit.MINUTES.toSeconds(1)
+ )
+ }
+
+ override fun start(ctx: Context) {
+ patcher.patchConnectedText()
+ }
+
+ private fun PatcherAPI.patchConnectedText() {
+ val currentVoiceStateField = StoreRtcConnection::class.java.getDeclaredField("currentVoiceState").apply { isAccessible = true }
+ val setupIndicatorStatusMethod = WidgetGlobalStatusIndicator::class.java.getDeclaredMethod("setupIndicatorStatus", WidgetGlobalStatusIndicatorViewModel.ViewState.CallOngoing::class.java).apply { isAccessible = true }
+ val getConnectedTextMethod = VoiceViewUtils::class.java.getDeclaredMethod("getConnectedText", Context::class.java, RtcConnection.State::class.java, StreamContext::class.java, Boolean::class.javaPrimitiveType)
+
+ patch(setupIndicatorStatusMethod, Hook {
+ val widget = it.thisObject as WidgetGlobalStatusIndicator
+ timerTask?.cancel()
+ timerTask = timer.schedule(1000) {
+ widget.activity?.runOnUiThread {
+ widget.view ?: return@runOnUiThread
+ setupIndicatorStatusMethod.invoke(widget, it.args[0])
+ }
+ }
+ })
+
+ patch(StoreRtcConnection::class.java.getDeclaredMethod("onVoiceStateUpdated"), Hook {
+ val store = it.thisObject as StoreRtcConnection
+ val currentVoiceState = currentVoiceStateField[store] as OutgoingPayload.VoiceStateUpdate?
+ ?: return@Hook
+ if (currentVoiceState.channelId != null && cachedVoiceState?.channelId != currentVoiceState.channelId) {
+ vcConnectedTime = clock.currentTimeMillis()
+ } else if (currentVoiceState.channelId == null && cachedVoiceState?.channelId != null) {
+ timerTask?.cancel()
+ timerTask = null
+ if (vcConnectedTime != null) Utils.showToast("Call ended. Elapsed time: $elapsedTime", true)
+ vcConnectedTime = null
+ }
+ cachedVoiceState = currentVoiceState
+ })
+ patch(getConnectedTextMethod, Hook {
+ it.result = "${it.result} ($elapsedTime)"
+ })
+
+ }
+
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ }
+}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/README.md b/plugins/EncryptDMs/README.md
new file mode 100644
index 0000000..b3d9ed7
--- /dev/null
+++ b/plugins/EncryptDMs/README.md
@@ -0,0 +1,24 @@
+# EncryptDMs
+
+Encrypts your dms
+
+## How it works
+There are 5 keys:
+- RSA-4096 public and private (for encrpting the AES key)
+- RSA-4096 public and private (for signing messages)
+- AES-265 (
+
+### Initial setup
+1. Both users send their RSA public keys (both encrytion and signing)
+2. Both users save the other's RSA public key (both encrytion and signing)
+3. User 1 generates an AES-256 key
+4. User 1 encrypts the AES key with User 2's public encryption key, and then signs with User 1's private signing key
+5. User 1 sends encrypted message
+6. User 2 verifies the encrypted AES key with User 1's public signing key and then decrypts with User 2's private encryption key and saves the decrypted AES key
+7. Both users now have the AES key
+
+### DM encryption
+1. User 1 encrypts message with AES-256 key
+2. User 1 signs the AES encrypted message with their RSA private signing key
+3. User 1 sends encrypted message
+4. User 2 verifies the signature with User 1's public signing key and then decrypts with the saved AES-256 key
diff --git a/plugins/EncryptDMs/build.gradle.kts b/plugins/EncryptDMs/build.gradle.kts
new file mode 100644
index 0000000..7b1b914
--- /dev/null
+++ b/plugins/EncryptDMs/build.gradle.kts
@@ -0,0 +1,18 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "0.0.1"
+description = "Encryption go brrrrrrrrrrrrr"
+
+aliucord {
+ changelog.set("""
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/AndroidManifest.xml b/plugins/EncryptDMs/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/EncryptDMs/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt
new file mode 100644
index 0000000..497f5c2
--- /dev/null
+++ b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt
@@ -0,0 +1,468 @@
+package tech.tyman.plugins.encryptdms
+
+import android.content.Context
+import android.view.View
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.entities.Plugin
+import com.aliucord.patcher.after
+import com.discord.widgets.channels.list.WidgetChannelsListItemChannelActions
+import com.lytefast.flexinput.R
+import com.aliucord.wrappers.ChannelWrapper.Companion.id
+import android.widget.TextView
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
+import com.discord.databinding.WidgetChannelsListItemActionsBinding
+import androidx.core.widget.NestedScrollView
+import com.aliucord.Utils
+import com.aliucord.api.CommandsAPI
+import com.aliucord.api.PatcherAPI
+import com.aliucord.api.SettingsAPI
+import com.aliucord.patcher.before
+import com.aliucord.settings.delegate
+import com.aliucord.utils.DimenUtils
+import com.aliucord.wrappers.ChannelWrapper.Companion.recipients
+import com.discord.api.commands.ApplicationCommandType
+import com.discord.api.message.LocalAttachment
+import com.discord.api.message.Message
+import com.discord.api.message.attachment.MessageAttachment
+import com.discord.databinding.WidgetChatListActionsBinding
+import com.discord.models.user.CoreUser
+import com.discord.stores.StoreStream
+import com.discord.utilities.SnowflakeUtils
+import com.discord.utilities.attachments.AttachmentUtilsKt
+import com.discord.widgets.chat.MessageContent
+import com.discord.widgets.chat.MessageManager
+import com.discord.widgets.chat.input.ChatInputViewModel
+import com.discord.widgets.chat.list.actions.WidgetChatListActions
+import com.discord.models.message.Message as ModelMessage
+import com.lytefast.flexinput.model.Attachment
+import java.io.File
+
+@AliucordPlugin
+class EncryptDMs : Plugin() {
+ // AES keys
+ private var SettingsAPI.aesKeys: HashMap by settings.delegate(HashMap())
+
+ private fun SettingsAPI.getKey(channel: Long) = this.aesKeys[channel.toString()]
+ private fun SettingsAPI.setKey(channel: Long, key: String) {
+ val keys = aesKeys.clone() as HashMap
+ keys[channel.toString()] = key
+ this.aesKeys = keys
+ }
+
+ // RSA keys (encryption)
+ private var SettingsAPI.pubKeys: HashMap by settings.delegate(HashMap())
+ private var SettingsAPI.publicKey: String by settings.delegate("")
+ private var SettingsAPI.privateKey: String by settings.delegate("")
+
+ private fun SettingsAPI.getPubKey(user: Long) = this.pubKeys[user.toString()]
+ private fun SettingsAPI.setPubKey(user: Long, key: String) {
+ val keys = this.pubKeys.clone() as HashMap
+ keys[user.toString()] = key
+ this.pubKeys = keys
+ }
+
+ // RSA keys (signing)
+ private var SettingsAPI.pubSigningKeys: HashMap by settings.delegate(HashMap())
+ private var SettingsAPI.publicSigningKey: String by settings.delegate("")
+ private var SettingsAPI.privateSigningKey: String by settings.delegate("")
+
+ private fun SettingsAPI.getPubSigningKey(user: Long) = this.pubSigningKeys[user.toString()]
+ private fun SettingsAPI.setPubSigningKey(user: Long, key: String) {
+ val keys = this.pubSigningKeys.clone() as HashMap
+ keys[user.toString()] = key
+ this.pubSigningKeys = keys
+ }
+
+ // Generic settings
+ private var SettingsAPI.enabledChannels: ArrayList by settings.delegate(arrayListOf())
+
+ private fun SettingsAPI.enableChannel(channel: Long) {
+ val channels = this.enabledChannels.clone() as ArrayList
+ channels.add(channel)
+ this.enabledChannels = channels
+ }
+ private fun SettingsAPI.disableChannel(channel: Long) {
+ val channels = this.enabledChannels.clone() as ArrayList
+ channels.remove(channel)
+ this.enabledChannels = channels
+ }
+
+ private val curChannel: Long
+ get() = StoreStream.getChannelsSelected().id
+
+ override fun start(context: Context) {
+ // Patch context menus
+ patcher.patchDirectMessageChannelActions()
+ patcher.patchMessageActions()
+ // Patch sent messages
+ patcher.patchSentMessages()
+ // Patch incoming messages
+ patcher.patchIncomingMessages()
+
+ // Add commands
+ commands.registerCommand(
+ "sendpubkeys",
+ "Sends your public keys (both encryption and signing) in the chat, and if either private or public keys are absent then regenerates them"
+ ) {
+ // Encryption keys
+ var publicKey = settings.publicKey
+ if (publicKey == "" || settings.privateKey == "") {
+ genRsaKeys().run {
+ settings.publicKey = public.asBase64()
+ settings.privateKey = private.asBase64()
+ publicKey = public.asBase64()
+ }
+ }
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(publicKey)
+ it.addAttachment(
+ this.toURI().toASCIIString(),
+ "publickey.txt"
+ )
+ }
+ // Signing keys
+ var publicSigningKey = settings.publicSigningKey
+ if (publicSigningKey == "" || settings.privateSigningKey == "") {
+ genRsaKeys().run {
+ settings.publicSigningKey = public.asBase64()
+ settings.privateSigningKey = private.asBase64()
+ publicSigningKey = public.asBase64()
+ }
+ }
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(publicSigningKey)
+ it.addAttachment(
+ this.toURI().toASCIIString(),
+ "publicsigningkey.txt"
+ )
+ }
+ return@registerCommand CommandsAPI.CommandResult(
+ "",
+ null,
+ true
+ )
+ }
+
+ commands.registerCommand(
+ "sendkey",
+ "Sends your generated channel key in the chat, encrypted with the other user's public key, so that the other user can decrypt and save it.",
+ listOf(
+ Utils.createCommandOption(
+ type = ApplicationCommandType.STRING,
+ name = "channel id",
+ description = "The channel id of the DM",
+ required = true
+ ),
+ Utils.createCommandOption(
+ type = ApplicationCommandType.USER,
+ name = "user",
+ description = "The user to send this to",
+ required = true
+ )
+ )
+ ) {
+ val userPubKey = settings.getPubKey(it.getRequiredUser("user").id)
+ ?: return@registerCommand CommandsAPI.CommandResult("This user's public key is not saved, ask them to run /sendpubkey, and then press and hold on their message to save their key!")
+ val channelKey = settings.getKey(it.getRequiredString("channel id").toLong())
+ ?: return@registerCommand CommandsAPI.CommandResult("This DM does not have a key generated! Press and hold on the DM in the list, and generate the key.")
+
+ val encryptedText = userPubKey.asPublicKey().encrypt(channelKey.asKey().encoded)
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(encryptedText)
+ it.addAttachment(
+ this.toURI().toASCIIString(),
+ "aeskey.txt"
+ )
+ }
+ var privateSigningKey = settings.privateSigningKey
+ if (privateSigningKey == "" || settings.publicSigningKey == "") {
+ genRsaKeys().run {
+ settings.publicSigningKey = public.asBase64()
+ settings.privateSigningKey = private.asBase64()
+ privateSigningKey = private.asBase64()
+ }
+ }
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(privateSigningKey.asPrivateKey().sign(encryptedText))
+ it.addAttachment(
+ this.toURI().toASCIIString(),
+ "signature.txt"
+ )
+ }
+
+ return@registerCommand CommandsAPI.CommandResult(
+ "",
+ null,
+ true
+ )
+ }
+ }
+
+ private fun PatcherAPI.patchDirectMessageChannelActions() {
+ this.after("configureUI", WidgetChannelsListItemChannelActions.Model::class.java) {
+ val channelId = (it.args[0] as WidgetChannelsListItemChannelActions.Model).channel.id
+
+ val state =
+ if (settings.getKey(channelId) == null) ChannelState.NO_KEY
+ else if (settings.enabledChannels.contains(channelId)) ChannelState.ENABLED
+ else ChannelState.DISABLED
+
+ val nestedScrollView = this.requireView() as NestedScrollView
+ val layout = nestedScrollView.getChildAt(0) as LinearLayout
+ val binding = com.discord.widgets.channels.list.WidgetChannelsListItemChannelActions::class.java.getDeclaredMethod("getBinding")
+ .apply { isAccessible = true }
+ .invoke(this) as WidgetChannelsListItemActionsBinding
+ val view = binding.j
+ val param = view.layoutParams
+ val params = LinearLayout.LayoutParams(param.width, param.height)
+ params.leftMargin = DimenUtils.dpToPx(20)
+ val tw = TextView(view.context, null, 0, R.i.UiKit_Settings_Item_Icon)
+ tw.text = when (state) {
+ ChannelState.NO_KEY -> "Setup encrypted DMs (EncryptDMs)"
+ ChannelState.DISABLED -> "Enable encrypted DMs (EncryptDMs)"
+ ChannelState.ENABLED -> "Disable encrypted DMs (EncryptDMs)"
+ }
+ tw.setCompoundDrawablesRelativeWithIntrinsicBounds(ContextCompat.getDrawable(view.context, R.e.avd_show_password), null, null, null)
+ tw.layoutParams = view.layoutParams
+ tw.id = View.generateViewId()
+ tw.setOnClickListener {
+ when (state) {
+ ChannelState.NO_KEY -> {
+ val key = genAesKey().asHex()
+ settings.setKey(channelId, key)
+ Utils.showToast("Key saved for channel. To send this key to the other user, tell the other user to run the /sendpubkey command, press and hold their message, and then run the /sendkey command.", true)
+ this.dismiss()
+ }
+ ChannelState.DISABLED -> {
+ settings.enableChannel(channelId)
+ Utils.showToast("Enabled encrypted DMs for this channel! Sent messages will now be encrypted.")
+ this.dismiss()
+ }
+ ChannelState.ENABLED -> {
+ settings.disableChannel(channelId)
+ Utils.showToast("Disabled encrypted DMs for this channel. Sent messages will no longer be encrypted.")
+ this.dismiss()
+ }
+ }
+ }
+ layout.addView(tw)
+ }
+ }
+
+ private fun PatcherAPI.patchMessageActions() {
+ this.after("configureUI", WidgetChatListActions.Model::class.java) {
+ val message = (it.args[0] as WidgetChatListActions.Model).message
+ val userId = CoreUser(message.author).id
+ val content = message.content
+ if (
+ !listOf(
+ "",
+ ""
+ ).contains(content)
+ ) return@after // If message is not relevant ignore it
+ val nestedScrollView = this.requireView() as NestedScrollView
+ val layout = nestedScrollView.getChildAt(0) as LinearLayout
+ val binding = WidgetChatListActions::class.java.getDeclaredMethod("getBinding")
+ .apply { isAccessible = true }
+ .invoke(this) as WidgetChatListActionsBinding
+ val view = binding.j
+ val param = view.layoutParams
+ val params = LinearLayout.LayoutParams(param.width, param.height)
+ params.leftMargin = DimenUtils.dpToPx(20)
+ val tw = TextView(view.context, null, 0, R.i.UiKit_Settings_Item_Icon)
+ tw.text = when (content) {
+ "" -> "Save public keys for user (EncryptDMs)"
+ "" -> "Save channel key from user (EncryptDMs)"
+ else -> "This should never happen wtf"
+ }
+ tw.setCompoundDrawablesRelativeWithIntrinsicBounds(ContextCompat.getDrawable(view.context, R.e.avd_show_password), null, null, null)
+ tw.layoutParams = view.layoutParams
+ tw.id = View.generateViewId()
+ tw.setOnClickListener {
+ when (content) {
+ "" -> Utils.threadPool.execute {
+ val key = message.getAttachmentText("publickey.txt") ?: return@execute
+ settings.setPubKey(userId, key)
+ val signingKey = message.getAttachmentText("publicsigningkey.txt") ?: return@execute
+ settings.setPubSigningKey(userId, signingKey)
+ Utils.showToast("Public keys saved for user.")
+ this.dismiss()
+ }
+ "" -> Utils.threadPool.execute {
+ // Encrypted text
+ val encryptedText = message.getAttachmentText("aeskey.txt") ?: return@execute
+ // Signature verification
+ val signature = message.getAttachmentText("signature.txt") ?: return@execute
+ val publicSigningKey = settings.getPubSigningKey(userId)
+ ?: return@execute Utils.showToast("Public signing key for user was not found, make sure you have saved their public keys first!", true)
+ val verified = try {
+ publicSigningKey.asPublicKey().verify(encryptedText, signature)
+ } catch (e: Exception) {
+ logger.error("Verification error:", e)
+ Utils.showToast("Error occurred while verifying signature, message could be modified, not saving key", true)
+ return@execute
+ }
+ if (!verified) {
+ Utils.showToast("Signature verification failed, message could be modified, not saving key", true)
+ return@execute
+ }
+ val key = settings.privateKey.asPrivateKey()
+ settings.setKey(message.channelId, key.decrypt(encryptedText))
+ Utils.showToast("Key decrypted and saved for this channel.")
+ this.dismiss()
+ }
+ }
+ }
+ layout.addView(tw)
+ }
+ }
+
+ private val textContentField =
+ MessageContent::class.java.getDeclaredField("textContent").apply {
+ isAccessible = true
+ }
+
+ private fun MessageContent.set(text: String) = textContentField.set(this, text)
+
+ private fun PatcherAPI.patchSentMessages() {
+ this.before(
+ "sendMessage",
+ Context::class.java,
+ MessageManager::class.java,
+ MessageContent::class.java,
+ List::class.java,
+ Boolean::class.javaPrimitiveType!!,
+ Function1::class.java
+ ) {
+ if (!settings.enabledChannels.contains(curChannel)) return@before
+ val recipients = StoreStream.getChannelsSelected().selectedChannel.recipients
+ ?: return@before // This is not useless fuck you android studio
+ if (recipients.size != 1) return@before
+ val content = it.args[2] as MessageContent
+ val context = it.args[0] as Context
+ @Suppress("UNCHECKED_CAST")
+ val attachments = (it.args[3] as List>).toMutableList()
+ val plainText = content.textContent
+
+ // Encrypt with AES
+ val key = settings.getKey(curChannel)
+ ?: run {
+ Utils.showToast("There is no key saved for this DM! Make sure to properly set it up with the other user first!", true)
+ it.result = null
+ return@before
+ }
+ val encryptedText = key.asKey().encrypt(plainText)
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(encryptedText.second)
+ attachments.add(
+ AttachmentUtilsKt.toAttachment(
+ LocalAttachment(
+ SnowflakeUtils.fromTimestamp(System.currentTimeMillis()),
+ this.toURI().toASCIIString(),
+ "message.txt"
+ )
+ )
+ )
+ }
+ // Add initialization vector because encryption
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(encryptedText.first.asHex())
+ attachments.add(
+ AttachmentUtilsKt.toAttachment(
+ LocalAttachment(
+ SnowflakeUtils.fromTimestamp(System.currentTimeMillis()),
+ this.toURI().toASCIIString(),
+ "iv.txt"
+ )
+ )
+ )
+ }
+ // Sign with RSA
+ var privateSigningKey = settings.privateSigningKey
+ if (privateSigningKey == "" || settings.publicSigningKey == "") {
+ genRsaKeys().run {
+ settings.publicSigningKey = public.asBase64()
+ settings.privateSigningKey = private.asBase64()
+ privateSigningKey = private.asBase64()
+ }
+ }
+ with (File.createTempFile("encryptdms", null, context.cacheDir)) {
+ this.writeText(privateSigningKey.asPrivateKey().sign(encryptedText.second))
+ attachments.add(
+ AttachmentUtilsKt.toAttachment(
+ LocalAttachment(
+ SnowflakeUtils.fromTimestamp(System.currentTimeMillis()),
+ this.toURI().toASCIIString(),
+ "signature.txt"
+ )
+ )
+ )
+ }
+ // Set attachments and content
+ it.args[3] = attachments
+ content.set("")
+ it.args[2] = content
+ }
+ }
+
+ private val contentField = Message::class.java.getDeclaredField("content").apply { isAccessible = true }
+ private val attachmentsField = Message::class.java.getDeclaredField("attachments").apply { isAccessible = true }
+
+ private fun PatcherAPI.patchIncomingMessages() {
+ // Hide attachments if message is encrypted
+ patcher.before(
+ "handleMessageCreate",
+ Message::class.java
+ ) {
+ val apiMessage = it.args[0] as Message
+ val message = ModelMessage(apiMessage)
+ if (message.content != "") return@before
+ Utils.threadPool.execute {
+ val response = run {
+ // Encrypted text
+ val encryptedText = message.getAttachmentTextWithError("message.txt")
+ if (!encryptedText.first) return@run encryptedText.second
+ // Signature verification
+ val signature = message.getAttachmentTextWithError("signature.txt")
+ if (!signature.first) return@run signature.second
+ val publicSigningKey = settings.getPubSigningKey(CoreUser(message.author).id)
+ ?: return@run "Public signing key for user was not found, make sure you have saved their public keys first!"
+ val verified = try {
+ publicSigningKey.asPublicKey().verify(encryptedText.second, signature.second)
+ } catch (e: Exception) {
+ logger.error("Verification error:", e)
+ return@run "Error occurred while verifying signature, message could be modified"
+ }
+ if (!verified) {
+ return@run "Signature verification failed, message could be modified, not saving key"
+ }
+ // Initialization vector
+ val iv = message.getAttachmentTextWithError("iv.txt")
+ if (!iv.first) return@run iv.second
+ // Decrypt
+ val key = settings.getKey(message.channelId)
+ ?: return@run "Key for channel not found, make sure it has been sent by the other user and saved by you!"
+ return@run key.asKey().decrypt(encryptedText.second, iv.second.asByteArray())
+ }
+ contentField[apiMessage] = response
+ @Suppress("UNCHECKED_CAST")
+ val attachments = attachmentsField[apiMessage] as MutableList
+ attachments.clear()
+ StoreStream.`access$handleMessageCreate`(this, apiMessage)
+ }
+ it.result = null
+ }
+ }
+
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ commands.unregisterAll()
+ }
+}
+
+enum class ChannelState {
+ NO_KEY,
+ DISABLED,
+ ENABLED
+}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt
new file mode 100644
index 0000000..2dd0e37
--- /dev/null
+++ b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt
@@ -0,0 +1,98 @@
+package tech.tyman.plugins.encryptdms
+
+import android.util.Base64
+import java.security.*
+import java.security.spec.PKCS8EncodedKeySpec
+import java.security.spec.X509EncodedKeySpec
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+fun genAesKey(): SecretKey {
+ val keyGenerator = KeyGenerator.getInstance("AES")
+ keyGenerator.init(256)
+ return keyGenerator.generateKey()
+}
+
+fun Key.asHex() = this.encoded.asHex()
+fun ByteArray.asHex() = this.joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
+fun Key.asBase64() = Base64.encodeToString(this.encoded, Base64.DEFAULT)
+
+fun String.asByteArray() = this.chunked(2)
+ .map { it.toInt(16).toByte() }
+ .toByteArray()
+fun String.asKey(): SecretKey {
+ val encodedKey = this.asByteArray()
+ return SecretKeySpec(encodedKey, 0, encodedKey.size, "AES")
+}
+
+fun genRsaKeys(): KeyPair {
+ val generator = KeyPairGenerator.getInstance("RSA")
+ generator.initialize(4096)
+ return generator.generateKeyPair()
+}
+
+fun String.asPublicKey() =
+ KeyFactory.getInstance("RSA").generatePublic(X509EncodedKeySpec(Base64.decode(this, Base64.DEFAULT)))
+
+fun String.asPrivateKey() =
+ KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(Base64.decode(this, Base64.DEFAULT)))
+
+fun PublicKey.encrypt(text: ByteArray): String {
+ val cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING")
+ cipher.init(Cipher.PUBLIC_KEY, this)
+ return Base64.encodeToString(cipher.doFinal(text), Base64.DEFAULT)
+}
+
+fun PrivateKey.decrypt(text: String): String {
+ val cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING")
+ cipher.init(Cipher.PRIVATE_KEY, this)
+ return cipher.doFinal(Base64.decode(text, Base64.DEFAULT)).asHex()
+}
+
+fun PrivateKey.sign(text: String): String {
+ val signature = Signature.getInstance("SHA512WithRSA")
+ signature.initSign(this)
+ signature.update(text.toByteArray())
+ return Base64.encodeToString(signature.sign(), Base64.DEFAULT)
+}
+
+fun PublicKey.verify(text: String, signature: String): Boolean {
+ val signatureInstance = Signature.getInstance("SHA512WithRSA")
+ signatureInstance.initVerify(this)
+ signatureInstance.update(text.toByteArray())
+
+ return signatureInstance.verify(Base64.decode(signature, Base64.DEFAULT))
+}
+
+fun SecretKey.encrypt(text: String): Pair {
+ // Get cipher
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
+ // Get key
+ val keySpec = SecretKeySpec(this.encoded, "AES")
+ // Generate initialization vector
+ val iv = ByteArray(16)
+ SecureRandom().nextBytes(iv)
+ val ivSpec = IvParameterSpec(iv)
+ // Encrypt
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
+ return Pair(iv, Base64.encodeToString(cipher.doFinal(text.toByteArray()), Base64.DEFAULT))
+}
+
+fun SecretKey.decrypt(encryptedText: String, iv: ByteArray): String {
+ // Get cipher
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
+ // Get key
+ val keySpec = SecretKeySpec(this.encoded, "AES")
+ // Set initialization vector
+ val ivSpec = IvParameterSpec(iv)
+ // Decrypt
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
+ return String(
+ cipher.doFinal(
+ Base64.decode(encryptedText, Base64.DEFAULT)
+ )
+ )
+}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt
new file mode 100644
index 0000000..24670fc
--- /dev/null
+++ b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt
@@ -0,0 +1,37 @@
+package tech.tyman.plugins.encryptdms
+
+import com.aliucord.Http
+import com.aliucord.Logger
+import com.aliucord.Utils
+import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.filename
+import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.url
+import com.discord.models.message.Message
+import java.io.IOException
+
+fun Message.getAttachmentText(name: String): String? {
+ val url = this.attachments.find { attachment -> attachment.filename == name }?.url
+ ?: run {
+ Utils.showToast("Unable to find $name attachment for some reason", true)
+ return null
+ }
+ return try {
+ Http.simpleGet(url)
+ } catch (e: IOException) {
+ Logger("EncryptDMs").error(e)
+ Utils.showToast("Unable to load $name attachment for some reason", true)
+ return null
+ }
+}
+
+fun Message.getAttachmentTextWithError(name: String): Pair {
+ val url = this.attachments.find { attachment -> attachment.filename == name }?.url
+ ?: run {
+ return Pair(false, "Unable to find $name attachment for some reason")
+ }
+ return try {
+ Pair(true, Http.simpleGet(url))
+ } catch (e: IOException) {
+ Logger("EncryptDMs").error(e)
+ return Pair(false, "Unable to load $name attachment for some reason")
+ }
+}
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/README.md b/plugins/ReverseImageSearch/README.md
new file mode 100644
index 0000000..638554b
--- /dev/null
+++ b/plugins/ReverseImageSearch/README.md
@@ -0,0 +1,3 @@
+# ReverseImageSearch
+
+Allows you to revere image search by long pressing the "open in browser" button on images.
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/build.gradle.kts b/plugins/ReverseImageSearch/build.gradle.kts
new file mode 100644
index 0000000..0c8d768
--- /dev/null
+++ b/plugins/ReverseImageSearch/build.gradle.kts
@@ -0,0 +1,24 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.0.3"
+description = "A plugin that adds a button for reverse image searching a sent image."
+
+aliucord {
+ changelog.set("""
+ # Version 1.0.3
+ * Replace google images search with google lens, as the old images URL no longer works (thanks Delphox)
+ # Version 1.0.2
+ * Fixed google images search (i love it when google)
+ # Version 1.0.1
+ * Fixed a bug where some images would not work when using google
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/src/main/AndroidManifest.xml b/plugins/ReverseImageSearch/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/ReverseImageSearch/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt
new file mode 100644
index 0000000..4d0538b
--- /dev/null
+++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/EngineSheet.kt
@@ -0,0 +1,40 @@
+package tech.tyman.plugins.reverseimagesearch
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.res.ResourcesCompat
+import com.aliucord.Utils
+import com.aliucord.api.SettingsAPI
+import com.aliucord.widgets.BottomSheet
+import com.discord.widgets.media.WidgetMedia
+import com.lytefast.flexinput.R
+
+class EngineSheet(val url: String?) : BottomSheet() {
+ @SuppressLint("SetTextI18n")
+ override fun onViewCreated(view: View, bundle: Bundle?) {
+ super.onViewCreated(view, bundle)
+
+ val context = view.context
+ val textViews = Engine.values().mapNotNull {
+ if (it == Engine.ASK) return@mapNotNull null
+ TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply {
+ val iconDrawable = ResourcesCompat
+ .getDrawable(resources, R.e.ic_open_in_browser_white_24dp, null)
+ text = "Open in ${it.niceName}"
+ setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
+ setOnClickListener { _ ->
+ val url = this@EngineSheet.url ?: return@setOnClickListener
+ val intent = Intent(Intent.ACTION_VIEW, it.getUrl(url))
+ startActivity(intent)
+ }
+ }
+ }
+ textViews.forEach {
+ linearLayout.addView(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
new file mode 100644
index 0000000..609c495
--- /dev/null
+++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
@@ -0,0 +1,87 @@
+package tech.tyman.plugins.reverseimagesearch
+
+import android.content.Context
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.entities.Plugin
+import androidx.appcompat.view.menu.ActionMenuItemView
+import com.aliucord.Utils
+import com.discord.widgets.media.WidgetMedia
+import com.aliucord.patcher.after
+import android.net.Uri
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import androidx.core.content.res.ResourcesCompat
+import com.aliucord.Logger
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+
+@AliucordPlugin
+class ReverseImageSearch : Plugin() {
+ lateinit var pluginIcon: Drawable
+
+ init {
+ settingsTab = SettingsTab(SearchSettings::class.java).withArgs(settings)
+ needsResources = true
+ }
+
+ override fun load(ctx: Context) {
+ pluginIcon = ResourcesCompat.getDrawable(
+ resources,
+ resources.getIdentifier("ic_baseline_image_search_24", "drawable", "com.aliucord.plugins"),
+ null
+ )!!
+ }
+
+ override fun start(context: Context) {
+ val isVideoMethod = WidgetMedia::class.java.getDeclaredMethod("isVideo").apply {
+ isAccessible = true
+ }
+ patcher.after("onViewBoundOrOnResume") {
+ if (isVideoMethod.invoke(this) as Boolean) return@after
+ val btnId = Utils.getResId("menu_media_browser", "id")
+ val btn = Utils.appActivity.findViewById(btnId)
+ btn.setOnLongClickListener {
+ val url = mostRecentIntent.getStringExtra("INTENT_MEDIA_URL") ?: return@setOnLongClickListener false
+ if (settings.engine == Engine.ASK) {
+ EngineSheet(url)
+ .show(Utils.appActivity.supportFragmentManager, "Reverse Image Search")
+ } else {
+ val intent = Intent(Intent.ACTION_VIEW, settings.engine.getUrl(url))
+ startActivity(intent)
+ }
+ true
+ }
+ }
+ }
+
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ }
+}
+
+@Suppress("unused")
+enum class Engine(val urlTemplate: String?, val niceName: String) {
+ ASK(null, "Ask every time"),
+ GOOGLE("https://lens.google.com/uploadbyurl?url=%s", "Google Lens"),
+ TIN_EYE("https://www.tineye.com/search?url=%s", "TinEye"),
+ YANDEX("https://yandex.com/images/search?url=%s&rpt=imageview", "Yandex"),
+ BING("https://www.bing.com/images/search?q=imgurl:%s&view=detailv2&iss=sbi&FORM=IRSBIQ", "Bing"),
+ IQDB("https://iqdb.org/?url=%s", "IQDB"),
+ SAUCE_NAO("https://saucenao.com/search.php?url=%s", "SauceNAO");
+
+ fun getUrl(url: String): Uri {
+ val uri = Uri.parse(
+ urlTemplate?.format(
+ URLEncoder.encode(url, StandardCharsets.UTF_8.toString())
+ )
+ )
+
+ return uri
+ }
+
+ companion object {
+ fun fromOrdinal(ordinal: Int): Engine? {
+ return values().find { it.ordinal == ordinal }
+ }
+ }
+}
diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt
new file mode 100644
index 0000000..064f96a
--- /dev/null
+++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/SearchSettings.kt
@@ -0,0 +1,52 @@
+package tech.tyman.plugins.reverseimagesearch
+
+import android.annotation.SuppressLint
+import android.view.View
+import android.widget.RadioGroup
+import android.widget.TextView
+import com.aliucord.Utils
+import com.aliucord.api.SettingsAPI
+import com.aliucord.fragments.SettingsPage
+import com.discord.utilities.color.ColorCompat
+import com.discord.views.CheckedSetting
+import com.discord.views.RadioManager
+import com.lytefast.flexinput.R
+
+var SettingsAPI.engine: Engine
+ get() = Engine.fromOrdinal(this.getInt("engine", Engine.ASK.ordinal)) ?: Engine.ASK
+ set(value) = this.setInt("engine", value.ordinal)
+
+@SuppressLint("SetTextI18n")
+class SearchSettings(private val settings: SettingsAPI) : SettingsPage() {
+ override fun onViewBound(view: View?) {
+ super.onViewBound(view)
+ setActionBarTitle("ReverseImageSearch")
+ val ctx = requireContext()
+
+ val radios = Engine.values().mapNotNull {
+ Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.RADIO, it.niceName, null)
+ }
+ val radioManager = RadioManager(radios)
+ val radioGroup = RadioGroup(ctx)
+
+ radios.forEachIndexed { i, radio ->
+ radio.e {
+ settings.engine = Engine.fromOrdinal(i) ?: return@e
+ radioManager.a(radio)
+ }
+ radioGroup.addView(radio)
+ if (settings.engine.ordinal == i) radioManager.a(radio)
+ }
+
+ val helpText = TextView(ctx)
+ helpText.linksClickable = true
+ helpText.text =
+ "To reverse image search an image, open it and long press the \"Open in browser\" icon. " +
+ "Then, a reverse image search using then engine of your choice (you can choose this above)" +
+ "will open in a browser."
+ helpText.setTextColor(ColorCompat.getThemedColor(ctx, R.b.colorOnPrimary))
+
+ addView(radioGroup)
+ addView(helpText)
+ }
+}
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml b/plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml
new file mode 100644
index 0000000..83058ae
--- /dev/null
+++ b/plugins/ReverseImageSearch/src/main/res/drawable/ic_baseline_image_search_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/plugins/RoleColorContrast/README.md b/plugins/RoleColorContrast/README.md
new file mode 100644
index 0000000..4beb641
--- /dev/null
+++ b/plugins/RoleColorContrast/README.md
@@ -0,0 +1,11 @@
+# RoleColorContrast
+
+A plugin to increase role color contrasts, by attempting to make them adhere to the WCAG AA or AAA guidelines (configurable).
+
+## Compatibility
+
+This is compatible with basic Themer themes, so long as they use dark mode and do not use transparency. Transparency support is entirely too difficult and so please don't ask.
+
+## Adjusted UI elements
+
+* Names in chat
\ No newline at end of file
diff --git a/plugins/RoleColorContrast/build.gradle.kts b/plugins/RoleColorContrast/build.gradle.kts
new file mode 100644
index 0000000..1baf777
--- /dev/null
+++ b/plugins/RoleColorContrast/build.gradle.kts
@@ -0,0 +1,23 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.1.0"
+description = "A plugin to improve contrast between role colors and the background"
+
+aliucord {
+ changelog.set("""
+ # Version 1.1.0
+ * Now supports names in the member list
+ * Fixed a possible issue when themer was not installed
+ * Contrast adjustments are now faster
+
+ # Version 1.0.0
+ * Initial release, supporting only names in chat
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/RoleColorContrast/src/main/AndroidManifest.xml b/plugins/RoleColorContrast/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/RoleColorContrast/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt
new file mode 100644
index 0000000..16b273f
--- /dev/null
+++ b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/RoleColorContrast.kt
@@ -0,0 +1,131 @@
+@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
+
+package xyz.myriation.plugins.rolecolorcontrast
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
+import com.aliucord.PluginManager
+import com.aliucord.Utils
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.entities.Plugin
+import com.aliucord.patcher.after
+import com.discord.models.member.GuildMember
+import com.discord.stores.StoreStream
+import com.discord.widgets.channels.memberlist.adapter.ChannelMembersListAdapter
+import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
+import com.lytefast.flexinput.R
+import kotlin.math.pow
+
+enum class ContrastLevel {
+ Failing, AA, AAA
+}
+
+@AliucordPlugin
+class RoleColorContrast : Plugin() {
+ lateinit var pluginIcon: Drawable
+
+ init {
+ settingsTab = SettingsTab(PluginSettings::class.java).withArgs(settings)
+ }
+
+ @get:ColorInt
+ val themerBackgroundColor by lazy {
+ val classLoader = (PluginManager.plugins["Themer"] ?: return@lazy null)::class.java.classLoader!!
+ val resourceManager =
+ classLoader.loadClass("dev.vendicated.aliucordplugs.themer.ResourceManager").getDeclaredField("INSTANCE")
+ .get(null)
+ resourceManager::class.java.getDeclaredMethod("getColorForName", String::class.java)
+ .invoke(resourceManager!!, "primary_dark_600") as Int?
+ }
+
+ override fun load(context: Context) {
+ pluginIcon = ContextCompat.getDrawable(context, R.e.ic_accessibility_24dp)!!
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun start(context: Context) {
+ // Patch names in chat
+ patcher.after("getAuthorTextColor", GuildMember::class.java) {
+ it.result = coerceToContrast(it.result as Int, getBackgroundColor() ?: return@after)
+ }
+ // Patch names in guild member list
+ patcher.after("getColor") {
+ it.result = coerceToContrast(it.result as Int, getBackgroundColor() ?: return@after)
+ }
+ }
+
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ }
+
+ private fun getBackgroundColor(): Int? {
+ val theme = StoreStream.getUserSettingsSystem().theme
+ return when (theme) {
+ "pureEvil" -> ContextCompat.getColor(Utils.appContext, R.c.black)
+ "dark" -> themerBackgroundColor ?: ContextCompat.getColor(Utils.appContext, R.c.primary_dark_600)
+ "light" -> ContextCompat.getColor(Utils.appContext, R.c.white)
+ else -> return null
+ }
+ }
+
+ private fun coerceToContrast(text: Int, background: Int): Int {
+ val hsl = FloatArray(3)
+ ColorUtils.colorToHSL(text, hsl)
+ val factor = if (calculateRelativeLuminance(background) < 0.5) 0.03f else -0.03f
+ val comparator = if (settings.getBool("useAAA", false)) ContrastLevel.AAA else ContrastLevel.AA
+ while (calculateContrast(
+ ColorUtils.setAlphaComponent(ColorUtils.HSLToColor(hsl), 255), background
+ ) < comparator
+ ) {
+ hsl[2] += factor
+ // If we can't create full contrast, settle for the best we can
+ if (hsl[2] > 1 || hsl[2] < 0) {
+ hsl[2] = hsl[2].coerceIn(0f..1f)
+ break
+ }
+ }
+ return ColorUtils.HSLToColor(hsl)
+ }
+
+ private fun calculateContrast(text: Int, background: Int): ContrastLevel {
+ val textLuminance = calculateRelativeLuminance(text)
+ val backgroundLuminance = calculateRelativeLuminance(background)
+ val (luminanceOne, luminanceTwo) = if (textLuminance >= backgroundLuminance) {
+ Pair(textLuminance, backgroundLuminance)
+ } else {
+ Pair(backgroundLuminance, textLuminance)
+ }
+
+ val ratio = (luminanceOne + 0.05) / (luminanceTwo + 0.05)
+ return when {
+ ratio >= 7 -> ContrastLevel.AAA
+ ratio >= 4.5 -> ContrastLevel.AA
+ else -> ContrastLevel.Failing
+ }
+ }
+
+ private fun calculateRelativeLuminance(color: Int): Double {
+ val r = Color.red(color) / 255.0
+ val g = Color.green(color) / 255.0
+ val b = Color.blue(color) / 255.0
+
+ return 0.2126 * if (r <= 0.03928) {
+ r / 12.92
+ } else {
+ ((r + 0.055) / 1.055).pow(2.4)
+ } + 0.7152 * if (g <= 0.03928) {
+ g / 12.92
+ } else {
+ ((g + 0.055) / 1.055).pow(2.4)
+ } + 0.0722 * if (b <= 0.03928) {
+ b / 12.92
+ } else {
+ ((b + 0.055) / 1.055).pow(2.4)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt
new file mode 100644
index 0000000..748663d
--- /dev/null
+++ b/plugins/RoleColorContrast/src/main/kotlin/xyz/myriation/plugins/rolecolorcontrast/Settings.kt
@@ -0,0 +1,29 @@
+@file:Suppress("MISSING_DEPENDENCY_CLASS", "MISSING_DEPENDENCY_SUPERCLASS")
+package xyz.myriation.plugins.rolecolorcontrast
+
+import android.view.View
+import com.aliucord.Utils
+import com.aliucord.api.SettingsAPI
+import com.aliucord.fragments.SettingsPage
+import com.discord.views.CheckedSetting
+
+// TODO: Add example text
+class PluginSettings(private val settings: SettingsAPI) : SettingsPage() {
+ override fun onViewBound(view: View?) {
+ super.onViewBound(view)
+ val ctx = requireContext()
+
+ setActionBarTitle("RoleColorContrast")
+ addView(
+ Utils.createCheckedSetting(
+ ctx,
+ CheckedSetting.ViewType.SWITCH,
+ "Use AAA requirements instead of AA requirements for text contrast",
+ "AAA requirements are stricter, and require more contrast than the AA ones"
+ ).apply {
+ isChecked = settings.getBool("useAAA", false)
+ setOnCheckedListener { settings.setBool("useAAA", it) }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/plugins/ShowNames/README.md b/plugins/ShowNames/README.md
new file mode 100644
index 0000000..2085659
--- /dev/null
+++ b/plugins/ShowNames/README.md
@@ -0,0 +1,3 @@
+# ShowNames
+
+A plugin that changes the color of usernames to stop them from blending into the background.
\ No newline at end of file
diff --git a/plugins/ShowNames/build.gradle.kts b/plugins/ShowNames/build.gradle.kts
new file mode 100644
index 0000000..3cf399b
--- /dev/null
+++ b/plugins/ShowNames/build.gradle.kts
@@ -0,0 +1,30 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.0.6"
+description = "A plugin that changes the color of usernames to stop them from blending into the background."
+
+aliucord {
+ changelog.set("""
+ # Version 1.0.6
+ * Remove debug logging i accidentally left in the code
+ # Version 1.0.5
+ * Updated for new discord/aliucord version
+ # Version 1.0.4
+ * Fixed errors while hooking
+ # Version 1.0.3
+ * Converted code to use kotlin
+ # Version 1.0.2
+ * Fixed crashing on outdated aliucord
+ # Version 1.0.1
+ * Fixed changing color when on amoled mode and user has no role color
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/ShowNames/src/main/AndroidManifest.xml b/plugins/ShowNames/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/ShowNames/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt b/plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt
new file mode 100644
index 0000000..b8b7e1b
--- /dev/null
+++ b/plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt
@@ -0,0 +1,49 @@
+package tech.tyman.plugins.shownames
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
+import com.aliucord.Logger
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.entities.Plugin
+import com.aliucord.patcher.Hook
+import com.discord.models.member.GuildMember
+import com.discord.stores.StoreStream
+import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
+import com.lytefast.flexinput.R
+import kotlin.math.abs
+
+@AliucordPlugin
+class ShowNames : Plugin() {
+ override fun start(context: Context) {
+ patcher.patch(WidgetChatListAdapterItemMessage::class.java.getDeclaredMethod("getAuthorTextColor", GuildMember::class.java), Hook {
+ val member = it.args[0] as GuildMember? ?: return@Hook
+ val color = member.color
+ val theme = StoreStream.getUserSettingsSystem().theme
+ if (color == -16777216) { // Default (no role) color
+ return@Hook
+ }
+ val nameHsl = colorToHsl(color)
+ val bg = when (theme) {
+ "pureEvil" -> ContextCompat.getColor(context, R.c.black)
+ "dark" -> ContextCompat.getColor(context, R.c.primary_600)
+ "light" -> ContextCompat.getColor(context, R.c.white)
+ else -> return@Hook
+ }
+ val bgHsl = colorToHsl(bg)
+ if (abs(nameHsl[2] - bgHsl[2]) >= 0.01) return@Hook
+ it.result = ColorUtils.HSLToColor(
+ if (nameHsl[2] > 0.5) nameHsl.apply { this[2] =- 0.05f }
+ else nameHsl.apply { this[2] =+ 0.05f }
+ )
+ })
+ }
+
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ }
+
+ private fun colorToHsl(color: Int): FloatArray = FloatArray(3).apply {
+ ColorUtils.colorToHSL(color, this)
+ }
+}
diff --git a/plugins/Texter/README.md b/plugins/Texter/README.md
new file mode 100644
index 0000000..d49541b
--- /dev/null
+++ b/plugins/Texter/README.md
@@ -0,0 +1,3 @@
+# Texter
+
+A port of [SkyblockDev/texter](https://github.com/SkyBlockDev/texter) to Aliucord
\ No newline at end of file
diff --git a/plugins/Texter/build.gradle.kts b/plugins/Texter/build.gradle.kts
new file mode 100644
index 0000000..6b52f2a
--- /dev/null
+++ b/plugins/Texter/build.gradle.kts
@@ -0,0 +1,25 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.0.3"
+description = "A plugin to modify text formatting inspired by plugins like BetterFormattingRedux (for BetterDiscord) and texter (for Powercord)"
+
+aliucord {
+ changelog.set("""
+ # Version 1.0.3
+ * Updated for new discord/aliucord version
+ # Version 1.0.2
+ * Converted code to use kotlin
+ # Version 1.0.1
+ * Refactored code
+ * Resolve older android compatibility issues
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/Texter/src/main/AndroidManifest.xml b/plugins/Texter/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/Texter/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt
new file mode 100644
index 0000000..2b82134
--- /dev/null
+++ b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Maps.kt
@@ -0,0 +1,348 @@
+package tech.tyman.plugins.texter
+
+import java.lang.StringBuilder
+
+object Maps {
+ fun getMappedString(map: Map, string: String): String {
+ val splitText = string.chunked(1)
+ val newString = StringBuilder()
+ for (character in splitText) {
+ newString.append(
+ map.getOrDefault(character, character)
+ )
+ }
+ return newString.toString()
+ }
+
+ val smallLetters = mapOf(
+ "q" to "ǫ",
+ "w" to "ᴡ",
+ "e" to "ᴇ",
+ "r" to "ʀ",
+ "t" to "ᴛ",
+ "y" to "ʏ",
+ "u" to "ᴜ",
+ "i" to "ɪ",
+ "o" to "ᴏ",
+ "p" to "ᴘ",
+ "a" to "ᴀ",
+ "s" to "s",
+ "d" to "ᴅ",
+ "f" to "ғ",
+ "g" to "ɢ",
+ "h" to "ʜ",
+ "j" to "ᴊ",
+ "k" to "ᴋ",
+ "l" to "ʟ",
+ "z" to "ᴢ",
+ "x" to "x",
+ "c" to "ᴄ",
+ "v" to "ᴠ",
+ "b" to "ʙ",
+ "n" to "ɴ",
+ "m" to "ᴍ",
+ "Q" to "ǫ",
+ "W" to "ᴡ",
+ "E" to "ᴇ",
+ "R" to "ʀ",
+ "T" to "ᴛ",
+ "Y" to "ʏ",
+ "U" to "ᴜ",
+ "I" to "ɪ",
+ "O" to "ᴏ",
+ "P" to "ᴘ",
+ "A" to "ᴀ",
+ "S" to "s",
+ "D" to "ᴅ",
+ "F" to "ғ",
+ "G" to "ɢ",
+ "H" to "ʜ",
+ "J" to "ᴊ",
+ "K" to "ᴋ",
+ "L" to "ʟ",
+ "Z" to "ᴢ",
+ "X" to "x",
+ "C" to "ᴄ",
+ "V" to "ᴠ",
+ "B" to "ʙ",
+ "N" to "ɴ",
+ "M" to "ᴍ"
+ )
+ val smallerLetters = mapOf(
+ "q" to "ᑫ",
+ "w" to "ʷ",
+ "e" to "ᵉ",
+ "r" to "ʳ",
+ "t" to "ᵗ",
+ "y" to "ʸ",
+ "u" to "ᵘ",
+ "i" to "ᶦ",
+ "o" to "ᵒ",
+ "p" to "ᵖ",
+ "a" to "ᵃ",
+ "s" to "ˢ",
+ "d" to "ᵈ",
+ "f" to "ᶠ",
+ "g" to "ᵍ",
+ "h" to "ʰ",
+ "j" to "ʲ",
+ "k" to "ᵏ",
+ "l" to "ˡ",
+ "z" to "ᶻ",
+ "x" to "ˣ",
+ "c" to "ᶜ",
+ "v" to "ᵛ",
+ "b" to "ᵇ",
+ "n" to "ⁿ",
+ "m" to "ᵐ",
+ "Q" to "ᑫ",
+ "W" to "ʷ",
+ "E" to "ᵉ",
+ "R" to "ʳ",
+ "T" to "ᵗ",
+ "Y" to "ʸ",
+ "U" to "ᵘ",
+ "I" to "ᶦ",
+ "O" to "ᵒ",
+ "P" to "ᵖ",
+ "A" to "ᵃ",
+ "S" to "ˢ",
+ "D" to "ᵈ",
+ "F" to "ᶠ",
+ "G" to "ᵍ",
+ "H" to "ʰ",
+ "J" to "ʲ",
+ "K" to "ᵏ",
+ "L" to "ˡ",
+ "Z" to "ᶻ",
+ "X" to "ˣ",
+ "C" to "ᶜ",
+ "V" to "ᵛ",
+ "B" to "ᵇ",
+ "N" to "ⁿ",
+ "M" to "ᵐ",
+ "1" to "¹",
+ "2" to "²",
+ "3" to "³",
+ "4" to "⁴",
+ "5" to "⁵",
+ "6" to "⁶",
+ "7" to "⁷",
+ "8" to "⁸",
+ "9" to "⁹",
+ "0" to "⁰"
+ )
+ val fullWidthLetters = mapOf(
+ "q" to "q",
+ "w" to "w",
+ "e" to "e",
+ "r" to "r",
+ "t" to "t",
+ "y" to "y",
+ "u" to "u",
+ "i" to "i",
+ "o" to "o",
+ "p" to "p",
+ "a" to "a",
+ "s" to "s",
+ "d" to "d",
+ "f" to "f",
+ "g" to "g",
+ "h" to "h",
+ "j" to "j",
+ "k" to "k",
+ "l" to "l",
+ "z" to "z",
+ "x" to "x",
+ "c" to "c",
+ "v" to "v",
+ "b" to "b",
+ "n" to "n",
+ "m" to "m",
+ "Q" to "Q",
+ "W" to "W",
+ "E" to "E",
+ "R" to "R",
+ "T" to "T",
+ "Y" to "Y",
+ "U" to "U",
+ "I" to "I",
+ "O" to "O",
+ "P" to "P",
+ "A" to "A",
+ "S" to "S",
+ "D" to "D",
+ "F" to "F",
+ "G" to "G",
+ "H" to "H",
+ "J" to "J",
+ "K" to "K",
+ "L" to "L",
+ "Z" to "Z",
+ "X" to "X",
+ "C" to "C",
+ "V" to "V",
+ "B" to "B",
+ "N" to "N",
+ "M" to "M",
+ "1" to "1",
+ "2" to "2",
+ "3" to "3",
+ "4" to "4",
+ "5" to "5",
+ "6" to "6",
+ "7" to "7",
+ "8" to "8",
+ "9" to "9",
+ "0" to "0"
+ )
+ val emojiLetters = mapOf(
+ "q" to ":regional_indicator_q:",
+ "w" to ":regional_indicator_w:",
+ "e" to ":regional_indicator_e:",
+ "r" to ":regional_indicator_r:",
+ "t" to ":regional_indicator_t:",
+ "y" to ":regional_indicator_y:",
+ "u" to ":regional_indicator_u:",
+ "i" to ":regional_indicator_i:",
+ "o" to ":regional_indicator_o:",
+ "p" to ":regional_indicator_p:",
+ "a" to ":regional_indicator_a:",
+ "s" to ":regional_indicator_s:",
+ "d" to ":regional_indicator_d:",
+ "f" to ":regional_indicator_f:",
+ "g" to ":regional_indicator_g:",
+ "h" to ":regional_indicator_h:",
+ "j" to ":regional_indicator_j:",
+ "k" to ":regional_indicator_k:",
+ "l" to ":regional_indicator_l:",
+ "z" to ":regional_indicator_z:",
+ "x" to ":regional_indicator_x:",
+ "c" to ":regional_indicator_c:",
+ "v" to ":regional_indicator_v:",
+ "b" to ":regional_indicator_b:",
+ "n" to ":regional_indicator_n:",
+ "m" to ":regional_indicator_m:",
+ "Q" to ":regional_indicator_q:",
+ "W" to ":regional_indicator_w:",
+ "E" to ":regional_indicator_e:",
+ "R" to ":regional_indicator_r:",
+ "T" to ":regional_indicator_t:",
+ "Y" to ":regional_indicator_y:",
+ "U" to ":regional_indicator_u:",
+ "I" to ":regional_indicator_i:",
+ "O" to ":regional_indicator_o:",
+ "P" to ":regional_indicator_p:",
+ "A" to ":regional_indicator_a:",
+ "S" to ":regional_indicator_s:",
+ "D" to ":regional_indicator_d:",
+ "F" to ":regional_indicator_f:",
+ "G" to ":regional_indicator_g:",
+ "H" to ":regional_indicator_h:",
+ "J" to ":regional_indicator_j:",
+ "K" to ":regional_indicator_k:",
+ "L" to ":regional_indicator_l:",
+ "Z" to ":regional_indicator_z:",
+ "X" to ":regional_indicator_x:",
+ "C" to ":regional_indicator_c:",
+ "V" to ":regional_indicator_v:",
+ "B" to ":regional_indicator_b:",
+ "N" to ":regional_indicator_n:",
+ "M" to ":regional_indicator_m:",
+ "1" to ":one:",
+ "2" to ":two:",
+ "3" to ":three:",
+ "4" to ":four:",
+ "5" to ":five:",
+ "6" to ":six:",
+ "7" to ":seven:",
+ "8" to ":eight:",
+ "9" to ":nine:",
+ "0" to ":zero:"
+ )
+ val flippedLetters = mapOf(
+ "q" to "b",
+ "w" to "ʍ",
+ "e" to "ǝ",
+ "r" to "ɹ",
+ "t" to "ʇ",
+ "y" to "ʎ",
+ "u" to "n",
+ "i" to "ᴉ",
+ "p" to "d",
+ "a" to "ɐ",
+ "s" to "s",
+ "d" to "p",
+ "f" to "ɟ",
+ "g" to "ɓ",
+ "h" to "ɥ",
+ "j" to "ſ",
+ "k" to "ʞ",
+ "l" to "ๅ",
+ "z" to "z",
+ "x" to "x",
+ "c" to "ɔ",
+ "v" to "ʌ",
+ "b" to "q",
+ "n" to "u",
+ "m" to "ɯ",
+ "Q" to "Ὸ",
+ "W" to "M",
+ "E" to "Ǝ",
+ "R" to "ꓤ",
+ "T" to "ꓕ",
+ "Y" to "⅄",
+ "U" to "ꓵ",
+ "I" to "I",
+ "O" to "O",
+ "P" to "ꓒ",
+ "A" to "Ɐ",
+ "S" to "S",
+ "D" to "ꓷ",
+ "F" to "ꓞ",
+ "G" to "ꓨ",
+ "H" to "H",
+ "J" to "ſ",
+ "K" to "ꓘ",
+ "L" to "ꓶ",
+ "Z" to "Z",
+ "X" to "X",
+ "C" to "ꓛ",
+ "V" to "ꓥ",
+ "B" to "ꓭ",
+ "N" to "N",
+ "M" to "W",
+ "1" to "Ɩ",
+ "2" to "Շ",
+ "3" to "Ɛ",
+ "4" to "h",
+ "5" to "૬",
+ "6" to "9",
+ "7" to "L",
+ "8" to "8",
+ "9" to "6",
+ "0" to "0"
+ )
+ val leetLetters = mapOf(
+ "e" to "3",
+ "r" to "Я",
+ "t" to "7",
+ "o" to "0",
+ "a" to "4",
+ "s" to "5",
+ "h" to "#",
+ "l" to "1",
+ "z" to "2",
+ "b" to "8",
+ "E" to "3",
+ "R" to "Я",
+ "T" to "7",
+ "O" to "0",
+ "A" to "4",
+ "S" to "5",
+ "H" to "#",
+ "L" to "1",
+ "Z" to "2",
+ "B" to "8"
+ )
+}
\ No newline at end of file
diff --git a/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt
new file mode 100644
index 0000000..9af0026
--- /dev/null
+++ b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Texter.kt
@@ -0,0 +1,103 @@
+package tech.tyman.plugins.texter
+
+import android.content.Context
+import com.aliucord.Utils
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.api.CommandsAPI
+import com.aliucord.entities.CommandContext
+import com.aliucord.entities.Plugin
+import com.discord.api.commands.ApplicationCommandType
+
+@AliucordPlugin
+class Texter : Plugin() {
+ override fun start(context: Context) {
+ registerConverterCommand(
+ "small",
+ "Turns your text into small letters", emptyList()
+ ) { ctx: CommandContext -> getResult(Maps.smallLetters, ctx.getString("text")!!) }
+ registerConverterCommand(
+ "smaller",
+ "Turns your text into smaller letters", emptyList()
+ ) { ctx: CommandContext -> getResult(Maps.smallerLetters, ctx.getString("text")!!) }
+ registerConverterCommand(
+ "fullwidth",
+ "Turns your text into full width letters", listOf(
+ "fw"
+ )
+ ) { ctx: CommandContext -> getResult(Maps.fullWidthLetters, ctx.getString("text")!!) }
+ registerConverterCommand(
+ "emoji",
+ "Turns your text into emoji letters",
+ listOf(
+ "emojify",
+ "blockify"
+ )
+ ) { ctx: CommandContext -> getResult(Maps.emojiLetters, ctx.getString("text")!!) }
+ registerConverterCommand(
+ "flip",
+ "Flips your text upside down"
+ ) { ctx: CommandContext -> getResult(Maps.flippedLetters, ctx.getString("text")!!.reverse()) }
+ registerConverterCommand(
+ "clap",
+ "Adds clapping icons to your text"
+ ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.clapify()) }
+ registerConverterCommand(
+ "reverse",
+ "Reverses your text"
+ ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.reverse()) }
+ registerConverterCommand(
+ "space",
+ "Spaces out your text"
+ ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.chunked(1).joinToString(" ")) }
+ registerConverterCommand(
+ "mock",
+ "Capitalizes random parts of your text"
+ ) { ctx: CommandContext -> getResult(ctx.getString("text")!!.mock()) }
+ registerConverterCommand(
+ "leet",
+ "Makes your text 1337 style", listOf(
+ "leetify"
+ )
+ ) { ctx: CommandContext -> getResult(Maps.leetLetters, ctx.getString("text")!!) }
+ }
+
+ private fun registerConverterCommand(name: String, description: String, execute: (CommandContext) -> CommandsAPI.CommandResult) {
+ commands.registerCommand(
+ name,
+ description, listOf(
+ Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to change")
+ ),
+ execute
+ )
+ }
+ private fun registerConverterCommand(name: String, description: String, aliases: List, execute: (CommandContext) -> CommandsAPI.CommandResult) {
+ commands.registerCommand(
+ name,
+ description, listOf(
+ Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to change")
+ ),
+ execute
+ )
+ for (alias in aliases) {
+ commands.registerCommand(
+ alias,
+ description, listOf(
+ Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to change")
+ ),
+ execute
+ )
+ }
+ }
+
+ override fun stop(context: Context) {
+ commands.unregisterAll()
+ }
+
+ private fun getResult(map: Map, textToMap: String): CommandsAPI.CommandResult {
+ return CommandsAPI.CommandResult(Maps.getMappedString(map, textToMap), null, true)
+ }
+
+ private fun getResult(textToSend: String): CommandsAPI.CommandResult {
+ return CommandsAPI.CommandResult(textToSend, null, true)
+ }
+}
\ No newline at end of file
diff --git a/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt
new file mode 100644
index 0000000..ccf4f51
--- /dev/null
+++ b/plugins/Texter/src/main/kotlin/tech/tyman/plugins/texter/Utils.kt
@@ -0,0 +1,16 @@
+package tech.tyman.plugins.texter
+
+import java.lang.StringBuilder
+import java.util.*
+
+fun String.clapify() = ":clap:" + this.replace(" ", ":clap:") + ":clap:"
+fun String.reverse() = StringBuilder(this).reverse().toString()
+
+fun String.mock(): String {
+ val builder = StringBuilder()
+ val random = Random()
+ for (character in this.chunked(1)) {
+ builder.append(if (random.nextBoolean()) character.lowercase() else character.uppercase())
+ }
+ return builder.toString()
+}
\ No newline at end of file
diff --git a/plugins/Translate/README.md b/plugins/Translate/README.md
new file mode 100644
index 0000000..2c03636
--- /dev/null
+++ b/plugins/Translate/README.md
@@ -0,0 +1,3 @@
+# Translate
+
+Adds a message context menu action to translate the message to english (configurable later). You can also use /translate to send translated text, or just view it locally.
\ No newline at end of file
diff --git a/plugins/Translate/build.gradle.kts b/plugins/Translate/build.gradle.kts
new file mode 100644
index 0000000..e6a3700
--- /dev/null
+++ b/plugins/Translate/build.gradle.kts
@@ -0,0 +1,39 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.3.1"
+description = "Adds an option to translate messages."
+aliucord {
+ changelog.set("""
+ # Version 1.3.1
+ * Fix translating multiple lines of text (finally)
+ # Version 1.3.0
+ * Added "show original functionality", will show the original, untranslated message, but still keeps it cached so you can instantly re-translate it.
+ # Version 1.2.4
+ * Fixed translate settings crash
+ # Version 1.2.3
+ * Updated to support discord 101.3
+ # Version 1.2.2
+ * Updated for new discord/aliucord version
+ # Version 1.2.1
+ * Improved/added language code validation
+ * Added a list of language codes to plugin settings
+ # Version 1.2.0
+ * Edits the message with the translated text instead of creating a new local message
+ # Version 1.1.2
+ * Fixed message links not having /channels/
+ * Moved classes to com.aliucord.plugins.translate
+ # Version 1.1.1
+ * Added settings to modify the default translation language
+ # Version 1.1.0
+ * Add /translate to translate text from one language to another, and send it in chat by default.
+ # Version 1.0.0
+ * Initial release
+ """.trimIndent())
+
+ author(Authors.Ty)
+}
\ No newline at end of file
diff --git a/plugins/Translate/src/main/AndroidManifest.xml b/plugins/Translate/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec11743
--- /dev/null
+++ b/plugins/Translate/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt
new file mode 100644
index 0000000..c42ef4e
--- /dev/null
+++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/PluginSettings.kt
@@ -0,0 +1,55 @@
+package tech.tyman.plugins.translate
+
+import android.text.Editable
+import android.text.util.Linkify
+import android.view.View
+import android.widget.TextView
+import com.aliucord.views.TextInput
+import com.aliucord.Utils
+import com.aliucord.api.SettingsAPI
+import com.aliucord.fragments.SettingsPage
+import com.aliucord.views.Button
+import com.discord.utilities.color.ColorCompat
+import com.discord.utilities.view.text.TextWatcher
+import com.lytefast.flexinput.R
+
+
+class PluginSettings(private val settings: SettingsAPI) : SettingsPage() {
+ override fun onViewBound(view: View?) {
+ super.onViewBound(view)
+ setActionBarTitle("Translate")
+ val ctx = requireContext()
+ val input = TextInput(ctx, "Language to translate messages to by default")
+ val editText = input.editText
+ val button = Button(ctx)
+ button.text = "Save"
+ button.setOnClickListener {
+ settings.setString("defaultLanguage", editText.text.toString())
+ Utils.showToast("Saved translate settings!")
+ close()
+ }
+ editText.maxLines = 1
+ editText.setText(settings.getString("defaultLanguage", "en"))
+ editText.addTextChangedListener(object : TextWatcher() {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
+ override fun afterTextChanged(s: Editable) {
+ if (!languageCodes.containsValue(s.toString())) {
+ button.alpha = 0.5f
+ button.isClickable = false
+ } else {
+ button.alpha = 1f
+ button.isClickable = true
+ }
+ }
+ })
+ val helpText = TextView(ctx)
+ helpText.linksClickable = true
+ helpText.text = "Supported language codes:\n\n${languageCodes.map { "${it.key} -> ${it.value}" }.joinToString("\n")}"
+ Linkify.addLinks(helpText, Linkify.WEB_URLS)
+ helpText.setTextColor(ColorCompat.getThemedColor(ctx, R.b.colorOnPrimary))
+ addView(input)
+ addView(button)
+ addView(helpText)
+ }
+}
diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt
new file mode 100644
index 0000000..aaea69e
--- /dev/null
+++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt
@@ -0,0 +1,234 @@
+package tech.tyman.plugins.translate
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.text.Spanned
+import android.text.style.RelativeSizeSpan
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.widget.NestedScrollView
+import com.aliucord.CollectionUtils
+import com.aliucord.Http
+import com.aliucord.Utils
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.api.CommandsAPI
+import com.aliucord.entities.Plugin
+import com.aliucord.patcher.Hook
+import com.discord.api.commands.ApplicationCommandType
+import com.discord.databinding.WidgetChatListActionsBinding
+import com.discord.utilities.textprocessing.node.EditedMessageNode
+import com.discord.utilities.view.text.SimpleDraweeSpanTextView
+import com.discord.widgets.chat.list.WidgetChatList
+import com.discord.widgets.chat.list.actions.WidgetChatListActions
+import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
+import com.discord.widgets.chat.list.entries.MessageEntry
+import com.facebook.drawee.span.DraweeSpanStringBuilder
+import com.lytefast.flexinput.R
+import org.json.JSONArray
+import java.lang.reflect.Field
+import java.util.regex.Pattern
+
+@AliucordPlugin
+class Translate : Plugin() {
+ lateinit var pluginIcon: Drawable
+ private val translatedMessages = mutableMapOf()
+ private var chatList: WidgetChatList? = null
+ private val messageLoggerEditedRegex = Pattern.compile("(?:.+ \\(.+: .+\\)\\n)+(.+)\$")
+
+ init {
+ settingsTab = SettingsTab(PluginSettings::class.java).withArgs(settings)
+ }
+
+ override fun load(ctx: Context) {
+ pluginIcon = ContextCompat.getDrawable(ctx, R.e.ic_locale_24dp)!!
+ }
+
+ override fun start(context: Context) {
+ patchMessageContextMenu()
+ patchProcessMessageText()
+ commands.registerCommand(
+ "translate",
+ "Translates text from one language to another, sends by default",
+ listOf(
+ Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to translate"),
+ Utils.createCommandOption(ApplicationCommandType.STRING, "to", "The language to translate to (default en, must be a language code described in plugin settings)", choices = languageCodeChoices),
+ Utils.createCommandOption(ApplicationCommandType.STRING, "from", "The language to translate from (default auto, must be a language code described in plugin settings)", choices = languageCodeChoices),
+ Utils.createCommandOption(ApplicationCommandType.BOOLEAN, "send", "Whether or not to send the message in chat (default true)")
+ )
+ ) { ctx ->
+ val translateData = translateMessage(
+ ctx.getRequiredString("text"),
+ ctx.getString("from"),
+ ctx.getString("to")
+ )
+ if (translateData !is TranslateSuccessData) {
+ with(translateData as TranslateErrorData) {
+ return@registerCommand CommandsAPI.CommandResult(
+ "$errorText ($errorCode)",
+ null,
+ false
+ )
+ }
+ }
+ return@registerCommand CommandsAPI.CommandResult(
+ translateData.translatedText,
+ null,
+ ctx.getBoolOrDefault("send", true)
+ )
+ }
+ }
+
+ private fun DraweeSpanStringBuilder.setTranslated(translateData: TranslateSuccessData, context: Context) {
+ val contentStartIndex = messageLoggerEditedRegex.matcher(this.toString()).let {
+ if (it.find()) {
+ it.start(1)
+ } else 0
+ }
+ this.replace(contentStartIndex, contentStartIndex + translateData.sourceText.length, translateData.translatedText)
+ val textEnd = this.length
+ this.append(" (translated: ${translateData.sourceLanguage} -> ${translateData.translatedLanguage})")
+ this.setSpan(RelativeSizeSpan(0.75f), textEnd, this.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ if (textEnd != this.length) {
+ this.setSpan(EditedMessageNode.Companion.`access$getForegroundColorSpan`(EditedMessageNode.Companion, context),
+ textEnd, this.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ }
+
+ private fun patchProcessMessageText() {
+ patcher.patch(WidgetChatList::class.java.getDeclaredConstructor(), Hook {
+ chatList = it.thisObject as WidgetChatList
+ })
+
+ val mDraweeStringBuilder: Field = SimpleDraweeSpanTextView::class.java.getDeclaredField("mDraweeStringBuilder")
+ mDraweeStringBuilder.isAccessible = true
+ patcher.patch(WidgetChatListAdapterItemMessage::class.java, "processMessageText", arrayOf(SimpleDraweeSpanTextView::class.java, MessageEntry::class.java), Hook {
+ val messageEntry = it.args[1] as MessageEntry
+ val message = messageEntry.message ?: return@Hook
+ val id = message.id
+ val translateData = translatedMessages[id] ?: return@Hook
+ if (translateData.showingOriginal) return@Hook
+ if (translateData.sourceText != message.content) {
+ translatedMessages.remove(id)
+ return@Hook
+ }
+ val textView = it.args[0] as SimpleDraweeSpanTextView
+ val builder = mDraweeStringBuilder[textView] as DraweeSpanStringBuilder?
+ ?: return@Hook
+ val context = textView.context
+ builder.setTranslated(translateData, context)
+ textView.setDraweeSpanStringBuilder(builder)
+ })
+ }
+
+ private fun patchMessageContextMenu() {
+ val viewId = View.generateViewId()
+ val messageContextMenu = WidgetChatListActions::class.java
+ val getBinding = messageContextMenu.getDeclaredMethod("getBinding").apply { isAccessible = true }
+
+ patcher.patch(messageContextMenu.getDeclaredMethod("configureUI", WidgetChatListActions.Model::class.java), Hook {
+ val menu = it.thisObject as WidgetChatListActions
+ val binding = getBinding.invoke(menu) as WidgetChatListActionsBinding
+ val translateButton = binding.a.findViewById(viewId)
+ translateButton.setOnClickListener { _ ->
+ val message = (it.args[0] as WidgetChatListActions.Model).message
+ val translationEntry = translatedMessages[message.id]
+
+ if (translationEntry == null) {
+ // If not translated yet, fetch and cache the translation, then rerender the message
+ Utils.threadPool.execute {
+ val response = translateMessage(message.content)
+ if (response !is TranslateSuccessData) {
+ with (response as TranslateErrorData) {
+ Utils.showToast("$errorText ($errorCode)", true)
+ return@execute
+ }
+ }
+ translatedMessages[message.id] = response
+ chatList?.rerenderMessage(message.id)
+ Utils.showToast("Translated message")
+ menu.dismiss()
+ }
+ } else {
+ // If translated, then no need to translate anything, so just flip the showingOriginal property and rerender
+ translationEntry.showingOriginal = !translationEntry.showingOriginal
+ chatList?.rerenderMessage(message.id)
+ menu.dismiss()
+ }
+ }
+ })
+
+ patcher.patch(messageContextMenu, "onViewCreated", arrayOf(View::class.java, Bundle::class.java), Hook {
+ val linearLayout = (it.args[0] as NestedScrollView).getChildAt(0) as LinearLayout
+ val context = linearLayout.context
+ val messageId = WidgetChatListActions.`access$getMessageId$p`(it.thisObject as WidgetChatListActions)
+ linearLayout.addView(TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply {
+ val translationEntry = translatedMessages[messageId]
+
+ id = viewId
+ text = if (translationEntry == null || translationEntry.showingOriginal) {
+ // If not translated yet, or original is currently shown
+ "Translate message"
+ } else {
+ // Otherwise, it must be translated and original is not currently being shown
+ "Show original"
+ }
+ setCompoundDrawablesRelativeWithIntrinsicBounds(pluginIcon, null, null, null)
+ })
+ })
+ }
+
+ override fun stop(context: Context?) = patcher.unpatchAll()
+
+ private fun translateMessage(text: String, from: String? = null, to: String? = null): TranslateData {
+ val toLang = to ?: settings.getString("defaultLanguage", "en")
+ val fromLang = from ?: "auto"
+ val queryBuilder = Http.QueryBuilder("https://translate.googleapis.com/translate_a/single").run {
+ append("client", "gtx")
+ append("sl", fromLang)
+ append("tl", toLang)
+ append("dt", "t")
+ append("q", text)
+ }
+ val translatedJsonReq = Http.Request(queryBuilder.toString(), "GET").apply {
+ setHeader("Content-Type", "application/json")
+ setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4592.0 Safari/537.36")
+ }.execute()
+
+ if (!translatedJsonReq.ok()) {
+ return when (translatedJsonReq.statusCode) {
+ 429 -> TranslateErrorData(
+ errorCode = 429,
+ errorText = "Translate API ratelimit reached. Please try again later."
+ )
+ else -> TranslateErrorData(
+ errorCode = translatedJsonReq.statusCode,
+ errorText = "An unknown error occurred. Please report this to the developer of Translate."
+ )
+ }
+ }
+ val parsedJson = JSONArray(translatedJsonReq.text())
+
+ val translatedSections = parsedJson.getJSONArray(0)
+
+ val translatedText = buildString {
+ for (i in 0 until translatedSections.length()) {
+ append(translatedSections.getJSONArray(i).getString(0))
+ }
+ }
+
+ return TranslateSuccessData(
+ sourceLanguage = parsedJson.getString(2),
+ translatedLanguage = toLang,
+ sourceText = text,
+ translatedText = translatedText
+ )
+ }
+
+// private fun Message.guildId(): Long? {
+// val channel = ChannelWrapper(StoreStream.getChannels().getChannel(this.channelId))
+// return if (channel.isGuild()) channel.guildId else null
+// }
+}
diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt
new file mode 100644
index 0000000..046ff6f
--- /dev/null
+++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Utils.kt
@@ -0,0 +1,171 @@
+package tech.tyman.plugins.translate
+
+import com.aliucord.CollectionUtils
+import com.discord.api.commands.CommandChoice
+import com.discord.widgets.chat.list.WidgetChatList
+import com.discord.widgets.chat.list.entries.MessageEntry
+
+fun WidgetChatList.rerenderMessage(messageId: Long) {
+ val adapter = WidgetChatList.`access$getAdapter$p`(this)
+ val data = adapter.internalData
+ val i = CollectionUtils.findIndex(data) { m ->
+ m is MessageEntry && m.message.id == messageId
+ }
+ if (i != -1) adapter.notifyItemChanged(i)
+}
+
+open class TranslateData
+
+data class TranslateSuccessData(
+ val sourceLanguage: String,
+ val translatedLanguage: String,
+ val sourceText: String,
+ val translatedText: String,
+ var showingOriginal: Boolean = false
+) : TranslateData()
+
+data class TranslateErrorData(
+ val errorCode: Number,
+ val errorText: String
+) : TranslateData()
+
+val languageCodes = mapOf(
+ "Afrikaans" to "af",
+ "Albanian" to "sq",
+ "Amharic" to "am",
+ "Arabic" to "ar",
+ "Armenian" to "hy",
+ "Assamese" to "as",
+ "Azerbaijani" to "az",
+ "Bangla" to "bn",
+ "Basque" to "eu",
+ "Belarusian" to "be",
+ "Bosnian" to "bs",
+ "Bulgarian" to "bg",
+ "Burmese" to "my",
+ "Cantonese" to "zh-yue",
+ "Catalan" to "ca",
+ "Cebuano" to "ceb",
+ "Central Kurdish" to "ckb",
+ "Chakma" to "ccp",
+ "Cherokee" to "chr",
+ "Chinese (Hong Kong)" to "zh-HK",
+ "Chinese (Simplified, China)" to "zh-Hans",
+ "Chinese (Simplified)" to "zh-CN",
+ "Chinese (Traditional, Taiwan)" to "zh-Hant",
+ "Chinese (Traditional)" to "zh-TW",
+ "Corsican" to "co",
+ "Croatian" to "hr",
+ "Czech" to "cs",
+ "Danish" to "da",
+ "Dutch" to "nl",
+ "Dzongkha" to "dz",
+ "English" to "en",
+ "Esperanto" to "eo",
+ "Estonian" to "et",
+ "Filipino" to "fil",
+ "Filipino" to "tl",
+ "Finnish" to "fi",
+ "French" to "fr",
+ "Fulah" to "ff",
+ "Galician" to "gl",
+ "Georgian" to "ka",
+ "German" to "de",
+ "Greek" to "el",
+ "Gujarati" to "gu",
+ "Haitian Creole" to "ht",
+ "Hawaiian" to "haw",
+ "Hebrew" to "he",
+ "Hindi" to "hi",
+ "Hmong" to "hmn",
+ "Hungarian" to "hu",
+ "Icelandic" to "is",
+ "Indonesian" to "id",
+ "Inuktitut" to "iu",
+ "Irish" to "ga",
+ "Italian" to "it",
+ "Japanese" to "ja",
+ "Javanese" to "jv",
+ "Javanese" to "jw",
+ "Kannada" to "kn",
+ "Kazakh" to "kk",
+ "Khmer" to "km",
+ "Korean" to "ko",
+ "Kurdish" to "ku",
+ "Kyrgyz" to "ky",
+ "Lao" to "lo",
+ "Latin" to "la",
+ "Latvian" to "lv",
+ "Lisu" to "lis",
+ "Lithuanian" to "lt",
+ "Luxembourgish" to "lb",
+ "Macedonian" to "mk",
+ "Makah" to "myh",
+ "Malagasy" to "mg",
+ "Malay" to "ms",
+ "Malayalam" to "ml",
+ "Maltese" to "mt",
+ "Manipuri (Meitei Mayek)" to "mni-Mtei",
+ "Maori" to "mi",
+ "Marathi" to "mr",
+ "Menominee" to "mez",
+ "Mongolian" to "mn",
+ "Navajo" to "nv",
+ "Nepali" to "ne",
+ "Norwegian Nynorsk" to "nn",
+ "Norwegian" to "nb",
+ "Norwegian" to "no",
+ "Nyanja" to "ny",
+ "Odia" to "or",
+ "Ojibwa" to "oj",
+ "Oneida" to "one",
+ "Osage" to "osa",
+ "Pashto" to "ps",
+ "Persian" to "fa",
+ "Plains Cree" to "crk",
+ "Polish" to "pl",
+ "Portuguese (Brazil)" to "pt-BR",
+ "Portuguese (Portugal)" to "pt-PT",
+ "Punjabi" to "pa",
+ "Rohingya" to "rhg",
+ "Romanian" to "ro",
+ "Romany" to "rom",
+ "Russian" to "ru",
+ "Samoan" to "sm",
+ "Sanskrit" to "sa",
+ "Scottish Gaelic" to "gd",
+ "Seneca" to "see",
+ "Serbian" to "sr",
+ "Shona" to "sn",
+ "Sindhi" to "sd",
+ "Sinhala" to "si",
+ "Slovak" to "sk",
+ "Slovenian" to "sl",
+ "Somali" to "so",
+ "Southern Uzbek" to "uzs",
+ "Spanish" to "es",
+ "Sundanese" to "su",
+ "Swahili" to "sw",
+ "Swedish" to "sv",
+ "Tajik" to "tg",
+ "Tamil" to "ta",
+ "Tatar" to "tt",
+ "Telugu" to "te",
+ "Thai" to "th",
+ "Tibetan" to "bo",
+ "Tigrinya" to "ti",
+ "Turkish" to "tr",
+ "Ukrainian" to "uk",
+ "Urdu" to "ur",
+ "Uyghur" to "ug",
+ "Uzbek" to "uz",
+ "Vietnamese" to "vi",
+ "Welsh" to "cy",
+ "Western Frisian" to "fy",
+ "Xhosa" to "xh",
+ "Yiddish" to "yi",
+ "Yoruba" to "yo",
+ "Zulu" to "zu"
+)
+
+val languageCodeChoices = languageCodes.map {CommandChoice(it.key, it.value)}
\ No newline at end of file
From 9d3dbf832d0f9f710684e474368c63652e1e04f4 Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 07:51:44 +0700
Subject: [PATCH 2/8] feat: Ty
---
build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt b/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt
index fa1558c..b15cd3f 100644
--- a/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt
+++ b/build-logic/src/main/kotlin/com/aliucord/gradle/repo/Authors.kt
@@ -39,7 +39,7 @@ object Authors {
val js6pak = Author("6pak", 141580516380901376L)
val RazerTexz = Author("RazerTexz", 633565155501801472L)
val LampDelivery = Author("LampDelivery", 650805815623680030L)
- val Ty = author("Ty", 487443883127472129L)
+ val Ty = Author("Ty", 487443883127472129L)
}
/**
From 7154e39b69f5ee9e3c925b4dd9fbfe13c300da2c Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 07:55:07 +0700
Subject: [PATCH 3/8] feat: Ty
---
plugins/ActionAlerts/README.md | 3 ---
.../ActionAlerts/src/main/AndroidManifest.xml | 2 --
plugins/CallTime/README.md | 3 ---
plugins/CallTime/src/main/AndroidManifest.xml | 2 --
plugins/EncryptDMs/README.md | 24 -------------------
.../EncryptDMs/src/main/AndroidManifest.xml | 2 --
plugins/ReverseImageSearch/README.md | 3 ---
.../src/main/AndroidManifest.xml | 2 --
plugins/RoleColorContrast/README.md | 11 ---------
.../src/main/AndroidManifest.xml | 2 --
plugins/ShowNames/README.md | 3 ---
.../ShowNames/src/main/AndroidManifest.xml | 2 --
plugins/Template/build.gradle.kts | 22 -----------------
.../com/aliucord/plugins/template/Template.kt | 14 -----------
plugins/Texter/README.md | 3 ---
plugins/Texter/src/main/AndroidManifest.xml | 2 --
plugins/Translate/README.md | 3 ---
.../Translate/src/main/AndroidManifest.xml | 2 --
18 files changed, 105 deletions(-)
delete mode 100644 plugins/ActionAlerts/README.md
delete mode 100644 plugins/ActionAlerts/src/main/AndroidManifest.xml
delete mode 100644 plugins/CallTime/README.md
delete mode 100644 plugins/CallTime/src/main/AndroidManifest.xml
delete mode 100644 plugins/EncryptDMs/README.md
delete mode 100644 plugins/EncryptDMs/src/main/AndroidManifest.xml
delete mode 100644 plugins/ReverseImageSearch/README.md
delete mode 100644 plugins/ReverseImageSearch/src/main/AndroidManifest.xml
delete mode 100644 plugins/RoleColorContrast/README.md
delete mode 100644 plugins/RoleColorContrast/src/main/AndroidManifest.xml
delete mode 100644 plugins/ShowNames/README.md
delete mode 100644 plugins/ShowNames/src/main/AndroidManifest.xml
delete mode 100644 plugins/Template/build.gradle.kts
delete mode 100644 plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt
delete mode 100644 plugins/Texter/README.md
delete mode 100644 plugins/Texter/src/main/AndroidManifest.xml
delete mode 100644 plugins/Translate/README.md
delete mode 100644 plugins/Translate/src/main/AndroidManifest.xml
diff --git a/plugins/ActionAlerts/README.md b/plugins/ActionAlerts/README.md
deleted file mode 100644
index 2085659..0000000
--- a/plugins/ActionAlerts/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# ShowNames
-
-A plugin that changes the color of usernames to stop them from blending into the background.
\ No newline at end of file
diff --git a/plugins/ActionAlerts/src/main/AndroidManifest.xml b/plugins/ActionAlerts/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/ActionAlerts/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/CallTime/README.md b/plugins/CallTime/README.md
deleted file mode 100644
index 79a6151..0000000
--- a/plugins/CallTime/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# CallTime
-
-A plugin that shows the time you have been in a VC for
\ No newline at end of file
diff --git a/plugins/CallTime/src/main/AndroidManifest.xml b/plugins/CallTime/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/CallTime/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/EncryptDMs/README.md b/plugins/EncryptDMs/README.md
deleted file mode 100644
index b3d9ed7..0000000
--- a/plugins/EncryptDMs/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# EncryptDMs
-
-Encrypts your dms
-
-## How it works
-There are 5 keys:
-- RSA-4096 public and private (for encrpting the AES key)
-- RSA-4096 public and private (for signing messages)
-- AES-265 (
-
-### Initial setup
-1. Both users send their RSA public keys (both encrytion and signing)
-2. Both users save the other's RSA public key (both encrytion and signing)
-3. User 1 generates an AES-256 key
-4. User 1 encrypts the AES key with User 2's public encryption key, and then signs with User 1's private signing key
-5. User 1 sends encrypted message
-6. User 2 verifies the encrypted AES key with User 1's public signing key and then decrypts with User 2's private encryption key and saves the decrypted AES key
-7. Both users now have the AES key
-
-### DM encryption
-1. User 1 encrypts message with AES-256 key
-2. User 1 signs the AES encrypted message with their RSA private signing key
-3. User 1 sends encrypted message
-4. User 2 verifies the signature with User 1's public signing key and then decrypts with the saved AES-256 key
diff --git a/plugins/EncryptDMs/src/main/AndroidManifest.xml b/plugins/EncryptDMs/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/EncryptDMs/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/README.md b/plugins/ReverseImageSearch/README.md
deleted file mode 100644
index 638554b..0000000
--- a/plugins/ReverseImageSearch/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# ReverseImageSearch
-
-Allows you to revere image search by long pressing the "open in browser" button on images.
\ No newline at end of file
diff --git a/plugins/ReverseImageSearch/src/main/AndroidManifest.xml b/plugins/ReverseImageSearch/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/ReverseImageSearch/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/RoleColorContrast/README.md b/plugins/RoleColorContrast/README.md
deleted file mode 100644
index 4beb641..0000000
--- a/plugins/RoleColorContrast/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# RoleColorContrast
-
-A plugin to increase role color contrasts, by attempting to make them adhere to the WCAG AA or AAA guidelines (configurable).
-
-## Compatibility
-
-This is compatible with basic Themer themes, so long as they use dark mode and do not use transparency. Transparency support is entirely too difficult and so please don't ask.
-
-## Adjusted UI elements
-
-* Names in chat
\ No newline at end of file
diff --git a/plugins/RoleColorContrast/src/main/AndroidManifest.xml b/plugins/RoleColorContrast/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/RoleColorContrast/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/ShowNames/README.md b/plugins/ShowNames/README.md
deleted file mode 100644
index 2085659..0000000
--- a/plugins/ShowNames/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# ShowNames
-
-A plugin that changes the color of usernames to stop them from blending into the background.
\ No newline at end of file
diff --git a/plugins/ShowNames/src/main/AndroidManifest.xml b/plugins/ShowNames/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/ShowNames/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/Template/build.gradle.kts b/plugins/Template/build.gradle.kts
deleted file mode 100644
index 90fcd54..0000000
--- a/plugins/Template/build.gradle.kts
+++ /dev/null
@@ -1,22 +0,0 @@
-import com.aliucord.gradle.repo.Authors
-import com.aliucord.gradle.repo.author
-
-plugins {
- id("com.aliucord.plugins-repo")
-}
-
-version = "1.0.0"
-description = "Template plugin"
-
-aliucord {
- author(Authors.Aliucord)
-
- changelog.set(
- """
- # 1.0.0
- * Initial plugin release!
- """.trimIndent(),
- )
-
- deploy.set(false)
-}
diff --git a/plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt b/plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt
deleted file mode 100644
index dad753f..0000000
--- a/plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.aliucord.plugins.template
-
-import android.content.Context
-import com.aliucord.annotations.AliucordPlugin
-import com.aliucord.entities.Plugin
-
-@AliucordPlugin(requiresRestart = false)
-@Suppress("unused")
-class Template : Plugin() {
- override fun start(context: Context) {}
- override fun stop(context: Context) {
- patcher.unpatchAll()
- }
-}
diff --git a/plugins/Texter/README.md b/plugins/Texter/README.md
deleted file mode 100644
index d49541b..0000000
--- a/plugins/Texter/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Texter
-
-A port of [SkyblockDev/texter](https://github.com/SkyBlockDev/texter) to Aliucord
\ No newline at end of file
diff --git a/plugins/Texter/src/main/AndroidManifest.xml b/plugins/Texter/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/Texter/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
diff --git a/plugins/Translate/README.md b/plugins/Translate/README.md
deleted file mode 100644
index 2c03636..0000000
--- a/plugins/Translate/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Translate
-
-Adds a message context menu action to translate the message to english (configurable later). You can also use /translate to send translated text, or just view it locally.
\ No newline at end of file
diff --git a/plugins/Translate/src/main/AndroidManifest.xml b/plugins/Translate/src/main/AndroidManifest.xml
deleted file mode 100644
index ec11743..0000000
--- a/plugins/Translate/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
\ No newline at end of file
From 7f7b12bab92edc3658c41fb5c513c1c6997689ff Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 08:01:31 +0700
Subject: [PATCH 4/8] feat: Ty
---
.../tyman/plugins/reverseimagesearch/ReverseImageSearch.kt | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
index 609c495..d1f2d5c 100644
--- a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
+++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
@@ -25,9 +25,11 @@ class ReverseImageSearch : Plugin() {
}
override fun load(ctx: Context) {
+ val res = resources!!
+
pluginIcon = ResourcesCompat.getDrawable(
- resources,
- resources.getIdentifier("ic_baseline_image_search_24", "drawable", "com.aliucord.plugins"),
+ res,
+ res.getIdentifier("ic_baseline_image_search_24", "drawable", "com.aliucord.plugins"),
null
)!!
}
From 54ad55f4e2d3e60150598892207e533a5e4b4698 Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 08:05:10 +0700
Subject: [PATCH 5/8] feat: Ty
---
.../src/main/kotlin/tech/tyman/plugins/translate/Translate.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt
index aaea69e..c1ea364 100644
--- a/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt
+++ b/plugins/Translate/src/main/kotlin/tech/tyman/plugins/translate/Translate.kt
@@ -180,7 +180,7 @@ class Translate : Plugin() {
})
}
- override fun stop(context: Context?) = patcher.unpatchAll()
+ override fun stop(context: Context) = patcher.unpatchAll()
private fun translateMessage(text: String, from: String? = null, to: String? = null): TranslateData {
val toLang = to ?: settings.getString("defaultLanguage", "en")
From 6c78e8dda21f542f8da44d8afd42e744897aee39 Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 08:11:01 +0700
Subject: [PATCH 6/8] feat: Ty
---
plugins/ReverseImageSearch/build.gradle.kts | 4 +++-
.../tyman/plugins/reverseimagesearch/ReverseImageSearch.kt | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/plugins/ReverseImageSearch/build.gradle.kts b/plugins/ReverseImageSearch/build.gradle.kts
index 0c8d768..9785116 100644
--- a/plugins/ReverseImageSearch/build.gradle.kts
+++ b/plugins/ReverseImageSearch/build.gradle.kts
@@ -5,11 +5,13 @@ plugins {
id("com.aliucord.plugins-repo")
}
-version = "1.0.3"
+version = "1.0.4"
description = "A plugin that adds a button for reverse image searching a sent image."
aliucord {
changelog.set("""
+ # Version 1.0.4
+ * Fix reverse searching not working on expired discord urls and images inside embeds (thanks Delphox)
# Version 1.0.3
* Replace google images search with google lens, as the old images URL no longer works (thanks Delphox)
# Version 1.0.2
diff --git a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
index d1f2d5c..5427d6d 100644
--- a/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
+++ b/plugins/ReverseImageSearch/src/main/kotlin/tech/tyman/plugins/reverseimagesearch/ReverseImageSearch.kt
@@ -43,7 +43,7 @@ class ReverseImageSearch : Plugin() {
val btnId = Utils.getResId("menu_media_browser", "id")
val btn = Utils.appActivity.findViewById(btnId)
btn.setOnLongClickListener {
- val url = mostRecentIntent.getStringExtra("INTENT_MEDIA_URL") ?: return@setOnLongClickListener false
+ val url = mostRecentIntent.getStringExtra("INTENT_IMAGE_URL") ?: return@setOnLongClickListener false
if (settings.engine == Engine.ASK) {
EngineSheet(url)
.show(Utils.appActivity.supportFragmentManager, "Reverse Image Search")
From e7ac5c9436f91e8fc1c964d10b77d6c80251a895 Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 08:14:43 +0700
Subject: [PATCH 7/8] feat: Ty
---
plugins/ActionAlerts/build.gradle.kts | 18 -
.../plugins/actionalerts/ActionAlerts.kt | 53 --
plugins/EncryptDMs/build.gradle.kts | 18 -
.../tyman/plugins/encryptdms/EncryptDMs.kt | 468 ------------------
.../tyman/plugins/encryptdms/Encryption.kt | 98 ----
.../tech/tyman/plugins/encryptdms/Utils.kt | 37 --
plugins/ShowNames/build.gradle.kts | 30 --
.../tech/tyman/plugins/shownames/ShowNames.kt | 49 --
8 files changed, 771 deletions(-)
delete mode 100644 plugins/ActionAlerts/build.gradle.kts
delete mode 100644 plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt
delete mode 100644 plugins/EncryptDMs/build.gradle.kts
delete mode 100644 plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt
delete mode 100644 plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt
delete mode 100644 plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt
delete mode 100644 plugins/ShowNames/build.gradle.kts
delete mode 100644 plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt
diff --git a/plugins/ActionAlerts/build.gradle.kts b/plugins/ActionAlerts/build.gradle.kts
deleted file mode 100644
index ad0bf0a..0000000
--- a/plugins/ActionAlerts/build.gradle.kts
+++ /dev/null
@@ -1,18 +0,0 @@
-import com.aliucord.gradle.repo.Authors
-import com.aliucord.gradle.repo.author
-
-plugins {
- id("com.aliucord.plugins-repo")
-}
-
-version = "1.0.0"
-description = "A plugin that alerts you for certain actions (ie being banned/kicked from a server, friend removals)"
-
-aliucord {
- changelog.set("""
- # Version 1.0.0
- * Initial release
- """.trimIndent())
-
- author(Authors.Ty)
-}
\ No newline at end of file
diff --git a/plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt b/plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt
deleted file mode 100644
index 3160f83..0000000
--- a/plugins/ActionAlerts/src/main/kotlin/tech/tyman/plugins/actionalerts/ActionAlerts.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package tech.tyman.plugins.actionalerts
-
-import android.content.Context
-import com.aliucord.annotations.AliucordPlugin
-import com.aliucord.api.NotificationsAPI
-import com.aliucord.entities.NotificationData
-import com.aliucord.entities.Plugin
-import com.aliucord.utils.RxUtils.await
-import com.aliucord.utils.RxUtils.subscribe
-import com.discord.gateway.GatewaySocket
-import com.discord.models.domain.ModelUserRelationship
-import com.discord.models.user.CoreUser
-import com.discord.restapi.RestAPIParams
-import com.discord.stores.StoreBans
-import com.discord.stores.StoreStream
-import com.discord.stores.StoreUserRelationships
-import com.discord.utilities.rest.RestAPI
-import rx.Subscription
-
-@AliucordPlugin
-class ActionAlerts : Plugin() {
- private var relationshipCache = mutableMapOf()
- private lateinit var relationshipListener: Subscription
-
- override fun start(context: Context) {
- relationshipListener = StoreStream.getUserRelationships().observe().subscribe {
- if (relationshipCache.size > this.size) {
- val removed = relationshipCache.entries.find {
- !this.keys.contains(it.key)
- } ?: return@subscribe
- val user = StoreStream.getUsers().users[removed.key] ?: CoreUser(
- RestAPI.getApi().userGet(removed.key).await().first ?: return@subscribe
- )
- if (removed.value != ModelUserRelationship.TYPE_FRIEND) {
- NotificationsAPI.display(
- NotificationData().apply {
- title = "ActionAlerts"
- body = "${user.username}#${String.format("%04d", user.discriminator)} removed you as a friend."
- autoDismissPeriodSecs = 5
- onClick = {}
- }
- )
- }
- }
- relationshipCache = this
- }
- }
-
- override fun stop(context: Context) {
- patcher.unpatchAll()
- relationshipListener.unsubscribe()
- }
-}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/build.gradle.kts b/plugins/EncryptDMs/build.gradle.kts
deleted file mode 100644
index 7b1b914..0000000
--- a/plugins/EncryptDMs/build.gradle.kts
+++ /dev/null
@@ -1,18 +0,0 @@
-import com.aliucord.gradle.repo.Authors
-import com.aliucord.gradle.repo.author
-
-plugins {
- id("com.aliucord.plugins-repo")
-}
-
-version = "0.0.1"
-description = "Encryption go brrrrrrrrrrrrr"
-
-aliucord {
- changelog.set("""
- # Version 1.0.0
- * Initial release
- """.trimIndent())
-
- author(Authors.Ty)
-}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt
deleted file mode 100644
index 497f5c2..0000000
--- a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/EncryptDMs.kt
+++ /dev/null
@@ -1,468 +0,0 @@
-package tech.tyman.plugins.encryptdms
-
-import android.content.Context
-import android.view.View
-import com.aliucord.annotations.AliucordPlugin
-import com.aliucord.entities.Plugin
-import com.aliucord.patcher.after
-import com.discord.widgets.channels.list.WidgetChannelsListItemChannelActions
-import com.lytefast.flexinput.R
-import com.aliucord.wrappers.ChannelWrapper.Companion.id
-import android.widget.TextView
-import android.widget.LinearLayout
-import androidx.core.content.ContextCompat
-import com.discord.databinding.WidgetChannelsListItemActionsBinding
-import androidx.core.widget.NestedScrollView
-import com.aliucord.Utils
-import com.aliucord.api.CommandsAPI
-import com.aliucord.api.PatcherAPI
-import com.aliucord.api.SettingsAPI
-import com.aliucord.patcher.before
-import com.aliucord.settings.delegate
-import com.aliucord.utils.DimenUtils
-import com.aliucord.wrappers.ChannelWrapper.Companion.recipients
-import com.discord.api.commands.ApplicationCommandType
-import com.discord.api.message.LocalAttachment
-import com.discord.api.message.Message
-import com.discord.api.message.attachment.MessageAttachment
-import com.discord.databinding.WidgetChatListActionsBinding
-import com.discord.models.user.CoreUser
-import com.discord.stores.StoreStream
-import com.discord.utilities.SnowflakeUtils
-import com.discord.utilities.attachments.AttachmentUtilsKt
-import com.discord.widgets.chat.MessageContent
-import com.discord.widgets.chat.MessageManager
-import com.discord.widgets.chat.input.ChatInputViewModel
-import com.discord.widgets.chat.list.actions.WidgetChatListActions
-import com.discord.models.message.Message as ModelMessage
-import com.lytefast.flexinput.model.Attachment
-import java.io.File
-
-@AliucordPlugin
-class EncryptDMs : Plugin() {
- // AES keys
- private var SettingsAPI.aesKeys: HashMap by settings.delegate(HashMap())
-
- private fun SettingsAPI.getKey(channel: Long) = this.aesKeys[channel.toString()]
- private fun SettingsAPI.setKey(channel: Long, key: String) {
- val keys = aesKeys.clone() as HashMap
- keys[channel.toString()] = key
- this.aesKeys = keys
- }
-
- // RSA keys (encryption)
- private var SettingsAPI.pubKeys: HashMap by settings.delegate(HashMap())
- private var SettingsAPI.publicKey: String by settings.delegate("")
- private var SettingsAPI.privateKey: String by settings.delegate("")
-
- private fun SettingsAPI.getPubKey(user: Long) = this.pubKeys[user.toString()]
- private fun SettingsAPI.setPubKey(user: Long, key: String) {
- val keys = this.pubKeys.clone() as HashMap
- keys[user.toString()] = key
- this.pubKeys = keys
- }
-
- // RSA keys (signing)
- private var SettingsAPI.pubSigningKeys: HashMap by settings.delegate(HashMap())
- private var SettingsAPI.publicSigningKey: String by settings.delegate("")
- private var SettingsAPI.privateSigningKey: String by settings.delegate("")
-
- private fun SettingsAPI.getPubSigningKey(user: Long) = this.pubSigningKeys[user.toString()]
- private fun SettingsAPI.setPubSigningKey(user: Long, key: String) {
- val keys = this.pubSigningKeys.clone() as HashMap
- keys[user.toString()] = key
- this.pubSigningKeys = keys
- }
-
- // Generic settings
- private var SettingsAPI.enabledChannels: ArrayList by settings.delegate(arrayListOf())
-
- private fun SettingsAPI.enableChannel(channel: Long) {
- val channels = this.enabledChannels.clone() as ArrayList
- channels.add(channel)
- this.enabledChannels = channels
- }
- private fun SettingsAPI.disableChannel(channel: Long) {
- val channels = this.enabledChannels.clone() as ArrayList
- channels.remove(channel)
- this.enabledChannels = channels
- }
-
- private val curChannel: Long
- get() = StoreStream.getChannelsSelected().id
-
- override fun start(context: Context) {
- // Patch context menus
- patcher.patchDirectMessageChannelActions()
- patcher.patchMessageActions()
- // Patch sent messages
- patcher.patchSentMessages()
- // Patch incoming messages
- patcher.patchIncomingMessages()
-
- // Add commands
- commands.registerCommand(
- "sendpubkeys",
- "Sends your public keys (both encryption and signing) in the chat, and if either private or public keys are absent then regenerates them"
- ) {
- // Encryption keys
- var publicKey = settings.publicKey
- if (publicKey == "" || settings.privateKey == "") {
- genRsaKeys().run {
- settings.publicKey = public.asBase64()
- settings.privateKey = private.asBase64()
- publicKey = public.asBase64()
- }
- }
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(publicKey)
- it.addAttachment(
- this.toURI().toASCIIString(),
- "publickey.txt"
- )
- }
- // Signing keys
- var publicSigningKey = settings.publicSigningKey
- if (publicSigningKey == "" || settings.privateSigningKey == "") {
- genRsaKeys().run {
- settings.publicSigningKey = public.asBase64()
- settings.privateSigningKey = private.asBase64()
- publicSigningKey = public.asBase64()
- }
- }
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(publicSigningKey)
- it.addAttachment(
- this.toURI().toASCIIString(),
- "publicsigningkey.txt"
- )
- }
- return@registerCommand CommandsAPI.CommandResult(
- "",
- null,
- true
- )
- }
-
- commands.registerCommand(
- "sendkey",
- "Sends your generated channel key in the chat, encrypted with the other user's public key, so that the other user can decrypt and save it.",
- listOf(
- Utils.createCommandOption(
- type = ApplicationCommandType.STRING,
- name = "channel id",
- description = "The channel id of the DM",
- required = true
- ),
- Utils.createCommandOption(
- type = ApplicationCommandType.USER,
- name = "user",
- description = "The user to send this to",
- required = true
- )
- )
- ) {
- val userPubKey = settings.getPubKey(it.getRequiredUser("user").id)
- ?: return@registerCommand CommandsAPI.CommandResult("This user's public key is not saved, ask them to run /sendpubkey, and then press and hold on their message to save their key!")
- val channelKey = settings.getKey(it.getRequiredString("channel id").toLong())
- ?: return@registerCommand CommandsAPI.CommandResult("This DM does not have a key generated! Press and hold on the DM in the list, and generate the key.")
-
- val encryptedText = userPubKey.asPublicKey().encrypt(channelKey.asKey().encoded)
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(encryptedText)
- it.addAttachment(
- this.toURI().toASCIIString(),
- "aeskey.txt"
- )
- }
- var privateSigningKey = settings.privateSigningKey
- if (privateSigningKey == "" || settings.publicSigningKey == "") {
- genRsaKeys().run {
- settings.publicSigningKey = public.asBase64()
- settings.privateSigningKey = private.asBase64()
- privateSigningKey = private.asBase64()
- }
- }
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(privateSigningKey.asPrivateKey().sign(encryptedText))
- it.addAttachment(
- this.toURI().toASCIIString(),
- "signature.txt"
- )
- }
-
- return@registerCommand CommandsAPI.CommandResult(
- "",
- null,
- true
- )
- }
- }
-
- private fun PatcherAPI.patchDirectMessageChannelActions() {
- this.after("configureUI", WidgetChannelsListItemChannelActions.Model::class.java) {
- val channelId = (it.args[0] as WidgetChannelsListItemChannelActions.Model).channel.id
-
- val state =
- if (settings.getKey(channelId) == null) ChannelState.NO_KEY
- else if (settings.enabledChannels.contains(channelId)) ChannelState.ENABLED
- else ChannelState.DISABLED
-
- val nestedScrollView = this.requireView() as NestedScrollView
- val layout = nestedScrollView.getChildAt(0) as LinearLayout
- val binding = com.discord.widgets.channels.list.WidgetChannelsListItemChannelActions::class.java.getDeclaredMethod("getBinding")
- .apply { isAccessible = true }
- .invoke(this) as WidgetChannelsListItemActionsBinding
- val view = binding.j
- val param = view.layoutParams
- val params = LinearLayout.LayoutParams(param.width, param.height)
- params.leftMargin = DimenUtils.dpToPx(20)
- val tw = TextView(view.context, null, 0, R.i.UiKit_Settings_Item_Icon)
- tw.text = when (state) {
- ChannelState.NO_KEY -> "Setup encrypted DMs (EncryptDMs)"
- ChannelState.DISABLED -> "Enable encrypted DMs (EncryptDMs)"
- ChannelState.ENABLED -> "Disable encrypted DMs (EncryptDMs)"
- }
- tw.setCompoundDrawablesRelativeWithIntrinsicBounds(ContextCompat.getDrawable(view.context, R.e.avd_show_password), null, null, null)
- tw.layoutParams = view.layoutParams
- tw.id = View.generateViewId()
- tw.setOnClickListener {
- when (state) {
- ChannelState.NO_KEY -> {
- val key = genAesKey().asHex()
- settings.setKey(channelId, key)
- Utils.showToast("Key saved for channel. To send this key to the other user, tell the other user to run the /sendpubkey command, press and hold their message, and then run the /sendkey command.", true)
- this.dismiss()
- }
- ChannelState.DISABLED -> {
- settings.enableChannel(channelId)
- Utils.showToast("Enabled encrypted DMs for this channel! Sent messages will now be encrypted.")
- this.dismiss()
- }
- ChannelState.ENABLED -> {
- settings.disableChannel(channelId)
- Utils.showToast("Disabled encrypted DMs for this channel. Sent messages will no longer be encrypted.")
- this.dismiss()
- }
- }
- }
- layout.addView(tw)
- }
- }
-
- private fun PatcherAPI.patchMessageActions() {
- this.after("configureUI", WidgetChatListActions.Model::class.java) {
- val message = (it.args[0] as WidgetChatListActions.Model).message
- val userId = CoreUser(message.author).id
- val content = message.content
- if (
- !listOf(
- "",
- ""
- ).contains(content)
- ) return@after // If message is not relevant ignore it
- val nestedScrollView = this.requireView() as NestedScrollView
- val layout = nestedScrollView.getChildAt(0) as LinearLayout
- val binding = WidgetChatListActions::class.java.getDeclaredMethod("getBinding")
- .apply { isAccessible = true }
- .invoke(this) as WidgetChatListActionsBinding
- val view = binding.j
- val param = view.layoutParams
- val params = LinearLayout.LayoutParams(param.width, param.height)
- params.leftMargin = DimenUtils.dpToPx(20)
- val tw = TextView(view.context, null, 0, R.i.UiKit_Settings_Item_Icon)
- tw.text = when (content) {
- "" -> "Save public keys for user (EncryptDMs)"
- "" -> "Save channel key from user (EncryptDMs)"
- else -> "This should never happen wtf"
- }
- tw.setCompoundDrawablesRelativeWithIntrinsicBounds(ContextCompat.getDrawable(view.context, R.e.avd_show_password), null, null, null)
- tw.layoutParams = view.layoutParams
- tw.id = View.generateViewId()
- tw.setOnClickListener {
- when (content) {
- "" -> Utils.threadPool.execute {
- val key = message.getAttachmentText("publickey.txt") ?: return@execute
- settings.setPubKey(userId, key)
- val signingKey = message.getAttachmentText("publicsigningkey.txt") ?: return@execute
- settings.setPubSigningKey(userId, signingKey)
- Utils.showToast("Public keys saved for user.")
- this.dismiss()
- }
- "" -> Utils.threadPool.execute {
- // Encrypted text
- val encryptedText = message.getAttachmentText("aeskey.txt") ?: return@execute
- // Signature verification
- val signature = message.getAttachmentText("signature.txt") ?: return@execute
- val publicSigningKey = settings.getPubSigningKey(userId)
- ?: return@execute Utils.showToast("Public signing key for user was not found, make sure you have saved their public keys first!", true)
- val verified = try {
- publicSigningKey.asPublicKey().verify(encryptedText, signature)
- } catch (e: Exception) {
- logger.error("Verification error:", e)
- Utils.showToast("Error occurred while verifying signature, message could be modified, not saving key", true)
- return@execute
- }
- if (!verified) {
- Utils.showToast("Signature verification failed, message could be modified, not saving key", true)
- return@execute
- }
- val key = settings.privateKey.asPrivateKey()
- settings.setKey(message.channelId, key.decrypt(encryptedText))
- Utils.showToast("Key decrypted and saved for this channel.")
- this.dismiss()
- }
- }
- }
- layout.addView(tw)
- }
- }
-
- private val textContentField =
- MessageContent::class.java.getDeclaredField("textContent").apply {
- isAccessible = true
- }
-
- private fun MessageContent.set(text: String) = textContentField.set(this, text)
-
- private fun PatcherAPI.patchSentMessages() {
- this.before(
- "sendMessage",
- Context::class.java,
- MessageManager::class.java,
- MessageContent::class.java,
- List::class.java,
- Boolean::class.javaPrimitiveType!!,
- Function1::class.java
- ) {
- if (!settings.enabledChannels.contains(curChannel)) return@before
- val recipients = StoreStream.getChannelsSelected().selectedChannel.recipients
- ?: return@before // This is not useless fuck you android studio
- if (recipients.size != 1) return@before
- val content = it.args[2] as MessageContent
- val context = it.args[0] as Context
- @Suppress("UNCHECKED_CAST")
- val attachments = (it.args[3] as List>).toMutableList()
- val plainText = content.textContent
-
- // Encrypt with AES
- val key = settings.getKey(curChannel)
- ?: run {
- Utils.showToast("There is no key saved for this DM! Make sure to properly set it up with the other user first!", true)
- it.result = null
- return@before
- }
- val encryptedText = key.asKey().encrypt(plainText)
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(encryptedText.second)
- attachments.add(
- AttachmentUtilsKt.toAttachment(
- LocalAttachment(
- SnowflakeUtils.fromTimestamp(System.currentTimeMillis()),
- this.toURI().toASCIIString(),
- "message.txt"
- )
- )
- )
- }
- // Add initialization vector because encryption
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(encryptedText.first.asHex())
- attachments.add(
- AttachmentUtilsKt.toAttachment(
- LocalAttachment(
- SnowflakeUtils.fromTimestamp(System.currentTimeMillis()),
- this.toURI().toASCIIString(),
- "iv.txt"
- )
- )
- )
- }
- // Sign with RSA
- var privateSigningKey = settings.privateSigningKey
- if (privateSigningKey == "" || settings.publicSigningKey == "") {
- genRsaKeys().run {
- settings.publicSigningKey = public.asBase64()
- settings.privateSigningKey = private.asBase64()
- privateSigningKey = private.asBase64()
- }
- }
- with (File.createTempFile("encryptdms", null, context.cacheDir)) {
- this.writeText(privateSigningKey.asPrivateKey().sign(encryptedText.second))
- attachments.add(
- AttachmentUtilsKt.toAttachment(
- LocalAttachment(
- SnowflakeUtils.fromTimestamp(System.currentTimeMillis()),
- this.toURI().toASCIIString(),
- "signature.txt"
- )
- )
- )
- }
- // Set attachments and content
- it.args[3] = attachments
- content.set("")
- it.args[2] = content
- }
- }
-
- private val contentField = Message::class.java.getDeclaredField("content").apply { isAccessible = true }
- private val attachmentsField = Message::class.java.getDeclaredField("attachments").apply { isAccessible = true }
-
- private fun PatcherAPI.patchIncomingMessages() {
- // Hide attachments if message is encrypted
- patcher.before(
- "handleMessageCreate",
- Message::class.java
- ) {
- val apiMessage = it.args[0] as Message
- val message = ModelMessage(apiMessage)
- if (message.content != "") return@before
- Utils.threadPool.execute {
- val response = run {
- // Encrypted text
- val encryptedText = message.getAttachmentTextWithError("message.txt")
- if (!encryptedText.first) return@run encryptedText.second
- // Signature verification
- val signature = message.getAttachmentTextWithError("signature.txt")
- if (!signature.first) return@run signature.second
- val publicSigningKey = settings.getPubSigningKey(CoreUser(message.author).id)
- ?: return@run "Public signing key for user was not found, make sure you have saved their public keys first!"
- val verified = try {
- publicSigningKey.asPublicKey().verify(encryptedText.second, signature.second)
- } catch (e: Exception) {
- logger.error("Verification error:", e)
- return@run "Error occurred while verifying signature, message could be modified"
- }
- if (!verified) {
- return@run "Signature verification failed, message could be modified, not saving key"
- }
- // Initialization vector
- val iv = message.getAttachmentTextWithError("iv.txt")
- if (!iv.first) return@run iv.second
- // Decrypt
- val key = settings.getKey(message.channelId)
- ?: return@run "Key for channel not found, make sure it has been sent by the other user and saved by you!"
- return@run key.asKey().decrypt(encryptedText.second, iv.second.asByteArray())
- }
- contentField[apiMessage] = response
- @Suppress("UNCHECKED_CAST")
- val attachments = attachmentsField[apiMessage] as MutableList
- attachments.clear()
- StoreStream.`access$handleMessageCreate`(this, apiMessage)
- }
- it.result = null
- }
- }
-
- override fun stop(context: Context) {
- patcher.unpatchAll()
- commands.unregisterAll()
- }
-}
-
-enum class ChannelState {
- NO_KEY,
- DISABLED,
- ENABLED
-}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt
deleted file mode 100644
index 2dd0e37..0000000
--- a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Encryption.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package tech.tyman.plugins.encryptdms
-
-import android.util.Base64
-import java.security.*
-import java.security.spec.PKCS8EncodedKeySpec
-import java.security.spec.X509EncodedKeySpec
-import javax.crypto.Cipher
-import javax.crypto.KeyGenerator
-import javax.crypto.SecretKey
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.SecretKeySpec
-
-fun genAesKey(): SecretKey {
- val keyGenerator = KeyGenerator.getInstance("AES")
- keyGenerator.init(256)
- return keyGenerator.generateKey()
-}
-
-fun Key.asHex() = this.encoded.asHex()
-fun ByteArray.asHex() = this.joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
-fun Key.asBase64() = Base64.encodeToString(this.encoded, Base64.DEFAULT)
-
-fun String.asByteArray() = this.chunked(2)
- .map { it.toInt(16).toByte() }
- .toByteArray()
-fun String.asKey(): SecretKey {
- val encodedKey = this.asByteArray()
- return SecretKeySpec(encodedKey, 0, encodedKey.size, "AES")
-}
-
-fun genRsaKeys(): KeyPair {
- val generator = KeyPairGenerator.getInstance("RSA")
- generator.initialize(4096)
- return generator.generateKeyPair()
-}
-
-fun String.asPublicKey() =
- KeyFactory.getInstance("RSA").generatePublic(X509EncodedKeySpec(Base64.decode(this, Base64.DEFAULT)))
-
-fun String.asPrivateKey() =
- KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(Base64.decode(this, Base64.DEFAULT)))
-
-fun PublicKey.encrypt(text: ByteArray): String {
- val cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING")
- cipher.init(Cipher.PUBLIC_KEY, this)
- return Base64.encodeToString(cipher.doFinal(text), Base64.DEFAULT)
-}
-
-fun PrivateKey.decrypt(text: String): String {
- val cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING")
- cipher.init(Cipher.PRIVATE_KEY, this)
- return cipher.doFinal(Base64.decode(text, Base64.DEFAULT)).asHex()
-}
-
-fun PrivateKey.sign(text: String): String {
- val signature = Signature.getInstance("SHA512WithRSA")
- signature.initSign(this)
- signature.update(text.toByteArray())
- return Base64.encodeToString(signature.sign(), Base64.DEFAULT)
-}
-
-fun PublicKey.verify(text: String, signature: String): Boolean {
- val signatureInstance = Signature.getInstance("SHA512WithRSA")
- signatureInstance.initVerify(this)
- signatureInstance.update(text.toByteArray())
-
- return signatureInstance.verify(Base64.decode(signature, Base64.DEFAULT))
-}
-
-fun SecretKey.encrypt(text: String): Pair {
- // Get cipher
- val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
- // Get key
- val keySpec = SecretKeySpec(this.encoded, "AES")
- // Generate initialization vector
- val iv = ByteArray(16)
- SecureRandom().nextBytes(iv)
- val ivSpec = IvParameterSpec(iv)
- // Encrypt
- cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
- return Pair(iv, Base64.encodeToString(cipher.doFinal(text.toByteArray()), Base64.DEFAULT))
-}
-
-fun SecretKey.decrypt(encryptedText: String, iv: ByteArray): String {
- // Get cipher
- val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
- // Get key
- val keySpec = SecretKeySpec(this.encoded, "AES")
- // Set initialization vector
- val ivSpec = IvParameterSpec(iv)
- // Decrypt
- cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
- return String(
- cipher.doFinal(
- Base64.decode(encryptedText, Base64.DEFAULT)
- )
- )
-}
\ No newline at end of file
diff --git a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt b/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt
deleted file mode 100644
index 24670fc..0000000
--- a/plugins/EncryptDMs/src/main/kotlin/tech/tyman/plugins/encryptdms/Utils.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package tech.tyman.plugins.encryptdms
-
-import com.aliucord.Http
-import com.aliucord.Logger
-import com.aliucord.Utils
-import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.filename
-import com.aliucord.wrappers.messages.AttachmentWrapper.Companion.url
-import com.discord.models.message.Message
-import java.io.IOException
-
-fun Message.getAttachmentText(name: String): String? {
- val url = this.attachments.find { attachment -> attachment.filename == name }?.url
- ?: run {
- Utils.showToast("Unable to find $name attachment for some reason", true)
- return null
- }
- return try {
- Http.simpleGet(url)
- } catch (e: IOException) {
- Logger("EncryptDMs").error(e)
- Utils.showToast("Unable to load $name attachment for some reason", true)
- return null
- }
-}
-
-fun Message.getAttachmentTextWithError(name: String): Pair {
- val url = this.attachments.find { attachment -> attachment.filename == name }?.url
- ?: run {
- return Pair(false, "Unable to find $name attachment for some reason")
- }
- return try {
- Pair(true, Http.simpleGet(url))
- } catch (e: IOException) {
- Logger("EncryptDMs").error(e)
- return Pair(false, "Unable to load $name attachment for some reason")
- }
-}
\ No newline at end of file
diff --git a/plugins/ShowNames/build.gradle.kts b/plugins/ShowNames/build.gradle.kts
deleted file mode 100644
index 3cf399b..0000000
--- a/plugins/ShowNames/build.gradle.kts
+++ /dev/null
@@ -1,30 +0,0 @@
-import com.aliucord.gradle.repo.Authors
-import com.aliucord.gradle.repo.author
-
-plugins {
- id("com.aliucord.plugins-repo")
-}
-
-version = "1.0.6"
-description = "A plugin that changes the color of usernames to stop them from blending into the background."
-
-aliucord {
- changelog.set("""
- # Version 1.0.6
- * Remove debug logging i accidentally left in the code
- # Version 1.0.5
- * Updated for new discord/aliucord version
- # Version 1.0.4
- * Fixed errors while hooking
- # Version 1.0.3
- * Converted code to use kotlin
- # Version 1.0.2
- * Fixed crashing on outdated aliucord
- # Version 1.0.1
- * Fixed changing color when on amoled mode and user has no role color
- # Version 1.0.0
- * Initial release
- """.trimIndent())
-
- author(Authors.Ty)
-}
\ No newline at end of file
diff --git a/plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt b/plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt
deleted file mode 100644
index b8b7e1b..0000000
--- a/plugins/ShowNames/src/main/kotlin/tech/tyman/plugins/shownames/ShowNames.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package tech.tyman.plugins.shownames
-
-import android.content.Context
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.ColorUtils
-import com.aliucord.Logger
-import com.aliucord.annotations.AliucordPlugin
-import com.aliucord.entities.Plugin
-import com.aliucord.patcher.Hook
-import com.discord.models.member.GuildMember
-import com.discord.stores.StoreStream
-import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage
-import com.lytefast.flexinput.R
-import kotlin.math.abs
-
-@AliucordPlugin
-class ShowNames : Plugin() {
- override fun start(context: Context) {
- patcher.patch(WidgetChatListAdapterItemMessage::class.java.getDeclaredMethod("getAuthorTextColor", GuildMember::class.java), Hook {
- val member = it.args[0] as GuildMember? ?: return@Hook
- val color = member.color
- val theme = StoreStream.getUserSettingsSystem().theme
- if (color == -16777216) { // Default (no role) color
- return@Hook
- }
- val nameHsl = colorToHsl(color)
- val bg = when (theme) {
- "pureEvil" -> ContextCompat.getColor(context, R.c.black)
- "dark" -> ContextCompat.getColor(context, R.c.primary_600)
- "light" -> ContextCompat.getColor(context, R.c.white)
- else -> return@Hook
- }
- val bgHsl = colorToHsl(bg)
- if (abs(nameHsl[2] - bgHsl[2]) >= 0.01) return@Hook
- it.result = ColorUtils.HSLToColor(
- if (nameHsl[2] > 0.5) nameHsl.apply { this[2] =- 0.05f }
- else nameHsl.apply { this[2] =+ 0.05f }
- )
- })
- }
-
- override fun stop(context: Context) {
- patcher.unpatchAll()
- }
-
- private fun colorToHsl(color: Int): FloatArray = FloatArray(3).apply {
- ColorUtils.colorToHSL(color, this)
- }
-}
From ebe5a7624a6e7eaa82edbd84a85ca415ae1f1def Mon Sep 17 00:00:00 2001
From: RazerTexz <66409416+RazerTexz@users.noreply.github.com>
Date: Thu, 21 May 2026 08:20:02 +0700
Subject: [PATCH 8/8] feat: Ty
---
plugins/Template/build.gradle.kts | 22 +++++++++++++++++++
.../com/aliucord/plugins/template/Template.kt | 14 ++++++++++++
2 files changed, 36 insertions(+)
create mode 100644 plugins/Template/build.gradle.kts
create mode 100644 plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt
diff --git a/plugins/Template/build.gradle.kts b/plugins/Template/build.gradle.kts
new file mode 100644
index 0000000..90fcd54
--- /dev/null
+++ b/plugins/Template/build.gradle.kts
@@ -0,0 +1,22 @@
+import com.aliucord.gradle.repo.Authors
+import com.aliucord.gradle.repo.author
+
+plugins {
+ id("com.aliucord.plugins-repo")
+}
+
+version = "1.0.0"
+description = "Template plugin"
+
+aliucord {
+ author(Authors.Aliucord)
+
+ changelog.set(
+ """
+ # 1.0.0
+ * Initial plugin release!
+ """.trimIndent(),
+ )
+
+ deploy.set(false)
+}
diff --git a/plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt b/plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt
new file mode 100644
index 0000000..dad753f
--- /dev/null
+++ b/plugins/Template/src/main/kotlin/com/aliucord/plugins/template/Template.kt
@@ -0,0 +1,14 @@
+package com.aliucord.plugins.template
+
+import android.content.Context
+import com.aliucord.annotations.AliucordPlugin
+import com.aliucord.entities.Plugin
+
+@AliucordPlugin(requiresRestart = false)
+@Suppress("unused")
+class Template : Plugin() {
+ override fun start(context: Context) {}
+ override fun stop(context: Context) {
+ patcher.unpatchAll()
+ }
+}