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() + } +}