From 4119027db4b70f88dc6bd464ae707adb94a29779 Mon Sep 17 00:00:00 2001 From: RazerTexz <66409416+RazerTexz@users.noreply.github.com> Date: Thu, 21 May 2026 08:28:37 +0700 Subject: [PATCH] feat: Cloudburst --- .../com/aliucord/gradle/repo/Authors.kt | 2 + plugins/DeleteEmbeds/build.gradle.kts | 13 + .../plugins/deleteembeds/DeleteEmbeds.kt | 110 ++++++ plugins/EditWebhooks/build.gradle.kts | 13 + .../plugins/editwebhooks/EditWebhooks.kt | 88 +++++ .../plugins/editwebhooks/ui/WebhookList.kt | 100 +++++ .../plugins/editwebhooks/ui/WebhookMenu.kt | 242 ++++++++++++ .../editwebhooks/ui/WebhookRecyclerAdapter.kt | 45 +++ .../editwebhooks/ui/WebhookViewHolder.kt | 30 ++ .../plugins/editwebhooks/utils/Webhook.kt | 37 ++ .../editwebhooks/utils/WebhookRequest.kt | 3 + plugins/HighlightOwnMessages/build.gradle.kts | 13 + .../HighlightOwnMessages.kt | 78 ++++ .../plugins/highlightownmessages/Settings.kt | 113 ++++++ plugins/MoreTags/build.gradle.kts | 13 + .../cloudburst/plugins/moretags/MoreTags.kt | 346 ++++++++++++++++++ .../plugins/moretags/ui/MoreTagsSettings.kt | 178 +++++++++ plugins/Notifier/build.gradle.kts | 13 + .../cloudburst/plugins/notifier/Notifier.kt | 151 ++++++++ .../plugins/notifier/ui/GuildSheet.kt | 81 ++++ .../plugins/notifier/ui/NotifierSettings.kt | 44 +++ plugins/ReactionEmojiName/build.gradle.kts | 14 + .../reactionemojiname/ReactionEmojiName.kt | 35 ++ plugins/SendEmbeds/build.gradle.kts | 13 + .../plugins/sendembeds/SendEmbeds.kt | 100 +++++ .../plugins/sendembeds/ui/EmbedModal.kt | 324 ++++++++++++++++ .../plugins/sendembeds/ui/ModeSelector.kt | 54 +++ .../sendembeds/ui/SendEmbedsSettings.kt | 44 +++ .../plugins/sendembeds/utils/Author.kt | 3 + .../plugins/sendembeds/utils/Embed.kt | 3 + .../plugins/sendembeds/utils/EmbedImage.kt | 3 + .../plugins/sendembeds/utils/Webhook.kt | 12 + .../sendembeds/utils/WebhookMessage.kt | 3 + plugins/TextReplace/build.gradle.kts | 13 + .../plugins/textreplace/TextReplace.kt | 123 +++++++ .../plugins/textreplace/ui/ReplacementCard.kt | 123 +++++++ .../plugins/textreplace/ui/ReplacerLayout.kt | 26 ++ .../textreplace/ui/ReplacerSettings.kt | 122 ++++++ .../textreplace/utils/TextReplacement.kt | 36 ++ 39 files changed, 2764 insertions(+) create mode 100644 plugins/DeleteEmbeds/build.gradle.kts create mode 100644 plugins/DeleteEmbeds/src/main/kotlin/cloudburst/plugins/deleteembeds/DeleteEmbeds.kt create mode 100644 plugins/EditWebhooks/build.gradle.kts create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/EditWebhooks.kt create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookList.kt create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookMenu.kt create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookRecyclerAdapter.kt create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookViewHolder.kt create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/Webhook.kt create mode 100644 plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/WebhookRequest.kt create mode 100644 plugins/HighlightOwnMessages/build.gradle.kts create mode 100644 plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/HighlightOwnMessages.kt create mode 100644 plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/Settings.kt create mode 100644 plugins/MoreTags/build.gradle.kts create mode 100644 plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/MoreTags.kt create mode 100644 plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/ui/MoreTagsSettings.kt create mode 100644 plugins/Notifier/build.gradle.kts create mode 100644 plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/Notifier.kt create mode 100644 plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/GuildSheet.kt create mode 100644 plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/NotifierSettings.kt create mode 100644 plugins/ReactionEmojiName/build.gradle.kts create mode 100644 plugins/ReactionEmojiName/src/main/kotlin/cloudburst/plugins/reactionemojiname/ReactionEmojiName.kt create mode 100644 plugins/SendEmbeds/build.gradle.kts create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/SendEmbeds.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/EmbedModal.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/ModeSelector.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/SendEmbedsSettings.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Author.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Embed.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/EmbedImage.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Webhook.kt create mode 100644 plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/WebhookMessage.kt create mode 100644 plugins/TextReplace/build.gradle.kts create mode 100644 plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/TextReplace.kt create mode 100644 plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacementCard.kt create mode 100644 plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerLayout.kt create mode 100644 plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerSettings.kt create mode 100644 plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/utils/TextReplacement.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 cc656bc..4a54c7e 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 @@ -46,6 +46,8 @@ object Authors { val LampDelivery = Author("LampDelivery", 650805815623680030L) val Ty = Author("Ty", 487443883127472129L) val miaaaa0a = Author("miaaaa0a", 435750383491481602L) + val Cloudburst = Author("Cloudburst", 295186738085756929L) + val HalalKing = Author("HalalKing", 261634919980204033L) } /** diff --git a/plugins/DeleteEmbeds/build.gradle.kts b/plugins/DeleteEmbeds/build.gradle.kts new file mode 100644 index 0000000..4155092 --- /dev/null +++ b/plugins/DeleteEmbeds/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.1.5" +description = "Delete embeds." + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/DeleteEmbeds/src/main/kotlin/cloudburst/plugins/deleteembeds/DeleteEmbeds.kt b/plugins/DeleteEmbeds/src/main/kotlin/cloudburst/plugins/deleteembeds/DeleteEmbeds.kt new file mode 100644 index 0000000..7f72144 --- /dev/null +++ b/plugins/DeleteEmbeds/src/main/kotlin/cloudburst/plugins/deleteembeds/DeleteEmbeds.kt @@ -0,0 +1,110 @@ +package cloudburst.plugins.deleteembeds + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import com.aliucord.Constants +import androidx.core.content.res.ResourcesCompat +import androidx.core.widget.NestedScrollView +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.fragments.InputDialog +import com.aliucord.patcher.Hook +import com.discord.databinding.WidgetChatListActionsBinding +import com.discord.utilities.color.ColorCompat +import com.discord.stores.StoreStream +import com.discord.widgets.chat.list.actions.WidgetChatListActions +import com.lytefast.flexinput.R +import java.lang.reflect.InvocationTargetException +import com.aliucord.Utils +import com.aliucord.utils.GsonUtils +import com.aliucord.Http +import com.google.gson.JsonObject + +@AliucordPlugin +class DeleteEmbeds : Plugin() { + @SuppressLint("SetTextI18n") + override fun start(context: Context) { + val icon = ContextCompat.getDrawable(context, R.e.ic_clear_24dp) + val deleteEmbedId = View.generateViewId() + + with(WidgetChatListActions::class.java, { + val getBinding = getDeclaredMethod("getBinding").apply { isAccessible = true } + + patcher.patch(getDeclaredMethod("configureUI", WidgetChatListActions.Model::class.java), Hook { callFrame -> + try { + val message = (callFrame.args[0] as WidgetChatListActions.Model).message + + val binding = getBinding.invoke(callFrame.thisObject) as WidgetChatListActionsBinding + val deleteEmbed = binding.a.findViewById(deleteEmbedId).apply { + if (message.hasEmbeds()) { + visibility = if ((callFrame.args[0] as WidgetChatListActions.Model).manageMessageContext.canDelete) + View.VISIBLE else View.GONE + } else if (message.hasAttachments() && !message.content.isEmpty()) { + visibility = if (message.author.id == StoreStream.getUsers().me.id) + View.VISIBLE else View.GONE + } else { + visibility = View.GONE + } + } + + if (!deleteEmbed.hasOnClickListeners()) deleteEmbed.setOnClickListener { + try { + Utils.threadPool.execute { + if (message.hasEmbeds()) { + deleteEmbed(message.channelId, message.id) + } + if (message.hasAttachments() && message.author.id == StoreStream.getUsers().me.id) { + deleteAttachements(message.channelId, message.id) + } + } + (callFrame.thisObject as WidgetChatListActions).dismiss() + } catch (e: IllegalAccessException) { + Utils.showToast("Internal error occured.") + e.printStackTrace() + } catch (e: InvocationTargetException) { + Utils.showToast("Internal error occured.") + e.printStackTrace() + } + } + } catch (ignored: Throwable) { + } + }) + + patcher.patch(getDeclaredMethod("onViewCreated", View::class.java, Bundle::class.java), Hook { callFrame -> + val linearLayout = (callFrame.args[0] as NestedScrollView).getChildAt(0) as LinearLayout + val ctx = linearLayout.context + val idx = linearLayout.indexOfChild(linearLayout.findViewById(Utils.getResId("dialog_chat_actions_delete", "id"))) + 1 + + icon?.setTint(ColorCompat.getThemedColor(ctx, R.b.colorInteractiveNormal)) + + val deleteEmbed = TextView(ctx, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = ctx.getString(R.h.delete) + " embed" + id = deleteEmbedId + setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) + typeface = ResourcesCompat.getFont(ctx, Constants.Fonts.whitney_medium) + } + + linearLayout.addView(deleteEmbed, idx) + }) + }) + } + + override fun stop(context: Context) = patcher.unpatchAll() + + fun deleteEmbed(channelId: Long, msgId: Long) { + Http.Request.newDiscordRequest("/channels/%d/messages/%d".format(channelId, msgId), "PATCH") + .setHeader("Referer", "https://discord.com/channels/%d/%d".format(channelId, msgId)) + .executeWithJson(GsonUtils.fromJson("{\"flags\":4}", JsonObject::class.java)) + } + + fun deleteAttachements(channelId: Long, msgId: Long) { + Http.Request.newDiscordRequest("/channels/%d/messages/%d".format(channelId, msgId), "PATCH") + .setHeader("Referer", "https://discord.com/channels/%d/%d".format(channelId, msgId)) + .executeWithJson(GsonUtils.fromJson("{\"attachments\":[]}", JsonObject::class.java)) + } +} diff --git a/plugins/EditWebhooks/build.gradle.kts b/plugins/EditWebhooks/build.gradle.kts new file mode 100644 index 0000000..31c8596 --- /dev/null +++ b/plugins/EditWebhooks/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.1.13" +description = "Edit Webhooks." + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/EditWebhooks.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/EditWebhooks.kt new file mode 100644 index 0000000..5288689 --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/EditWebhooks.kt @@ -0,0 +1,88 @@ +package cloudburst.plugins.editwebhooks + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.widget.NestedScrollView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.fragments.InputDialog +import com.aliucord.patcher.Hook +import androidx.core.content.res.ResourcesCompat +import com.discord.utilities.color.ColorCompat +import com.discord.widgets.channels.settings.WidgetTextChannelSettings +import com.discord.databinding.WidgetTextChannelSettingsBinding +import com.lytefast.flexinput.R +import java.lang.reflect.InvocationTargetException +import com.aliucord.utils.ReflectUtils +import com.aliucord.Constants +import com.discord.stores.StoreStream +import com.aliucord.fragments.ConfirmDialog +import com.aliucord.Utils +import com.aliucord.utils.GsonUtils +import com.aliucord.Http +import com.google.gson.JsonObject +import com.discord.utilities.rest.RestAPI +import com.discord.utilities.analytics.AnalyticSuperProperties +import com.aliucord.patcher.Patcher +import cloudburst.plugins.editwebhooks.ui.WebhookList + +@AliucordPlugin +class EditWebhooks : Plugin() { + @SuppressLint("SetTextI18n") + override fun start(context: Context) { + val iconLeft = ContextCompat.getDrawable(context, R.e.ic_publish_24dp) + val iconRight = ContextCompat.getDrawable(context, R.e.icon_carrot) + val editWebooksId = View.generateViewId() + + with(WidgetTextChannelSettings::class.java, { + val getBinding = getDeclaredMethod("getBinding").apply { isAccessible = true } + + patcher.patch(getDeclaredMethod("configureUI", WidgetTextChannelSettings.Model::class.java), Hook { callFrame -> + try { + val channel = (callFrame.args[0] as WidgetTextChannelSettings.Model).channel + + + val binding = getBinding.invoke(callFrame.thisObject) as WidgetTextChannelSettingsBinding + + binding.a.findViewById(editWebooksId).apply { + visibility = View.VISIBLE + setOnClickListener { + val list = WebhookList(channel) + Utils.openPageWithProxy(Utils.appActivity, list) + } + isClickable = true + } + } catch (ignored: Throwable) { + logger.error(ignored) + } + }) + + patcher.patch(getDeclaredMethod("onViewBound", View::class.java), Hook { callFrame -> + val coordinatorLayout = (callFrame.args[0] as CoordinatorLayout) + val nestedScrollView = coordinatorLayout.getChildAt(1) as NestedScrollView + val linearLayout = nestedScrollView.findViewById(Utils.getResId("channel_settings_section_privacy_safety", "id")) as LinearLayout + val ctx = linearLayout.context + + iconLeft?.setTint(ColorCompat.getThemedColor(ctx, R.b.colorInteractiveNormal)) + iconRight?.setTint(ColorCompat.getThemedColor(ctx, R.b.colorInteractiveNormal)) + + val editWebhooks = TextView(ctx, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = ctx.getString(R.h.edit) + " webhooks" + id = editWebooksId + setCompoundDrawablesRelativeWithIntrinsicBounds(iconLeft, null, iconRight, null) + typeface = ResourcesCompat.getFont(ctx, Constants.Fonts.whitney_medium) + } + + linearLayout.addView(editWebhooks) + }) + }) + } + + override fun stop(context: Context) = patcher.unpatchAll() +} diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookList.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookList.kt new file mode 100644 index 0000000..100fe39 --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookList.kt @@ -0,0 +1,100 @@ +package cloudburst.plugins.editwebhooks.ui + +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RectShape +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.aliucord.views.Divider +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.fragments.SettingsPage +import com.aliucord.utils.DimenUtils +import com.aliucord.fragments.InputDialog +import com.aliucord.wrappers.ChannelWrapper +import cloudburst.plugins.editwebhooks.ui.WebhookRecyclerAdapter +import com.discord.api.channel.Channel +import com.aliucord.utils.GsonUtils +import cloudburst.plugins.editwebhooks.utils.Webhook +import cloudburst.plugins.editwebhooks.utils.WebhookRequest +import com.aliucord.Http +import com.aliucord.views.Button +import com.lytefast.flexinput.R + +class WebhookList(val channelRaw: Channel) : SettingsPage() { + + private val channel = ChannelWrapper(channelRaw) + + override fun onViewBound(view: View) { + super.onViewBound(view) + setActionBarTitle(channel.name) + + fetchList() + } + + public fun fetchList() { + val context = requireContext() + val createWebhookI18n = context.getString(R.h.create) + " webhook" + Utils.threadPool.execute { + val list = Http.Request.newDiscordRequest("/channels/%d/webhooks".format(channel.id)) + .setHeader("Referer", "https://discord.com/channels/%d/%d".format(channel.guildId, channel.id)) + .execute() + .json(Array::class.java) + + + Utils.mainThread.post { + clear() + val shape = ShapeDrawable(RectShape()) + .apply { + setTint(Color.TRANSPARENT) + intrinsicHeight = DimenUtils.defaultPadding + } + + val decoration = DividerItemDecoration( + context, + DividerItemDecoration.VERTICAL + ).apply { + setDrawable(shape) + } + + val recyclerView = RecyclerView(context) + .apply { + adapter = WebhookRecyclerAdapter(list.toList(), this@WebhookList) + layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + addItemDecoration(decoration) + } + + addView(recyclerView) + + val addButton = Button(context).apply { + text = createWebhookI18n + setOnClickListener { + val inDialog = InputDialog() + .setTitle(createWebhookI18n) + .setDescription("Enter name of the webhook") + .setPlaceholderText("Webhook Name") + inDialog.setOnOkListener { + Utils.threadPool.execute { + createWebhook(inDialog.input.toString().trim()) + } + inDialog.dismiss() + } + inDialog.show(parentFragmentManager, "CreateWebhook") + } + } + + addView(Divider(context)) + addView(addButton) + } + } + } + + private fun createWebhook(name: String) { + Http.Request.newDiscordRequest("/channels/%s/webhooks".format(channel.id), "POST") + .executeWithJson(WebhookRequest(name, null)) + Utils.showToast("Webhook created") + fetchList() + } +} \ No newline at end of file diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookMenu.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookMenu.kt new file mode 100644 index 0000000..9bd312e --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookMenu.kt @@ -0,0 +1,242 @@ +package cloudburst.plugins.editwebhooks.ui + +import android.content.Context +import android.os.Bundle +import android.view.* +import androidx.core.content.ContextCompat +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.widgets.LinearLayout +import cloudburst.plugins.editwebhooks.utils.Webhook +import com.discord.app.AppBottomSheet +import com.discord.utilities.color.ColorCompat +import com.discord.views.CheckedSetting +import com.lytefast.flexinput.R +import android.widget.TextView +import com.aliucord.fragments.ConfirmDialog +import com.aliucord.fragments.InputDialog +import com.aliucord.Constants +import androidx.core.content.res.ResourcesCompat +import cloudburst.plugins.editwebhooks.utils.WebhookRequest +import java.util.Base64 + +import com.aliucord.Http +import com.google.gson.JsonObject +import com.aliucord.utils.ReflectUtils +import com.aliucord.PluginManager +import androidx.fragment.app.FragmentManager + +import com.discord.api.message.attachment.MessageAttachment +import com.discord.utilities.SnowflakeUtils +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemAttachment +import com.discord.widgets.guilds.profile.WidgetGuildProfileSheet + +import cloudburst.plugins.editwebhooks.ui.WebhookList + +class WebhookMenu( + private val webhook: Webhook, + private val parent: WebhookList +) : AppBottomSheet() { + + override fun getContentViewResId() = 0 + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View { + val context = inflater.context + val layout = LinearLayout(context) + layout.setBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary)) + + val sendEmbeds = (if (PluginManager.isPluginEnabled("SendEmbeds")) PluginManager.plugins.get("SendEmbeds") else null) + + val font = ResourcesCompat.getFont(context, Constants.Fonts.whitney_medium) + val deleteIcon = ContextCompat.getDrawable(context, R.e.ic_delete_24dp) + val copyIcon = ContextCompat.getDrawable(context, R.e.ic_copy_24dp) + val sendEmbedIcon = ContextCompat.getDrawable(context, R.e.ic_embed_white_24dp) + val avatarIcon = ContextCompat.getDrawable(context, R.e.ic_profile_24dp) + val renameIcon = ContextCompat.getDrawable(context, R.e.ic_edit_24dp) + val changeAvatarIcon = ContextCompat.getDrawable(context, R.e.ic_camera_24dp) + val guildIcon = ContextCompat.getDrawable(context, R.e.ic_info_24dp) + + val title = TextView(context, null, 0, R.i.UiKit_Settings_Item_Header).apply { + text = webhook.name + } + + val deleteWebhook = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.delete) + setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null) + setOnClickListener { + val coDialog = ConfirmDialog() + .setTitle("Delete webhook") + .setIsDangerous(true) + .setDescription("Do you want to delete \"${webhook.name}\" webhook?") + coDialog.setOnOkListener { + coDialog.dismiss() + Utils.threadPool.execute { deleteWebhook() } + + } + coDialog.show(parentFragmentManager, "DeleteWebhook") + } + setClickable(true) + typeface = font + } + + val copyWebhook = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.copy_link) + setCompoundDrawablesRelativeWithIntrinsicBounds(copyIcon, null, null, null) + setOnClickListener { + Utils.setClipboard("Webhook Url", webhook.url) + Utils.showToast("Webhook url copied to clipboard") + dismiss() + } + setClickable(true) + typeface = font + } + + val sendEmbed = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.send_message) + setCompoundDrawablesRelativeWithIntrinsicBounds(sendEmbedIcon, null, null, null) + setOnClickListener { + if (sendEmbeds != null) { + dismiss() + val makeModal = ReflectUtils.getField(sendEmbeds, "makeModal") as ((Long, String?) -> AppBottomSheet) + makeModal.invoke(0L, "webhooks/%s/%s".format(webhook.id, webhook.token)) + .show(parentFragmentManager, "SendEmbeds") + } + } + setClickable(true) + typeface = font + } + + val viewAvatar = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.user_settings_avatar) + setCompoundDrawablesRelativeWithIntrinsicBounds(avatarIcon, null, null, null) + setOnClickListener { + viewAvatar() + } + setClickable(true) + typeface = font + } + + val renameWebhook = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.change_nickname) + setCompoundDrawablesRelativeWithIntrinsicBounds(renameIcon, null, null, null) + setOnClickListener { + val inDialog = InputDialog() + .setTitle("Rename webhook") + .setDescription("Enter name of the name for ${webhook.name}") + .setPlaceholderText("Webhook Name") + inDialog.setOnOkListener { + Utils.threadPool.execute { + rename(inDialog.input.toString().trim()) + } + inDialog.dismiss() + } + inDialog.show(parentFragmentManager, "RenameWebhook") + } + setClickable(true) + typeface = font + } + + val setAvatar = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.change_avatar) + setCompoundDrawablesRelativeWithIntrinsicBounds(changeAvatarIcon, null, null, null) + setOnClickListener { + val inDialog = InputDialog() + .setTitle("Set Avatar") + .setDescription("Enter image url") + .setPlaceholderText("image url") + inDialog.setOnOkListener { + Utils.threadPool.execute { + setAvatar(inDialog.input.toString().trim()) + } + inDialog.dismiss() + } + inDialog.show(parentFragmentManager, "SetAvatar") + } + setClickable(true) + typeface = font + } + + val showGuild = TextView(context, null, 0, R.i.UiKit_Settings_Item_Icon).apply { + text = context.getString(R.h.this_server) + setCompoundDrawablesRelativeWithIntrinsicBounds(guildIcon, null, null, null) + setOnClickListener { + if (webhook.source_guild != null && webhook.source_channel != null) { + WidgetGuildProfileSheet.Companion(null).show(parentFragmentManager, true, webhook.source_guild.id, webhook.source_channel.id, false) + } + } + setClickable(true) + typeface = font + } + + layout.addView(title) + if (webhook.token != null) + layout.addView(copyWebhook) + if (webhook.token != null && sendEmbeds != null) + layout.addView(sendEmbed) + if (webhook.avatar != null) + layout.addView(viewAvatar) + if (webhook.source_guild != null && webhook.source_channel != null) + layout.addView(showGuild) + layout.addView(renameWebhook) + layout.addView(setAvatar) + layout.addView(deleteWebhook) + + return layout + } + + private fun deleteWebhook() { + Http.Request.newDiscordRequest("/webhooks/%s".format(webhook.id), "DELETE") + .execute() + Utils.showToast("Webhook deleted") + parent.fetchList() + dismiss() + } + + private fun viewAvatar() { + val msgClass = MessageAttachment::class.java + val filenameField = msgClass.getDeclaredField("filename").apply { setAccessible(true) } + val idField = msgClass.getDeclaredField("id").apply { setAccessible(true) } + val urlField = msgClass.getDeclaredField("url").apply { setAccessible(true) } + val proxyUrlField = msgClass.getDeclaredField("proxyUrl").apply { setAccessible(true) } + + val attachment = ReflectUtils.allocateInstance(MessageAttachment::class.java) + try { + filenameField.set(attachment, "%s.webp".format(webhook.name)) + idField.set(attachment, SnowflakeUtils.fromTimestamp(System.currentTimeMillis())) + urlField.set(attachment, webhook.avatarUrl+"?size=2048") + proxyUrlField.set(attachment, webhook.avatarUrl+"?size=2048") + } catch (err: Throwable) { + return + } + + WidgetChatListAdapterItemAttachment.Companion.`access$navigateToAttachment`(WidgetChatListAdapterItemAttachment.Companion, view?.context, attachment) + } + + private fun rename(newName: String) { + Http.Request.newDiscordRequest("/webhooks/%s".format(webhook.id), "PATCH") + .executeWithJson(WebhookRequest(newName, null)) + Utils.showToast("Webhook renamed") + parent.fetchList() + dismiss() + } + + private fun setAvatar(url: String) { + val type = url.substring(url.lastIndexOf('.') + 1) + setAvatar(Http.Request(url) + .setHeader("Accept", "*/*") + .execute() + .bytes, + type) + } + + private fun setAvatar(bytes: ByteArray, type: String) { + var dataType = type.lowercase() + if (dataType == "jpg") dataType = "jpeg" + val datauri = "data:image/%s;base64,%s".format(dataType, Base64.getEncoder().encodeToString(bytes)) + Http.Request.newDiscordRequest("/webhooks/%s".format(webhook.id), "PATCH") + .executeWithJson(WebhookRequest(null, datauri)) + Utils.showToast("Avatar changed") + parent.fetchList() + dismiss() + } +} \ No newline at end of file diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookRecyclerAdapter.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookRecyclerAdapter.kt new file mode 100644 index 0000000..80763ee --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookRecyclerAdapter.kt @@ -0,0 +1,45 @@ +package cloudburst.plugins.editwebhooks.ui + +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import android.widget.TextView +import com.lytefast.flexinput.R +import cloudburst.plugins.editwebhooks.utils.Webhook +import com.aliucord.Utils +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import com.discord.utilities.icon.IconUtils +import com.discord.utilities.color.ColorCompat +import cloudburst.plugins.editwebhooks.ui.WebhookList + +class WebhookRecyclerAdapter( + private val webhooks: List, + private val parent: WebhookList +) : RecyclerView.Adapter() { + + private val layoutId = Utils.getResId("widget_user_profile_adapter_item_server", "layout") + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val layout = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) + layout.setBackgroundColor(ColorCompat.getThemedColor(parent.context, R.b.colorBackgroundSecondary)) + layout.setClickable(true) + return ViewHolder(this, layout as ConstraintLayout) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) = webhooks.elementAt(position).let { + holder.name.text = it.name + holder.subTitle.text = it.typeReadable + IconUtils.setIcon(holder.icon, it.avatarUrl+"?size=128") + } + + override fun getItemCount(): Int { + return webhooks.size + } + + fun onClick(position: Int) = webhooks.elementAt(position).let { + WebhookMenu(it, parent).show(parent.parentFragmentManager, it.name) + } +} \ No newline at end of file diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookViewHolder.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookViewHolder.kt new file mode 100644 index 0000000..3312ba1 --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/ui/WebhookViewHolder.kt @@ -0,0 +1,30 @@ +package cloudburst.plugins.editwebhooks.ui + +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.aliucord.Utils +import com.facebook.drawee.view.SimpleDraweeView + +class ViewHolder(private val adapter: WebhookRecyclerAdapter, layout: ConstraintLayout) : RecyclerView.ViewHolder(layout), View.OnClickListener { + private val iconId = Utils.getResId("user_profile_adapter_item_server_image", "id") + private val iconTextId = Utils.getResId("user_profile_adapter_item_server_text", "id") + private val serverNameId = Utils.getResId("user_profile_adapter_item_server_name", "id") + private val displayNameId = Utils.getResId("user_profile_adapter_item_user_display_name", "id") + private val guildAvatarId = Utils.getResId("guild_member_avatar", "id") + + val icon = layout.findViewById(iconId) as SimpleDraweeView + val name = layout.findViewById(serverNameId) as TextView + val subTitle = layout.findViewById(displayNameId) as TextView + + init { + layout.findViewById(iconTextId).visibility = View.GONE + layout.findViewById(guildAvatarId).visibility = View.GONE + layout.setOnClickListener(this) + } + + override fun onClick(view: View) { + adapter.onClick(adapterPosition) + } +} \ No newline at end of file diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/Webhook.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/Webhook.kt new file mode 100644 index 0000000..fbf580f --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/Webhook.kt @@ -0,0 +1,37 @@ +package cloudburst.plugins.editwebhooks.utils + +data class Webhook( + val type: Int?, + val avatar: String?, + val token: String?, + val name: String?, + val id: String?, + val source_guild: Source?, + val source_channel: Source? +) { + val url: String + get() { + return "https://discord.com/api/webhooks/%s/%s".format(id, token) + } + val avatarUrl: String + get() { + return "https://cdn.discordapp.com/avatars/%s/%s.webp".format(id, avatar) + } + val typeReadable: String + get() { + // https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types + if (type == 1) + return "Incoming" + else if (type == 2) + return "Channel Follower" + else if (type == 3) + return "Application" + else + return "Unknown" + } +} + +data class Source( + val id: Long, + val name: String +) \ No newline at end of file diff --git a/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/WebhookRequest.kt b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/WebhookRequest.kt new file mode 100644 index 0000000..db777fa --- /dev/null +++ b/plugins/EditWebhooks/src/main/kotlin/cloudburst/plugins/editwebhooks/utils/WebhookRequest.kt @@ -0,0 +1,3 @@ +package cloudburst.plugins.editwebhooks.utils + +data class WebhookRequest(val name: String?, val avatar: String?) \ No newline at end of file diff --git a/plugins/HighlightOwnMessages/build.gradle.kts b/plugins/HighlightOwnMessages/build.gradle.kts new file mode 100644 index 0000000..3d68743 --- /dev/null +++ b/plugins/HighlightOwnMessages/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.1.2" +description = "Customize how messages from yourself look." + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/HighlightOwnMessages.kt b/plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/HighlightOwnMessages.kt new file mode 100644 index 0000000..f1a6696 --- /dev/null +++ b/plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/HighlightOwnMessages.kt @@ -0,0 +1,78 @@ +package cloudburst.plugins.highlightownmessages + +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.Hook +import com.aliucord.patcher.Patcher +import com.aliucord.Utils +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage +import com.discord.widgets.chat.list.entries.ChatListEntry +import com.discord.widgets.chat.list.entries.MessageEntry +import com.discord.stores.StoreUser +import com.discord.stores.StoreStream +import android.content.Context +import android.view.View +import android.view.Gravity +import android.widget.TextView +import android.widget.FrameLayout +import com.lytefast.flexinput.R +import com.discord.utilities.color.ColorCompat + +@AliucordPlugin +class HighlightOwnMessages : Plugin() { + init { + settingsTab = SettingsTab(Settings::class.java, SettingsTab.Type.PAGE).withArgs(settings) + } + + override fun start(context: Context) { + val userId = StoreStream.getUsers().me.id + + val textViewId = Utils.getResId("chat_list_adapter_item_text", "id") + + with(WidgetChatListAdapterItemMessage::class.java) { + patcher.patch(getDeclaredMethod("onConfigure", Int::class.java, ChatListEntry::class.java), Hook { callFrame -> try { + val message = callFrame.args[1] as MessageEntry + + val view = callFrame.thisObject as WidgetChatListAdapterItemMessage + val textView = (view.itemView.findViewById(textViewId) as TextView) + + val padding = settings.getInt("Padding", 256) + + if (settings.getBool("RightLeft", true)) { + if (message.author?.userId == userId) { + view.itemView.layoutDirection = View.LAYOUT_DIRECTION_RTL + if (textView.lineCount > 1 && settings.getBool("Multiline", false)) { + textView.setPadding(padding, textView.paddingTop, 0, textView.paddingBottom) + textView.gravity = Gravity.START + } else { + textView.setPadding(padding, textView.paddingTop, 0, textView.paddingBottom) + textView.gravity = Gravity.END + } + } else { + view.itemView.layoutDirection = View.LAYOUT_DIRECTION_LTR + textView.setPadding(0, textView.paddingTop, 0, textView.paddingBottom) + textView.gravity = Gravity.START + } + } else { + if (message.author?.userId == userId) { textView.setPadding(padding, textView.paddingTop, 0, textView.paddingBottom) } + else { textView.setPadding(0, textView.paddingTop, 0, textView.paddingBottom) } + } + + val selfFg = settings.getInt("SelfFg", 0) + if (selfFg != 0) { + textView.setTextColor(if (message.author?.userId == userId) selfFg else ColorCompat.getColor(context, R.c.primary_dark_200)) + } + + val selfBg = settings.getInt("SelfBg", 0) + if (selfBg != 0) { + textView.setBackgroundColor(if (message.author?.userId == userId) selfBg else 0) + } + + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + } + + override fun stop(context: Context) = patcher.unpatchAll() +} \ No newline at end of file diff --git a/plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/Settings.kt b/plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/Settings.kt new file mode 100644 index 0000000..2518162 --- /dev/null +++ b/plugins/HighlightOwnMessages/src/main/kotlin/cloudburst/plugins/highlightownmessages/Settings.kt @@ -0,0 +1,113 @@ +package cloudburst.plugins.highlightownmessages + +import com.aliucord.fragments.SettingsPage +import android.view.View +import android.view.Gravity + +import com.aliucord.Utils +import com.aliucord.utils.DimenUtils +import com.discord.views.CheckedSetting +import com.aliucord.widgets.LinearLayout +import com.aliucord.api.SettingsAPI +import android.widget.TextView +import com.lytefast.flexinput.R +import com.aliucord.views.TextInput +import android.text.InputType +import com.discord.utilities.colors.ColorPickerUtils +import android.graphics.Color + +class Settings(private val settings: SettingsAPI) : SettingsPage() { + override fun onViewBound(view: View) { + super.onViewBound(view) + + setActionBarTitle("HighlightOwnMessages") + val p = DimenUtils.defaultPadding / 2 + + addView(Utils.createCheckedSetting(view.context, CheckedSetting.ViewType.SWITCH, "Right and Left", "Whether to make your messages show on the right.").apply { + val key = "RightLeft" + isChecked = settings.getBool(key, true) + setOnCheckedListener { + settings.setBool(key, it) + } + }) + + addView(Utils.createCheckedSetting(view.context, CheckedSetting.ViewType.SWITCH, "Left Align Multiline", "Whether to make multiline text left aligned, but add padding.").apply { + val key = "Multiline" + isChecked = settings.getBool(key, false) + setOnCheckedListener { + settings.setBool(key, it) + } + }) + + val padding = TextInput(view.context).apply { + setHint("Padding") + editText.setText(settings.getInt("Padding", 256).toString()) + editText.inputType = InputType.TYPE_CLASS_NUMBER + editText.maxLines = 1 + setPadding(p, p, p, p) + } + addView(padding) + + addView(TextView(view.context, null, 0, R.i.UiKit_TextView).apply { + text = "Set color to 0 if you wish to disable changing colors. Click the edit icon to open a color picker." + setPadding(p, p, p, p) + }) + + var selfFg: View? = null + selfFg = TextInput(view.context, "Self Foreground Color", settings.getInt("SelfFg", 0).toString(), View.OnClickListener { + if (selfFg != null) colorPicker(selfFg as TextInput) + }).apply { + editText.inputType = InputType.TYPE_CLASS_NUMBER + editText.maxLines = 1 + setThemedEndIcon(R.e.ic_theme_24dp) + setPadding(p, p, p, p) + } + addView(selfFg) + + var selfBg: View? = null + selfBg = TextInput(view.context, "Self Background Color", settings.getInt("SelfBg", 0).toString(), View.OnClickListener { + if (selfBg != null) colorPicker(selfBg as TextInput) + }).apply { + editText.inputType = InputType.TYPE_CLASS_NUMBER + editText.maxLines = 1 + setThemedEndIcon(R.e.ic_theme_24dp) + setPadding(p, p, p, p) + } + addView(selfBg) + + setOnBackPressed { + try { + settings.setInt("Padding", padding.editText.text.toString().toInt()) + settings.setInt("SelfFg", selfFg.editText.text.toString().toInt()) + settings.setInt("SelfBg", selfBg.editText.text.toString().toInt()) + } catch(e:Throwable) { + Utils.showToast(e.message.toString()) + } + return@setOnBackPressed false + } + } + + + private fun colorPicker(input: TextInput) { + val builder = ColorPickerUtils.INSTANCE.buildColorPickerDialog( + context, + Utils.getResId("color_picker_title", "string"), + Color.BLACK + ) + builder.arguments?.putBoolean("alpha", true) + builder.k = object: b.k.a.a.f { // color picker listener i guess + override fun onColorReset(i: Int) { } + + override fun onColorSelected(i: Int, i2: Int) { + try { + input.editText.setText(i2.toString()) + } catch(e:Throwable) { + Utils.showToast(e.message.toString()) + } + } + + override fun onDialogDismissed(i: Int) { } + } + builder.show(parentFragmentManager, "COLOR_PICKER") + } +} \ No newline at end of file diff --git a/plugins/MoreTags/build.gradle.kts b/plugins/MoreTags/build.gradle.kts new file mode 100644 index 0000000..b01978d --- /dev/null +++ b/plugins/MoreTags/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.2.4" +description = "Show \"WEBHOOK\" \"OWNER\" \"MOD\" and \"STAFF\" tags appropriately." + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/MoreTags.kt b/plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/MoreTags.kt new file mode 100644 index 0000000..4c64bba --- /dev/null +++ b/plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/MoreTags.kt @@ -0,0 +1,346 @@ +package cloudburst.plugins.moretags + +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.Hook +import com.aliucord.patcher.PreHook +import com.aliucord.patcher.Patcher +import com.aliucord.Utils +import android.content.Context +import androidx.core.content.ContextCompat +import android.view.View + +import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemMessage +import com.discord.models.message.Message +import com.discord.models.member.GuildMember +import com.aliucord.wrappers.ChannelWrapper +import com.aliucord.utils.ReflectUtils +import com.aliucord.wrappers.GuildMemberWrapper +import com.aliucord.wrappers.GuildRoleWrapper +import com.discord.api.user.User +import com.discord.stores.StoreStream +import com.discord.utilities.permissions.PermissionUtils +import android.widget.TextView +import android.graphics.Color +import com.discord.api.permission.* +import com.lytefast.flexinput.R +import com.discord.utilities.color.ColorCompat +import com.discord.widgets.channels.memberlist.adapter.* +import com.discord.databinding.WidgetChannelMembersListItemUserBinding +import com.discord.widgets.user.profile.UserProfileHeaderView +import com.discord.widgets.user.profile.UserProfileHeaderViewModel +import com.discord.databinding.UserProfileHeaderViewBinding +import com.discord.databinding.WidgetChannelsListItemVoiceUserBinding +import com.discord.models.user.CoreUser +import com.discord.widgets.channels.list.WidgetChannelsListAdapter +import com.discord.widgets.channels.list.items.ChannelListItem +import com.discord.widgets.channels.list.items.ChannelListItemVoiceUser + +import cloudburst.plugins.moretags.ui.MoreTagsSettings + +@AliucordPlugin +class MoreTags : Plugin() { + + val cache: HashMap, String?> = hashMapOf(); + init { + settingsTab = SettingsTab(MoreTagsSettings::class.java, SettingsTab.Type.PAGE).withArgs(this) + } + + override fun start(context: Context) { + with(WidgetChatListAdapterItemMessage::class.java) { // in chat + val tagField = getDeclaredField("itemTag").apply { isAccessible = true; } + patcher.patch(getDeclaredMethod("configureItemTag", Message::class.java, Boolean::class.javaPrimitiveType), Hook { callFrame -> try { + if (!settings.getBool("MoreTags_ShowChat", true)) return@Hook + + val tag = tagField.get(callFrame.thisObject) as TextView? + if (tag == null) return@Hook + + tag.visibility = View.GONE + + val msg = callFrame.args[0] as Message + val isOP = callFrame.args[1] as Boolean + val user = CoreUser(msg.author) + + if (user.discriminator == 0) { + setTag(context, tag, + if (settings.getBool("MoreTags_Webhook", true) && msg.webhookId != null) + (if (msg.isCrosspost()) "SERVER" else "WEBHOOK") + else if (settings.getBool("MoreTags_System", true) && (user.isSystemUser() || user.id == -1L)) + "SYSTEM" + else if (user.isBot()) + "BOT" + else if (isOP) + "OP" + else + "" + , null) + return@Hook + } + + val botText = if (settings.getBool("MoreTags_System", true) && user.isSystemUser()) + "SYSTEM" + else if (user.isBot()) + "BOT" + else if (isOP) + "OP" + else + "" + + val channel = ChannelWrapper(StoreStream.getChannels().getChannel(msg.channelId)) + if (!channel.isGuild() || channel.guildId == null) { + setTag(context, tag, botText, null) + } else { + val member = StoreStream.getGuilds().getMember(channel.guildId, user.id) + if (member == null) return@Hook; + + val tagStr = getTag(channel.guildId, member) + setTag(context, tag, + if ((settings.getBool("MoreTags_BotOnly", false) && tagStr != "") || tagStr == null) botText + else if (botText != "") "${botText} • ${tagStr}" + else tagStr ?: "", + member.color) + } + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + + with(ChannelMembersListViewHolderMember::class.java) { // in member list + val bindingField = getDeclaredField("binding").apply { isAccessible = true; } + patcher.patch(getDeclaredMethod("bind", ChannelMembersListAdapter.Item.Member::class.java, Function0::class.java), Hook { callFrame -> try { + if (!settings.getBool("MoreTags_ShowList", true)) return@Hook + + val layout = (bindingField.get(callFrame.thisObject) as WidgetChannelMembersListItemUserBinding).root + val user = callFrame.args[0] as ChannelMembersListAdapter.Item.Member + val tag = layout.findViewById(Utils.getResId("username_tag", "id")) as TextView + + tag.visibility = View.GONE + + val botText = if (user.isBot()) + "BOT" + else + "" + + val guildId = user.guildId + if (guildId == null) { + setTag(context, tag, botText, null) + } else { + val member = StoreStream.getGuilds().getMember(guildId, user.userId) + if (member == null) return@Hook; + + val tagStr = getTag(guildId, member) + setTag(context, tag, + if ((settings.getBool("MoreTags_BotOnly", false) && tagStr != "") || tagStr == null) botText + else if (botText != "") "${botText} • ${tagStr}" + else tagStr ?: "", + member.color) + } + + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + + with(UserProfileHeaderView::class.java) { + val bindingField = getDeclaredField("binding").apply { isAccessible = true; } + patcher.patch(getDeclaredMethod("updateViewState", UserProfileHeaderViewModel.ViewState.Loaded::class.java), Hook { callFrame -> try { + if (!settings.getBool("MoreTags_ShowUser", true)) return@Hook + + val layout = (bindingField.get(callFrame.thisObject) as UserProfileHeaderViewBinding).a + val state = callFrame.args[0] as UserProfileHeaderViewModel.ViewState.Loaded + val member = state.guildMember + val user = state.user + val tag = layout.findViewById(Utils.getResId("username_tag", "id")) as TextView + + tag.visibility = View.GONE + if (member == null) { + setTag(context, tag, + if (user.discriminator == 0 && settings.getBool("MoreTags_System", true) && (user.isSystemUser() || user.id == -1L)) + "SYSTEM" + else if (user.isBot()) + "BOT" + else + "" + , null) + return@Hook + } + + val botText = if (settings.getBool("MoreTags_System", true) && user.isSystemUser()) + "SYSTEM" + else if (user.isBot()) + "BOT" + else + "" + + val tagStr = getTag(member.guildId, member) + setTag(context, tag, + if ((settings.getBool("MoreTags_BotOnly", false) && tagStr != "") || tagStr == null) botText + else if (botText != "") "${botText} • ${tagStr}" + else tagStr ?: "", + member.color) + + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + + with(WidgetChannelsListAdapter.ItemVoiceUser::class.java) { // vc + val bindingField = getDeclaredField("binding").apply { isAccessible = true; } + patcher.patch(getDeclaredMethod("onConfigure", Int::class.java, ChannelListItem::class.java), Hook { callFrame -> try { + if (!settings.getBool("MoreTags_ShowVC", true)) return@Hook + + val tag = (bindingField.get(callFrame.thisObject) as WidgetChannelsListItemVoiceUserBinding).e + val channelListItemVoiceUser = callFrame.args[1] as ChannelListItemVoiceUser + val member = channelListItemVoiceUser.computed + val user = channelListItemVoiceUser.user + + if (member == null) { + setTag(context, tag, + if (user.discriminator == 0 && settings.getBool("MoreTags_System", true) && (user.isSystemUser() || user.id == -1L)) + "SYSTEM" + else if (user.isBot()) + "BOT" + else + "" + , null) + return@Hook + } + + val botText = if (settings.getBool("MoreTags_System", true) && user.isSystemUser()) + "SYSTEM" + else if (user.isBot()) + "BOT" + else + "" + + val tagStr = getTag(member.guildId, member) + setTag(context, tag, + if ((settings.getBool("MoreTags_BotOnly", false) && tagStr != "") || tagStr == null) botText + else if (botText != "") "${botText} • ${tagStr}" + else tagStr ?: "", + member.color) + + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + } + + override fun stop(context: Context) = patcher.unpatchAll() + + private fun setTag(context: Context, tag: TextView, newText: String, color: Int?) { + val bgColor = if (settings.getBool("MoreTags_Colorize", true) && color != null) + color + else + ColorCompat.getColor(context, R.c.brand_new_500) + val fgColor = contrastColor(bgColor) + tag.apply { + text = newText + visibility = if (newText == "") View.GONE else View.VISIBLE + background.setTint(bgColor) + setTextColor(fgColor) + getCompoundDrawables().let { + it[0]?.setVisible(false, false) + it[0]?.setTint(fgColor) + } + } + } + + private fun contrastColor(color: Int): Int { + val c = Color.valueOf(color) + if ((c.red()*54.213 + c.green()*182.376 + c.blue()*18.411) >= 140) return Color.BLACK.toInt() + else return Color.WHITE.toInt() + } + + private fun getTag(guildId: Long, member: GuildMember): String? { + val useCache = settings.getBool("MoreTags_Cache", false) + if (useCache && cache.containsKey(Pair(guildId, member.userId))) + return cache.get(Pair(guildId, member.userId)) + val checkOwner = settings.getBool("MoreTags_Owner", true) + val checkAdmin = settings.getBool("MoreTags_Admin", true) + val checkStaff = settings.getBool("MoreTags_Staff", true) + val checkMod = settings.getBool("MoreTags_Mod", true) + val checkVoiceMod = settings.getBool("MoreTags_VoiceMod", true) + val roleName = settings.getBool("MoreTags_RoleName", false) + val allRoles = settings.getBool("MoreTags_AllRoles", false) + if (!(checkOwner || checkAdmin || checkStaff || checkMod || checkVoiceMod || allRoles)) return null + + val guild = StoreStream.getGuilds().getGuild(guildId) + if (guild == null) { + if (useCache) cache[Pair(guildId, member.userId)] = null + return null + } + + if (checkOwner && guild.isOwner(member.userId)) { + if (useCache) cache[Pair(guildId, member.userId)] = "OWNER" + return "OWNER" + } + val roleList = StoreStream.getGuilds().roles.get(guildId) + if (roleList == null) { + if (useCache) cache[Pair(guildId, member.userId)] = null + return null + } + + var isAdmin: String = "" + var isMod: String = "" + var isVoiceMod: String = "" + var isStaff: String = "" + + for (roleId in member.roles) { + val rawRole = roleList.get(roleId) + if (rawRole == null) continue + val role = GuildRoleWrapper(rawRole) + val perms = role.permissions + val name = if (roleName) role.name.uppercase() else "" + if (checkAdmin && PermissionUtils.can(Permission.ADMINISTRATOR, perms)) { + isAdmin = if (roleName) name else "ADMIN" + break + } + if (PermissionUtils.can(Permission.MANAGEMENT_PERMISSIONS, perms) + || PermissionUtils.can(Permission.MANAGE_CHANNELS, perms) + || PermissionUtils.can(Permission.MANAGE_ROLES, perms)) { + isStaff = if (roleName) name else "STAFF" + } + if (PermissionUtils.can(Permission.KICK_MEMBERS, perms) + || PermissionUtils.can(Permission.BAN_MEMBERS, perms) + || PermissionUtils.can(Permission.MANAGE_MESSAGES, perms)) { + isMod = if (roleName) name else "MOD" + } + if (PermissionUtils.can(Permission.MOVE_MEMBERS, perms) || + PermissionUtils.can(Permission.MUTE_MEMBERS, perms) || + PermissionUtils.can(Permission.DEAFEN_MEMBERS, perms)) { + isVoiceMod = if (roleName) name else "VOICE MOD" + } + } + + if (checkAdmin && isAdmin != "") { + if (useCache) cache[Pair(guildId, member.userId)] = isAdmin + return isAdmin + } + else if (checkStaff && isStaff != "") { + if (useCache) cache[Pair(guildId, member.userId)] = isStaff + return isStaff + } + else if (checkMod && isMod != "") { + if (useCache) cache[Pair(guildId, member.userId)] = isMod + return isMod + } + else if (checkVoiceMod && isVoiceMod != "") { + if (useCache) cache[Pair(guildId, member.userId)] = isVoiceMod + return isVoiceMod + } + + if (allRoles) { + val role = roleList.get(member.hoistRoleId) ?: + if (member.roles.size > 0) roleList.get(member.roles.get(0)) else null + if (role != null) { + val name = GuildRoleWrapper(role).name.uppercase() + if (useCache) cache[Pair(guildId, member.userId)] = name + return name + } + } + + if (useCache) cache[Pair(guildId, member.userId)] = null + return null + } +} diff --git a/plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/ui/MoreTagsSettings.kt b/plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/ui/MoreTagsSettings.kt new file mode 100644 index 0000000..cf974f0 --- /dev/null +++ b/plugins/MoreTags/src/main/kotlin/cloudburst/plugins/moretags/ui/MoreTagsSettings.kt @@ -0,0 +1,178 @@ +package cloudburst.plugins.moretags.ui + +import com.aliucord.fragments.SettingsPage +import android.view.View +import android.view.Gravity + +import com.aliucord.Utils +import com.discord.views.CheckedSetting +import com.aliucord.views.Divider +import com.aliucord.widgets.LinearLayout +import com.aliucord.api.SettingsAPI +import com.aliucord.utils.MDUtils +import android.widget.TextView +import com.lytefast.flexinput.R +import cloudburst.plugins.moretags.MoreTags + +class MoreTagsSettings(private val plugin: MoreTags) : SettingsPage() { + override fun onViewBound(view: View) { + super.onViewBound(view) + plugin.cache.clear() + val ctx = view.context + + setActionBarTitle("MoreTags") + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Cache", "Use cache for displaying role tags, which resets on app restart.").apply { + val key = "MoreTags_Cache" + isChecked = plugin.settings.getBool(key, false) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Colorize", "Colorize tags acording to role colors.").apply { + val key = "MoreTags_Colorize" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Webhook", "Rename \"BOT\" to \"WEBHOOK\" or \"SERVER\" when appropriate.").apply { + val key = "MoreTags_Webhook" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "System", "Rename \"BOT\" to \"SYSTEM\" system messages (eg. Clyde) .").apply { + val key = "MoreTags_System" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Bot Only", "Wheter to display bots with admin permissions as \"BOT\" or \"BOT • ADMIN\".").apply { + val key = "MoreTags_BotOnly" + isChecked = plugin.settings.getBool(key, false) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + + addView(Divider(ctx)) + + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Role name", "Use role name instead of permission name").apply { + val key = "MoreTags_RoleName" + isChecked = plugin.settings.getBool(key, false) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "All roles", "Display tags for all users with a role, not just admin/mods").apply { + val key = "MoreTags_AllRoles" + isChecked = plugin.settings.getBool(key, false) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + + addView(Divider(ctx)) + + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Owner", "Show \"OWNER\" tags.").apply { + val key = "MoreTags_Owner" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Administrator", "Show \"ADMIN\" tags.").apply { + val key = "MoreTags_Admin" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Staff", "Show \"STAFF\" tags.").apply { + val key = "MoreTags_Staff" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Moderator", "Show \"MOD\" tags.").apply { + val key = "MoreTags_Mod" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Voice moderator", "Show \"VOICE MOD\" tags.").apply { + val key = "MoreTags_VoiceMod" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + + addView(Divider(ctx)) + + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Show in chat", "Show tags in chat.").apply { + val key = "MoreTags_ShowChat" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Show in member list", "Show tags in member list.").apply { + val key = "MoreTags_ShowList" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Show in user pane", "Show tags in user details pane.").apply { + val key = "MoreTags_ShowUser" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + addView(Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, "Show in VC", "Show tags in voice chat list.").apply { + val key = "MoreTags_ShowVC" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + + addView(Divider(ctx)) + + addView(TextView(ctx, null, 0, R.i.UiKit_TextView).apply { + text = MDUtils.render(""" + + ***OWNER***: Owner of the server _(duh)_ + + ***ADMIN***: Has the administrator permission + + ***STAFF***: Has at least one of the following permissions: + - Manage Server + - Manage Channels + - Manage Roles + + ***MOD***: Has at least one of the following permissions: + - Kick Members + - Ban Members + - Manage Messages + + ***VOICE MOD***: Has at least one of the following permissions: + - Mute Members + - Deafen Members + - Move Members + + ***SERVER***: Message that was crossposted from another server + + ***WEBHOOK***: Message that was sent by a webhook + + ***SYSTEM***: Sender is a system user (eg. Clyde) + """.trimIndent()) + }) + } +} diff --git a/plugins/Notifier/build.gradle.kts b/plugins/Notifier/build.gradle.kts new file mode 100644 index 0000000..53f9758 --- /dev/null +++ b/plugins/Notifier/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.0.1" +description = "Log guild leaves and unfriends" + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/Notifier.kt b/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/Notifier.kt new file mode 100644 index 0000000..699e3d0 --- /dev/null +++ b/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/Notifier.kt @@ -0,0 +1,151 @@ +package cloudburst.plugins.notifier + +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.PreHook +import com.aliucord.patcher.Hook +import com.aliucord.patcher.Patcher +import com.aliucord.entities.NotificationData +import com.aliucord.Utils +import com.aliucord.api.NotificationsAPI + +import android.content.Context +import android.view.View +import android.os.Bundle + +import com.discord.stores.StoreGuilds +import com.discord.stores.StoreUserRelationships +import com.discord.stores.StoreStream + +import com.discord.api.guild.Guild +import com.discord.models.domain.ModelUserRelationship +import com.discord.widgets.user.usersheet.WidgetUserSheet +import com.lytefast.flexinput.fragment.FlexInputFragment +import com.discord.widgets.guilds.profile.WidgetGuildProfileSheet + +import cloudburst.plugins.notifier.ui.* + +@AliucordPlugin +class Notifier : Plugin() { + + var fragmentManager = Utils.appActivity.supportFragmentManager + + init { + settingsTab = SettingsTab(NotifierSettings::class.java, SettingsTab.Type.BOTTOM_SHEET).withArgs(this) + } + + override fun start(context: Context) { + with(FlexInputFragment::class.java) { // this is only here because Utils.appActivity.supportFragmentManager is scuffed + patcher.patch(getDeclaredMethod("onViewCreated", View::class.java, Bundle::class.java), Hook { callFrame -> + try { + fragmentManager = (callFrame.thisObject as FlexInputFragment).parentFragmentManager + } catch (ignored: Exception) { + logger.error(ignored) + } + }) + } + + with(StoreGuilds::class.java) { + patcher.patch(getDeclaredMethod("handleGuildRemove", Guild::class.java), PreHook { callFrame -> try { + if (!settings.getBool("Notifier_GuildLeaves", true)) return@PreHook; + if (callFrame.args[0] is Guild) { + onGuildRemoval(callFrame.args[0] as Guild) + } else { + onGuildRemoval(null) + } + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + with(StoreUserRelationships::class.java) { + patcher.patch(getDeclaredMethod("handleRelationshipRemove", ModelUserRelationship::class.java), PreHook { callFrame -> try { + if (!settings.getBool("Notifier_FriendRemovals", true)) return@PreHook; + if (callFrame.args[0] is ModelUserRelationship) { + onUnfriend((callFrame.args[0] as ModelUserRelationship).id) + } else { + onUnfriend(null) + } + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + } + + override fun stop(context: Context) = patcher.unpatchAll() + + private fun onGuildRemoval(guild: Guild?) { + NotificationsAPI.display(if (guild == null || guild.r() == 0L || guild.r() == -1L) { + logger.info("Unknown guild removal") + NotificationData().apply { + title = "Unknown guild was removed" + body = "You have been kicked/banned from a guild or it was removed" + } + } else { + val id = guild.r() + val cachedGuild = StoreStream.getGuilds().getGuild(guild.r()) + if (cachedGuild == null) { + logger.info("Guild removal: ${id}") + NotificationData().apply { + title = "Guild $id was removed" + body = "You have been kicked/banned from a guild or it was removed.\nClick to copy id" + setOnClick { + Utils.setClipboard("id", id.toString()) + Utils.showToast("Copied to clipboard", false) + } + } + } else { + logger.info("Guild removal: ${cachedGuild.toString()}") + NotificationData().apply { + iconUrl = "https://cdn.discordapp.com/icons/$id/${cachedGuild.icon}.png" + title = "Guild \"${cachedGuild.name}\" was removed" + body = "You have been kicked/banned from a guild or it was removed.\nClick to see details" + setOnClick { + try { + GuildSheet(cachedGuild).show(fragmentManager, "GuildSheet") + } catch (ignored: Throwable) { + logger.error(ignored) + } + } + } + } + + }) + } + + private fun onUnfriend(id: Long?) { + NotificationsAPI.display(if (id == null) { + NotificationData().apply { + title = "Unknown user unfriended you" + body = "You have been unfriended" + } + } else { + val user = StoreStream.getUsers().getUsers(arrayListOf(id), true).values.firstOrNull() + if (user != null) { + NotificationData().apply { + iconUrl = "https://cdn.discordapp.com/avatars/${id}/${user.avatar}.png" + title = "User ${user.username} unfriended you" + body = "You have been unfriended\nClick to open user sheet" + setOnClick { + try { + WidgetUserSheet.Companion(null).show(id, fragmentManager) + } catch (ignored: Throwable) { + logger.error(ignored) + } + } + } + } else { + NotificationData().apply { + title = "User ${id} unfriended you" + body = "You have been unfriended\nClick to open user sheet" + setOnClick { + try { + WidgetUserSheet.Companion(null).show(id, fragmentManager) + } catch (ignored: Throwable) { + logger.error(ignored) + } + } + } + } + }) + } +} diff --git a/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/GuildSheet.kt b/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/GuildSheet.kt new file mode 100644 index 0000000..625852e --- /dev/null +++ b/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/GuildSheet.kt @@ -0,0 +1,81 @@ +package cloudburst.plugins.notifier.ui + +import com.discord.app.AppBottomSheet +import com.discord.models.guild.Guild +import com.discord.app.AppViewFlipper +import com.discord.utilities.color.ColorCompat +import com.facebook.drawee.view.SimpleDraweeView +import com.google.android.material.button.MaterialButton + +import com.aliucord.Utils +import com.aliucord.views.Button +import com.lytefast.flexinput.R + +import android.view.* +import android.widget.LinearLayout +import android.os.Bundle +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import com.discord.stores.StoreInviteSettings + +import com.discord.widgets.guilds.invite.WidgetGuildInvite; + +class GuildSheet(val guild : Guild) : AppBottomSheet() { + override fun getContentViewResId() = 0 + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View { + val view = inflater.inflate(Utils.getResId("widget_guild_profile_sheet", "layout"), container, false) + view.visibility = View.VISIBLE + val flipper = view.findViewById(Utils.getResId("guild_profile_sheet_flipper", "id")) as AppViewFlipper + flipper.displayedChild = 1 + + val banner = view.findViewById(Utils.getResId("guild_profile_sheet_banner", "id")) as SimpleDraweeView? + banner?.setImageURI("https://cdn.discordapp.com/banners/${guild.id}/${guild.banner}.jpg?size=512") + + val icon = view.findViewById(Utils.getResId("guild_profile_sheet_icon", "id")) as SimpleDraweeView? + icon?.setImageURI("https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png") + + val name = view.findViewById(Utils.getResId("guild_profile_sheet_name", "id")) as TextView? + name?.setText(guild.name) + + val description = view.findViewById(Utils.getResId("guild_profile_sheet_description", "id")) as TextView? + description?.setText(guild.description) + + val onlineCount = view.findViewById(Utils.getResId("guild_profile_sheet_online_count", "id")) as LinearLayout? + onlineCount?.visibility = View.GONE + + val memberCount = view.findViewById(Utils.getResId("guild_profile_sheet_member_count", "id")) as LinearLayout? + memberCount?.visibility = View.GONE + + val bottomSheet = view.findViewById(Utils.getResId("guild_profile_sheet_tab_items", "id")) as LinearLayout? + bottomSheet?.findViewById(Utils.getResId("guild_profile_sheet_boosts", "id"))?.apply { + visibility = View.GONE + } + bottomSheet?.findViewById(Utils.getResId("guild_profile_sheet_notifications", "id"))?.apply { + setCompoundDrawablesRelativeWithIntrinsicBounds(null, ContextCompat.getDrawable(context, R.e.ic_copy_24dp), null, null) + setText("Copy ID") + setOnClickListener { + Utils.setClipboard("id", guild.id.toString()) + Utils.showToast("Copied to clipboard", false) + } + + } + bottomSheet?.findViewById(Utils.getResId("guild_profile_sheet_settings", "id"))?.apply { + visibility = View.GONE + } + bottomSheet?.findViewById(Utils.getResId("guild_profile_sheet_invite", "id"))?.apply { + if (guild.vanityUrlCode != null) { + visibility = View.VISIBLE + setOnClickListener { + WidgetGuildInvite.Companion(null).launch(context, StoreInviteSettings.InviteCode(guild.vanityUrlCode, "", null)) + } + } else { + visibility = View.GONE + } + } + + + return view + } +} \ No newline at end of file diff --git a/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/NotifierSettings.kt b/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/NotifierSettings.kt new file mode 100644 index 0000000..94ee0d3 --- /dev/null +++ b/plugins/Notifier/src/main/kotlin/cloudburst/plugins/notifier/ui/NotifierSettings.kt @@ -0,0 +1,44 @@ +package cloudburst.plugins.notifier.ui + +import cloudburst.plugins.notifier.Notifier +import com.discord.app.AppBottomSheet +import com.aliucord.widgets.LinearLayout +import android.content.Context +import android.os.Bundle +import android.view.* +import com.discord.utilities.color.ColorCompat +import com.aliucord.Utils +import com.discord.views.CheckedSetting +import com.lytefast.flexinput.R +import android.widget.TextView + +class NotifierSettings(private val plugin: Notifier) : AppBottomSheet() { + override fun getContentViewResId() = 0 + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View { + val context = inflater.context + val layout = LinearLayout(context) + layout.setBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary)) + + val title = TextView(context, null, 0, R.i.UiKit_Settings_Item_Header).apply { + text = "Notifier settings" + } + layout.addView(title) + + layout.addView(Utils.createCheckedSetting(context, CheckedSetting.ViewType.SWITCH, "Guild leaves", null).apply { + val key = "Notifier_GuildLeaves" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + layout.addView(Utils.createCheckedSetting(context, CheckedSetting.ViewType.SWITCH, "Friend removals", null).apply { + val key = "Notifier_FriendRemovals" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + return layout + } +} diff --git a/plugins/ReactionEmojiName/build.gradle.kts b/plugins/ReactionEmojiName/build.gradle.kts new file mode 100644 index 0000000..166584c --- /dev/null +++ b/plugins/ReactionEmojiName/build.gradle.kts @@ -0,0 +1,14 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.0.0" +description = "Reaction Emoji Name" + +aliucord { + author(Authors.Cloudburst) + author(Authors.HalalKing) +} \ No newline at end of file diff --git a/plugins/ReactionEmojiName/src/main/kotlin/cloudburst/plugins/reactionemojiname/ReactionEmojiName.kt b/plugins/ReactionEmojiName/src/main/kotlin/cloudburst/plugins/reactionemojiname/ReactionEmojiName.kt new file mode 100644 index 0000000..cc8079e --- /dev/null +++ b/plugins/ReactionEmojiName/src/main/kotlin/cloudburst/plugins/reactionemojiname/ReactionEmojiName.kt @@ -0,0 +1,35 @@ +package cloudburst.plugins.reactionemojiname + +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.Hook +import com.aliucord.patcher.Patcher + +import android.content.Context +import android.os.Build + +import com.discord.widgets.chat.managereactions.ManageReactionsEmojisAdapter +import com.discord.databinding.WidgetManageReactionsEmojiBinding + +@AliucordPlugin +class ReactionEmojiName : Plugin() { + + override fun start(context: Context) { + with(ManageReactionsEmojisAdapter.ReactionEmojiViewHolder::class.java) { + val field = getDeclaredField("binding") + field.isAccessible = true + + patcher.patch(getDeclaredMethod("onConfigure", Int::class.java, ManageReactionsEmojisAdapter.ReactionEmojiItem::class.java), Hook { callFrame -> try { + val binding = field.get(callFrame.thisObject) as WidgetManageReactionsEmojiBinding + val emojiItem = callFrame.args[1] as ManageReactionsEmojisAdapter.ReactionEmojiItem + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + binding.a.setTooltipText(emojiItem.reaction.b().d()) + } + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + } + + override fun stop(context: Context) = patcher.unpatchAll() +} diff --git a/plugins/SendEmbeds/build.gradle.kts b/plugins/SendEmbeds/build.gradle.kts new file mode 100644 index 0000000..ff2c2ca --- /dev/null +++ b/plugins/SendEmbeds/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.2.9" +description = "Send embed messages with /embed or with a button." + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/SendEmbeds.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/SendEmbeds.kt new file mode 100644 index 0000000..59bedad --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/SendEmbeds.kt @@ -0,0 +1,100 @@ +package cloudburst.plugins.sendembeds + +import android.content.Context +import com.discord.api.commands.ApplicationCommandType +import com.discord.models.commands.ApplicationCommandOption +import com.discord.stores.StoreStream +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin + +import com.aliucord.Utils +import com.aliucord.utils.ReflectUtils +import cloudburst.plugins.sendembeds.ui.EmbedModal + +import androidx.core.content.ContextCompat +import com.lytefast.flexinput.R +import android.view.View +import com.aliucord.patcher.Patcher +import com.aliucord.patcher.Hook +import com.aliucord.api.SettingsAPI +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.lytefast.flexinput.fragment.FlexInputFragment +import android.os.Bundle +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.appcompat.widget.AppCompatImageButton +import android.view.ViewGroup.LayoutParams +import android.view.ContextThemeWrapper +import android.app.Activity +import com.discord.utilities.color.ColorCompat +import cloudburst.plugins.sendembeds.ui.SendEmbedsSettings +import com.aliucord.fragments.AppFragmentProxy +import com.aliucord.widgets.BottomSheet + +@AliucordPlugin +class SendEmbeds : Plugin() { + + // channel, author, title, content, url, imageUrl, color + public val extraFunctions = hashMapOf Unit>() + public val modes = mutableListOf( + "embed.rauf.workers.dev", + "embed.rauf.wtf", + "rauf.wtf/embed" + ) + public val makeModal = ::createModal // bruh momento + + init { + settingsTab = SettingsTab(SendEmbedsSettings::class.java, SettingsTab.Type.BOTTOM_SHEET).withArgs(this) + } + + override fun start(context: Context) { + val icon = ContextCompat.getDrawable(context, R.e.ic_embed_white_24dp) + var fragmentManager = Utils.appActivity.supportFragmentManager + Utils.tintToTheme(icon)?.setAlpha(0x99); + + commands.registerCommand( + "embed", + "Send Embeds", + emptyList() + ) { ctx -> + createModal(ctx.getChannelId(), null).show(fragmentManager, "SendEmbeds") + return@registerCommand null + } + + with(FlexInputFragment::class.java, { + patcher.patch(getDeclaredMethod("onViewCreated", View::class.java, Bundle::class.java), Hook { callFrame -> + try { + fragmentManager = (callFrame.thisObject as FlexInputFragment).parentFragmentManager + if (settings.getBool("SendEmbeds_ButtonVisible", false)) { + + val view = ((callFrame.args[0] as LinearLayout).getChildAt(1) as RelativeLayout).getChildAt(0) as LinearLayout + + val btn = AppCompatImageButton(ContextThemeWrapper(view.context, R.i.UiKit_ImageView_Clickable), null, 0).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + setImageDrawable(icon) + setBackgroundColor(0) + setOnClickListener { + val channelId = StoreStream.getChannelsSelected().id + createModal(channelId, null).show(fragmentManager, "SendEmbeds") + } + setPadding(0, 0, 8, 0) + setClickable(true) + } + view.addView(btn) + } + } catch (ignored: Throwable) { + logger.error(ignored) + } + }) + }) + } + + override fun stop(context: Context) { + commands.unregisterAll() + patcher.unpatchAll() + } + + public fun createModal(channelId: Long, modeOverride: String?) : BottomSheet { + return EmbedModal(channelId, this@SendEmbeds, modeOverride) + } +} diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/EmbedModal.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/EmbedModal.kt new file mode 100644 index 0000000..e139f2e --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/EmbedModal.kt @@ -0,0 +1,324 @@ +package cloudburst.plugins.sendembeds.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.* +import android.widget.EditText +import android.text.TextWatcher +import android.text.Editable +import com.aliucord.widgets.BottomSheet +import com.aliucord.widgets.LinearLayout +import com.aliucord.views.TextInput +import androidx.appcompat.widget.Toolbar +import androidx.core.graphics.ColorUtils +import android.view.inputmethod.EditorInfo +import com.aliucord.views.Button +import com.aliucord.utils.DimenUtils +import com.discord.utilities.color.ColorCompat +import com.lytefast.flexinput.R +import com.discord.utilities.colors.ColorPickerUtils +import cloudburst.plugins.sendembeds.ui.ModeSelector + +import com.aliucord.Utils +import com.aliucord.utils.GsonUtils + +import cloudburst.plugins.sendembeds.utils.* +import cloudburst.plugins.sendembeds.SendEmbeds +import com.aliucord.Http + +import com.discord.utilities.permissions.PermissionUtils +import com.discord.api.permission.Permission + +import com.aliucord.utils.ReflectUtils +import com.discord.stores.StoreStream +import com.discord.stores.StorePermissions +import com.discord.utilities.rest.RestAPI + +import com.discord.models.domain.NonceGenerator +import com.discord.utilities.time.ClockFactory + +import com.discord.restapi.RestAPIParams +import com.aliucord.utils.RxUtils.createActionSubscriber +import com.aliucord.utils.RxUtils.subscribe +import com.jaredrummler.android.colorpicker.ColorPickerDialog +import java.net.URLEncoder +import java.util.HashMap +import com.aliucord.Logger + + +fun View.setMarginEnd( + value: Int +) { + val params = Toolbar.LayoutParams(Toolbar.LayoutParams.MATCH_PARENT, Toolbar.LayoutParams.MATCH_PARENT) + params.gravity = Gravity.END + params.bottomMargin = value + this.layoutParams = params +} + +class EmbedModal(val channelId: Long, val plugin: SendEmbeds, private val modeOverride: String?) : BottomSheet() { + + class EmptyTextWatcher(): TextWatcher { + override public fun afterTextChanged(s: Editable) { } + override public fun beforeTextChanged(p0: CharSequence, p1: Int, p2: Int, p3: Int) { } + override public fun onTextChanged(p0: CharSequence, p1: Int, p2: Int, p3: Int) { } + } + + private val logger = Logger("SendEmbeds") + + @SuppressLint("SetTextI18n") + override fun onViewCreated(view: View, bundle: Bundle?) { + super.onViewCreated(view, bundle) + val context = view.context + val padding = DimenUtils.defaultPadding + val p = padding / 2; + this.setPadding(padding) + + val authorInput = TextInput(context, "Author").apply { + editText.apply { + setText(StoreStream.getUsers().me.username) + inputType = (EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE or EditorInfo.TYPE_CLASS_TEXT) + imeOptions = EditorInfo.IME_ACTION_NEXT + } + setMarginEnd(p) + } + + val titleInput = TextInput(context, "Title").apply { + editText.apply { + inputType = (EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE or EditorInfo.TYPE_CLASS_TEXT) + imeOptions = EditorInfo.IME_ACTION_NEXT + } + setMarginEnd(p) + } + + val contentInput = TextInput(context, "Content").apply { + editText.apply { + maxLines = Int.MAX_VALUE + inputType = (EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE or + EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or + EditorInfo.TYPE_CLASS_TEXT) + imeOptions = EditorInfo.IME_ACTION_DONE + setHorizontallyScrolling(false) + } + setMarginEnd(p) + } + + val urlInput = TextInput(context, "Url").apply { + editText.apply { + inputType = (EditorInfo.TYPE_TEXT_VARIATION_URI or EditorInfo.TYPE_CLASS_TEXT) + imeOptions = EditorInfo.IME_ACTION_NEXT + } + setMarginEnd(p) + } + + val colorInput = TextInput(context, "Color", "#%06X".format(ColorCompat.getThemedColor(context, R.b.colorAccent) and 0x00FFFFFF), EmptyTextWatcher()).apply { + editText.apply { + inputType = EditorInfo.TYPE_NULL + setOnClickListener { + val builder = ColorPickerUtils.INSTANCE.buildColorPickerDialog( + context, + Utils.getResId("color_picker_title", "string"), + ColorCompat.getThemedColor(context, R.b.colorAccent) + ) + builder.k = object: b.k.a.a.f { // color picker listener i guess + override fun onColorReset(i: Int) { } + + override fun onColorSelected(i: Int, i2: Int) { + editText.setText("#%06X".format(i2 and 0x00FFFFFF)) // remove alpha component + } + + override fun onDialogDismissed(i: Int) { } + } + builder.show(parentFragmentManager, "COLOR_PICKER") + } + setClickable(true) + } + setMarginEnd(p) + } + + val imageInput = TextInput(context, "Image Url").apply { + editText.apply { + inputType = (EditorInfo.TYPE_TEXT_VARIATION_URI or EditorInfo.TYPE_CLASS_TEXT) + imeOptions = EditorInfo.IME_ACTION_NEXT + } + setMarginEnd(p) + } + + val modeInput = Button(context).apply { + text = "embed.rauf.workers.dev" + setBackgroundColor(ColorCompat.getThemedColor(view.getContext(), R.b.colorBackgroundTertiary)) + setOnClickListener { + Utils.threadPool.execute({ + val webhooks = HashMap() + val perms = StoreStream.getPermissions() + if (PermissionUtils.can(Permission.MANAGE_WEBHOOKS, perms.permissionsByChannel.get(channelId))) { + for(hook in getWebhooks()) { + if (hook.token == null) continue + var name = hook.name + if (name == null) { + name = hook.token + } + while (webhooks.containsKey(name)) { + name += "." + } + webhooks.put(name, hook) + } + } + + val modes = plugin.modes.toMutableList() + + webhooks.keys.forEach { + modes.add("Webhook: %s".format(it)) + } + + val modeSelector = ModeSelector(modes, {mode -> + if (mode.startsWith("Webhook: ")) { + val hook = webhooks.get(mode.drop(9)) + this.setText("webhooks/%s/%s".format(hook?.id, hook?.token)) + } else { + this.setText(mode) + } + }) + modeSelector.show(parentFragmentManager, "Embed Mode") + + }) + + } + setMarginEnd(p) + } + + val sendBtn = Button(context).apply { + text = context.getString(R.h.send_message) + setOnClickListener { + try { + Utils.threadPool.execute(object : Runnable { + override fun run() { + onSend( + modeOverride ?: modeInput.text.toString(), + authorInput.editText.text.toString(), + titleInput.editText.text.toString(), + contentInput.editText.text.toString(), + urlInput.editText.text.toString(), + imageInput.editText.text.toString(), + colorInput.editText.text.toString() + ) + } + }) + } catch (e: Throwable) { + Utils.showToast("An error occured") + logger.error(e) + } + dismiss() + } + } + + addView(authorInput) + addView(titleInput) + addView(contentInput) + addView(urlInput) + addView(imageInput) + addView(colorInput) + if (modeOverride == null) + addView(modeInput) + addView(sendBtn) + } + + private fun getWebhooks(): Array { + try { + return Http.Request.newDiscordRequest("/channels/%d/webhooks".format(channelId)) + .execute() + .json(Array::class.java) + } catch (e: Throwable) { + logger.error(e) + } + return emptyArray() + } + + private fun sendWebhookEmbed(webhook: String, author: String, title: String, content: String, url: String, imageUrl: String, color: Int) { + + Http.Request("https://discord.com/api/%s".format(webhook), "POST") + .executeWithJson(WebhookMessage( + null, + listOf( + Embed( + Author(author), + title, + content, + url, + EmbedImage(imageUrl), + color + ) + ) + )) + } + + private fun sendNonBotEmbed(site: String, author: String, title: String, content: String, url: String, imageUrl: String, color: Int) { + val msg = if (plugin.settings.getBool("SendEmbeds_NQNCompatibility", true)) + "[](https://%s/?author=%s&title=%s&description=%s&color=%06x&image=%s&redirect=%s)".format(site, URLEncoder.encode(author, "utf-8"), URLEncoder.encode(title, "utf-8"), URLEncoder.encode(content, "utf-8"), color, URLEncoder.encode(imageUrl, "utf-8"), URLEncoder.encode(url, "utf-8")) + else + "https://%s/?author=%s&title=%s&description=%s&color=%06x&image=%s&redirect=%s".format(site, URLEncoder.encode(author, "utf-8"), URLEncoder.encode(title, "utf-8"), URLEncoder.encode(content, "utf-8"), color, URLEncoder.encode(imageUrl, "utf-8"), URLEncoder.encode(url, "utf-8")) + val message = RestAPIParams.Message( + msg, + NonceGenerator.computeNonce(ClockFactory.get()).toString(), + null, + null, + emptyList(), + null, + RestAPIParams.Message.AllowedMentions( + emptyList(), + emptyList(), + emptyList(), + false + ), + null, + null + ) + RestAPI.api.sendMessage(channelId, message).subscribe(createActionSubscriber({ })) + } + + private fun onSend(mode: String, author: String, title: String, content: String, url: String, imageUrl: String, color: String) { + if (plugin.extraFunctions.containsKey(mode)) { + plugin.extraFunctions.get(mode)?.invoke( + channelId, + author, + title, + content, + url, + imageUrl, + color + ) + } + else if (mode.startsWith("webhooks/")) { + sendWebhookEmbed( + mode, + author, + title, + content, + url, + imageUrl, + toColorInt(color) + ) + } else { + sendNonBotEmbed( + mode, + author, + title, + content, + url, + imageUrl, + toColorInt(color) + ) + } + } + + public fun toColorInt(a: String): Int { + try { + return a + .replace("#", "") + .toInt(16) + } catch(e:Throwable) { + Utils.showToast("Color parser error: %s".format(e.message)) + e.printStackTrace() + } + return 0 + } +} diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/ModeSelector.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/ModeSelector.kt new file mode 100644 index 0000000..51cdae3 --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/ModeSelector.kt @@ -0,0 +1,54 @@ +package cloudburst.plugins.sendembeds.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.aliucord.Utils +import com.aliucord.api.SettingsAPI +import com.aliucord.widgets.LinearLayout +import com.discord.app.AppBottomSheet +import com.discord.utilities.color.ColorCompat +import com.discord.views.CheckedSetting +import com.discord.views.RadioManager +import android.content.DialogInterface +import com.lytefast.flexinput.R + +class ModeSelector(val radioNames: List, val onClose: (String) -> Unit) : AppBottomSheet() { + var mode = "embed.rauf.workers.dev" + + override fun getContentViewResId() = 0 + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val context = inflater.context + + val layout = LinearLayout(context).apply { + setBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary)) + } + + val radios = radioNames.map { + Utils.createCheckedSetting(context, CheckedSetting.ViewType.RADIO, it, null) + } + + val radioManager = RadioManager(radios) + val radioSize = radios.size + for (i in 0 until radioSize) { + val radio = radios[i] + radio.e { + radioManager.a(radio) + mode = radioNames[i] + } + layout.addView(radio) + } + return layout + } + + override fun onDismiss(dialogInteface: DialogInterface) { + onClose.invoke(mode) + super.onDismiss(dialogInteface) + } +} \ No newline at end of file diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/SendEmbedsSettings.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/SendEmbedsSettings.kt new file mode 100644 index 0000000..5819f99 --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/ui/SendEmbedsSettings.kt @@ -0,0 +1,44 @@ +package cloudburst.plugins.sendembeds.ui + +import cloudburst.plugins.sendembeds.SendEmbeds +import com.discord.app.AppBottomSheet +import com.aliucord.widgets.LinearLayout +import android.content.Context +import android.os.Bundle +import android.view.* +import com.discord.utilities.color.ColorCompat +import com.aliucord.Utils +import com.discord.views.CheckedSetting +import com.lytefast.flexinput.R +import android.widget.TextView + +class SendEmbedsSettings(private val plugin: SendEmbeds) : AppBottomSheet() { + override fun getContentViewResId() = 0 + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View { + val context = inflater.context + val layout = LinearLayout(context) + layout.setBackgroundColor(ColorCompat.getThemedColor(context, R.b.colorBackgroundPrimary)) + + val title = TextView(context, null, 0, R.i.UiKit_Settings_Item_Header).apply { + text = "SendEmbeds settings" + } + layout.addView(title) + + layout.addView(Utils.createCheckedSetting(context, CheckedSetting.ViewType.SWITCH, "Show button", null).apply { + val key = "SendEmbeds_ButtonVisible" + isChecked = plugin.settings.getBool(key, false) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + layout.addView(Utils.createCheckedSetting(context, CheckedSetting.ViewType.SWITCH, "NQN compatibility", "Sends links with markdown syntax, so NQN will convert them to webhooks without links visible").apply { + val key = "SendEmbeds_NQNCompatibility" + isChecked = plugin.settings.getBool(key, true) + setOnCheckedListener { + plugin.settings.setBool(key, it) + } + }) + return layout + } +} diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Author.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Author.kt new file mode 100644 index 0000000..08d7133 --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Author.kt @@ -0,0 +1,3 @@ +package cloudburst.plugins.sendembeds.utils + +data class Author(val name: String) \ No newline at end of file diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Embed.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Embed.kt new file mode 100644 index 0000000..26040d3 --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Embed.kt @@ -0,0 +1,3 @@ +package cloudburst.plugins.sendembeds.utils + +data class Embed(val author: Author, val title: String, val description: String, val url: String, val image: EmbedImage?, val color: Int) \ No newline at end of file diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/EmbedImage.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/EmbedImage.kt new file mode 100644 index 0000000..33091c8 --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/EmbedImage.kt @@ -0,0 +1,3 @@ +package cloudburst.plugins.sendembeds.utils + +data class EmbedImage(val url: String?) \ No newline at end of file diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Webhook.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Webhook.kt new file mode 100644 index 0000000..3f0ec1b --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/Webhook.kt @@ -0,0 +1,12 @@ +package cloudburst.plugins.sendembeds.utils + +data class Webhook( + val token: String?, + val name: String?, + val id: String? +) { + val url: String + get() { + return "https://discord.com/api/webhooks/%s/%s".format(id, token) + } +} \ No newline at end of file diff --git a/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/WebhookMessage.kt b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/WebhookMessage.kt new file mode 100644 index 0000000..328d9a5 --- /dev/null +++ b/plugins/SendEmbeds/src/main/kotlin/cloudburst/plugins/sendembeds/utils/WebhookMessage.kt @@ -0,0 +1,3 @@ +package cloudburst.plugins.sendembeds.utils + +data class WebhookMessage(val content: String?, val embeds: List) \ No newline at end of file diff --git a/plugins/TextReplace/build.gradle.kts b/plugins/TextReplace/build.gradle.kts new file mode 100644 index 0000000..7ae6696 --- /dev/null +++ b/plugins/TextReplace/build.gradle.kts @@ -0,0 +1,13 @@ +import com.aliucord.gradle.repo.Authors +import com.aliucord.gradle.repo.author + +plugins { + id("com.aliucord.plugins-repo") +} + +version = "1.0.10" +description = "Customizable text replacer." + +aliucord { + author(Authors.Cloudburst) +} \ No newline at end of file diff --git a/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/TextReplace.kt b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/TextReplace.kt new file mode 100644 index 0000000..28ee476 --- /dev/null +++ b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/TextReplace.kt @@ -0,0 +1,123 @@ +package cloudburst.plugins.textreplace + +import android.graphics.drawable.Drawable +import androidx.core.content.ContextCompat +import com.aliucord.annotations.AliucordPlugin +import com.aliucord.entities.Plugin +import com.aliucord.patcher.Hook +import com.aliucord.patcher.PreHook +import com.aliucord.patcher.Patcher +import com.aliucord.Utils +import android.content.Context +import android.widget.FrameLayout +import com.aliucord.utils.ReflectUtils +import com.aliucord.api.SettingsAPI +import com.discord.widgets.chat.MessageContent +import com.discord.widgets.chat.MessageManager +import com.discord.stores.StoreStream +import com.discord.api.premium.PremiumTier + +import cloudburst.plugins.textreplace.utils.TextReplacement +import cloudburst.plugins.textreplace.ui.ReplacerSettings + +@AliucordPlugin +class TextReplace : Plugin() { + lateinit var pluginIcon: Drawable + private val textContentField = + MessageContent::class.java.getDeclaredField("textContent").apply { + isAccessible = true + } + + init { + settingsTab = SettingsTab(ReplacerSettings::class.java) + } + + override fun load(context: Context) { + pluginIcon = ContextCompat.getDrawable(context, Utils.getResId("drawable_icon_sync_integration", "drawable"))!! + } + + override fun start(context: Context) { + mSettings = settings + TextReplace.replacementRules = settings.getObject( + "TextReplace_Rules", + TextReplacement.DEFAULT_LIST, + Array::class.java + ) + + with(com.discord.api.message.Message::class.java) { + patcher.patch(getDeclaredMethod("i"), Hook { callFrame -> try { + if (callFrame.result == null) return@Hook + var content = callFrame.result as String + if (content == "") return@Hook + for (rule in TextReplace.replacementRules) { + if (!rule.matchSent) continue + if (rule.matches(content)) { + content = rule.replace(content) + } + } + callFrame.result = content + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + + with(com.discord.widgets.chat.input.ChatInputViewModel::class.java) { + patcher.patch(getDeclaredMethod("sendMessage", Context::class.java, MessageManager::class.java, MessageContent::class.java, List::class.java, Boolean::class.javaPrimitiveType, Function1::class.java), PreHook { callFrame -> try { + //val isNitro = StoreStream.getUsers().me.premiumTier == PremiumTier.TIER_2 + val messageContent = callFrame.args[2] as MessageContent? + if (messageContent == null) return@PreHook + var content = textContentField.get(messageContent) as String? ?:"" + if (content == "") return@PreHook + for (rule in TextReplace.replacementRules) { + if (!rule.matchUnsent) continue + if (rule.matches(content)) { + content = rule.replace(content) + } + } + textContentField.set(messageContent, content) //textContentField.set(messageContent, content.take(if (isNitro) 4000 else 2000)) + return@PreHook + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + + with(com.discord.api.message.embed.MessageEmbed::class.java) { + patcher.patch(getDeclaredMethod("j"), Hook { callFrame -> try { + if (callFrame.result == null) return@Hook + var content = callFrame.result as String + if (content == "") return@Hook + for (rule in TextReplace.replacementRules) { + if (!rule.matchEmbeds) continue + if (rule.matches(content)) { + content = rule.replace(content) + } + } + callFrame.result = content + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + + patcher.patch(getDeclaredMethod("c"), Hook { callFrame -> try { + if (callFrame.result == null) return@Hook + var content = callFrame.result as String + if (content == "") return@Hook + for (rule in TextReplace.replacementRules) { + if (!rule.matchEmbeds) continue + if (rule.matches(content)) { + content = rule.replace(content) + } + } + callFrame.result = content + } catch (ignored: Throwable) { + logger.error(ignored) + }}) + } + } + + override fun stop(context: Context) = patcher.unpatchAll() + + companion object { + lateinit var mSettings: SettingsAPI + var replacementRules = TextReplacement.DEFAULT_LIST + } +} diff --git a/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacementCard.kt b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacementCard.kt new file mode 100644 index 0000000..73895a2 --- /dev/null +++ b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacementCard.kt @@ -0,0 +1,123 @@ +package cloudburst.plugins.textreplace.ui + +import android.content.Context +import android.view.View +import androidx.cardview.widget.CardView +import com.google.android.material.card.MaterialCardView +import com.google.android.material.textfield.TextInputLayout +import com.aliucord.views.TextInput +import com.aliucord.utils.DimenUtils +import com.aliucord.utils.ReflectUtils +import com.discord.utilities.color.ColorCompat +import com.lytefast.flexinput.R +import cloudburst.plugins.textreplace.utils.TextReplacement +import com.discord.views.CheckedSetting +import com.aliucord.Utils + +class ReplacerCard(ctx: Context) : MaterialCardView(ctx) { + val fromInput: TextInput + val replacementInput: TextInput + val isRegex: CheckedSetting + val ignoreCase: CheckedSetting + val matchUnsent: CheckedSetting + val matchSent: CheckedSetting + val matchEmbeds: CheckedSetting + + init { + radius = DimenUtils.defaultCardRadius.toFloat() + setCardBackgroundColor(ColorCompat.getThemedColor(ctx, R.b.colorBackgroundTertiary)) + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + val p = DimenUtils.defaultPadding + val linearLayout = com.aliucord.widgets.LinearLayout(ctx).apply { + setPadding(p, p, p, p) + } + + fromInput = TextInput(ctx) + fromInput.setInputHint("From") + linearLayout.addView(fromInput) + + replacementInput = TextInput(ctx) + replacementInput.setInputHint("To") + linearLayout.addView(replacementInput) + + isRegex = Utils.createCheckedSetting( + ctx, + CheckedSetting.ViewType.SWITCH, + "Regex", + "Whether to interpret from as regex" + ) + linearLayout.addView(isRegex) + + ignoreCase = Utils.createCheckedSetting( + ctx, + CheckedSetting.ViewType.SWITCH, + "Case Insensitive", + "Whether to ignore if upper or lower case" + ) + linearLayout.addView(ignoreCase) + + matchUnsent = Utils.createCheckedSetting( + ctx, + CheckedSetting.ViewType.SWITCH, + "Match unsent", + "Whether to match unsent messages" + ) + linearLayout.addView(matchUnsent) + + matchSent = Utils.createCheckedSetting( + ctx, + CheckedSetting.ViewType.SWITCH, + "Match sent", + "Whether to match sent messages" + ) + linearLayout.addView(matchSent) + + matchEmbeds = Utils.createCheckedSetting( + ctx, + CheckedSetting.ViewType.SWITCH, + "Match embeds", + "Whether to match embeds" + ) + linearLayout.addView(matchEmbeds) + + addView(linearLayout) + } + + + fun TextInput.setInputHint(hint: CharSequence) { + if (this is CardView) { + val root = ReflectUtils.invokeMethod(this, "getRoot") as TextInputLayout + root.hint = hint + } else { + (this as TextInputLayout).hint = hint + } + } + + public fun apply(replacement: TextReplacement) { + fromInput.editText.setText(replacement.fromInput) + replacementInput.editText.setText(replacement.replacement) + isRegex.isChecked = replacement.isRegex + ignoreCase.isChecked = replacement.ignoreCase + matchUnsent.isChecked = replacement.matchUnsent + matchSent.isChecked = replacement.matchSent + matchEmbeds.isChecked = replacement.matchEmbeds + } + + public fun createReplacement(): TextReplacement? { + if ((fromInput.editText.text.toString() == "") or (replacementInput.editText.text.toString() == "")) return null + try { + return TextReplacement( + fromInput.editText.text.toString(), + replacementInput.editText.text.toString(), + isRegex.isChecked, + ignoreCase.isChecked, + matchUnsent.isChecked, + matchSent.isChecked, + matchEmbeds.isChecked, + ) + } catch (e: Throwable) { + + } + return null + } +} diff --git a/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerLayout.kt b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerLayout.kt new file mode 100644 index 0000000..086799d --- /dev/null +++ b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerLayout.kt @@ -0,0 +1,26 @@ +package cloudburst.plugins.textreplace.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.aliucord.Utils +import com.aliucord.fragments.ConfirmDialog +import com.aliucord.fragments.SettingsPage +import cloudburst.plugins.textreplace.utils.TextReplacement +import android.text.TextWatcher +import android.text.Editable + +class ReplacerAdapter(private val settings: SettingsPage, private val rules: List) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ReplacerHolder(this, ReplacerCard(parent.context)) + + override fun onBindViewHolder(holder: ReplacerHolder, position: Int) = rules.elementAt(position).let { + holder.card.apply(it) + } + + override fun getItemCount() = rules.size +} + +class ReplacerHolder(private val adapter: ReplacerAdapter, val card: ReplacerCard) : RecyclerView.ViewHolder(card) { + +} \ No newline at end of file diff --git a/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerSettings.kt b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerSettings.kt new file mode 100644 index 0000000..18f7973 --- /dev/null +++ b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/ui/ReplacerSettings.kt @@ -0,0 +1,122 @@ +package cloudburst.plugins.textreplace.ui + +import com.aliucord.fragments.SettingsPage +import android.view.View +import android.view.Gravity +import android.view.WindowManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import cloudburst.plugins.textreplace.TextReplace +import cloudburst.plugins.textreplace.utils.TextReplacement +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RectShape +import com.aliucord.utils.DimenUtils +import com.aliucord.utils.GsonUtils +import com.aliucord.Utils +import android.graphics.Color +import com.aliucord.views.Button +import com.aliucord.views.ToolbarButton +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import com.aliucord.fragments.InputDialog + +class ReplacerSettings : SettingsPage() { + val headerId = View.generateViewId() + var replacementRules = TextReplace.replacementRules.toMutableList() + + override fun onViewBound(view: View) { + super.onViewBound(view) + + activity!!.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) + + val ctx = view.context + setActionBarTitle("TextReplace") + + val recycler = RecyclerView(ctx).apply { + adapter = ReplacerAdapter(this@ReplacerSettings, replacementRules) + layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + + val decoration = DividerItemDecoration(ctx, DividerItemDecoration.VERTICAL) + ShapeDrawable(RectShape()).run { + setTint(Color.TRANSPARENT) + intrinsicHeight = DimenUtils.defaultPadding + decoration.setDrawable(this) + } + addItemDecoration(decoration) + } + addView(recycler) + + Button(ctx).run { + text = "New Rule" + DimenUtils.defaultPadding.let { + setPadding(it, it, it, it) + } + setOnClickListener { + replacementRules.add(TextReplacement.emptyRule()) + recycler.adapter!!.notifyItemInserted(replacementRules.size-1) + } + linearLayout.addView(this) + } + + setOnBackPressed { + try { + updateRules(recycler) + TextReplace.mSettings.setObject("TextReplace_Rules", replacementRules.toTypedArray()) + TextReplace.replacementRules = replacementRules.toTypedArray() + } catch (e: Throwable) { + Utils.showToast(e.toString()) + return@setOnBackPressed true + } + return@setOnBackPressed false + } + + if (headerBar.findViewById(headerId) == null) { + + addHeaderButton("Import", ContextCompat.getDrawable(ctx, Utils.getResId("ic_file_upload_24dp", "drawable"))) { + val inDialog = InputDialog() + .setTitle("Import rules") + .setDescription("Paste previously exported rules here") + .setPlaceholderText("Rules JSON") + inDialog.setOnOkListener { + try { + replacementRules = GsonUtils + .fromJson(inDialog.input + .toString() + .trim(), + Array::class.java) + .toMutableList() + + inDialog.dismiss() + close() + } catch (e: Throwable) { + Utils.showToast("Error: ${e.message}") + inDialog.dismiss() + } + } + inDialog.show(parentFragmentManager, "ImportRules") + true + } + addHeaderButton("Export", ContextCompat.getDrawable(ctx, Utils.getResId("ic_file_download_white_24dp", "drawable"))) { + updateRules(recycler) + Utils.setClipboard("TextReplaceRules", GsonUtils.toJson(replacementRules)) + Utils.showToast("Rules copied to clipboard") + true + } + + } + } + + private fun updateRules(recycler: RecyclerView) { + replacementRules.clear() + var i = 0; + while (true) { + val holder = recycler.findViewHolderForLayoutPosition(i) as ReplacerHolder? + if (holder == null) break + val replacement = holder.card.createReplacement() + if (replacement != null) replacementRules.add(replacement) + i++ + } + } + +} diff --git a/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/utils/TextReplacement.kt b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/utils/TextReplacement.kt new file mode 100644 index 0000000..c32b320 --- /dev/null +++ b/plugins/TextReplace/src/main/kotlin/cloudburst/plugins/textreplace/utils/TextReplacement.kt @@ -0,0 +1,36 @@ +package cloudburst.plugins.textreplace.utils + +data class TextReplacement( + val fromInput: String, + val replacement: String, + val isRegex: Boolean, + val ignoreCase: Boolean, + val matchUnsent: Boolean, + val matchSent: Boolean, + val matchEmbeds: Boolean) { + public fun matches(message: String): Boolean { + if (isRegex) { + return fromInput.toRegex().containsMatchIn(message) + } else { + return message.contains(fromInput, ignoreCase = ignoreCase) + } + } + + public fun replace(message: String): String { + if (isRegex) { + return fromInput.toRegex().replace(message, replacement) + } else { + return message.replace(fromInput, replacement, ignoreCase) + } + } + + companion object { + val DEFAULT_LIST = arrayOf( + TextReplacement("media.discordapp.net", "cdn.discordapp.com", false, true, true, false, false), + TextReplacement("bluesmods.com", "aliucord.tk", false, true, true, false, false), + ) + public fun emptyRule(): TextReplacement { + return TextReplacement("", "", false, true, true, true, true) + } + } +}