diff --git a/plugins/ChangeDownloadPath/Plugin.java b/plugins/ChangeDownloadPath/Plugin.java new file mode 100644 index 0000000..5cdbc51 --- /dev/null +++ b/plugins/ChangeDownloadPath/Plugin.java @@ -0,0 +1,52 @@ +package grzesiek11.aliucordplugins.changedownloadpath; + +import android.app.DownloadManager; +import android.content.Context; +import android.content.Context; +import android.net.Uri; +import com.aliucord.annotations.AliucordPlugin; +import com.aliucord.api.SettingsAPI; +import com.aliucord.Logger; +import com.aliucord.patcher.InsteadHook; +import com.discord.utilities.io.NetworkUtils; +import java.io.File; +import kotlin.jvm.functions.Function1; + +@AliucordPlugin(requiresRestart = false) +public class Plugin extends com.aliucord.entities.Plugin { + static final String PATH_SETTING_KEY = "path"; + static final String PATH_SETTING_DEFAULT = "/storage/emulated/0/Download"; + + @Override + public void start(Context context) throws Throwable { + var settings = new SettingsAPI("ChangeDownloadPath"); + this.settingsTab = new SettingsTab(Settings.class, SettingsTab.Type.PAGE).withArgs(settings); + + this.patcher.patch( + NetworkUtils.class.getDeclaredMethod("downloadFile", Context.class, Uri.class, String.class, String.class, Function1.class, Function1.class), + new InsteadHook(param -> { + var uri = (Uri) param.args[1]; + var filename = (String) param.args[2]; + var description = (String) param.args[3]; + var successCallback = param.args[4]; + var errorCallback = param.args[5]; + + var downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + var downloadPath = settings.getString(PATH_SETTING_KEY, PATH_SETTING_DEFAULT); + var downloadUri = Uri.fromFile(new File(downloadPath)); + var downloadRequest = new DownloadManager.Request(uri) + .setTitle(filename) + .setDescription(description) + .setDestinationUri(Uri.withAppendedPath(downloadUri, filename)); + downloadManager.enqueue(downloadRequest); + + return null; + }) + ); + } + + @Override + public void stop(Context context) { + this.patcher.unpatchAll(); + } +} diff --git a/plugins/ChangeDownloadPath/Settings.java b/plugins/ChangeDownloadPath/Settings.java new file mode 100644 index 0000000..2bb931b --- /dev/null +++ b/plugins/ChangeDownloadPath/Settings.java @@ -0,0 +1,43 @@ +package grzesiek11.aliucordplugins.changedownloadpath; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import com.aliucord.api.SettingsAPI; +import com.aliucord.fragments.SettingsPage; +import com.aliucord.views.TextInput; + +public class Settings extends SettingsPage { + private SettingsAPI settings; + + Settings(SettingsAPI settings) { + this.settings = settings; + } + + @Override + public void onViewCreated(View view, Bundle bundle) { + super.onViewCreated(view, bundle); + var context = this.requireContext(); + + this.setActionBarTitle("ChangeDownloadPath"); + + var pathSetting = new TextInput(context); + pathSetting.setHint("Path"); + var pathEditText = pathSetting.getEditText(); + pathEditText.setText(this.settings.getString(Plugin.PATH_SETTING_KEY, Plugin.PATH_SETTING_DEFAULT)); + pathEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence charSequence, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable editable) { + Settings.this.settings.setString(Plugin.PATH_SETTING_KEY, editable.toString()); + } + }); + this.addView(pathSetting); + } +} diff --git a/plugins/ForceNotifications/Plugin.java b/plugins/ForceNotifications/Plugin.java new file mode 100644 index 0000000..28e7f4b --- /dev/null +++ b/plugins/ForceNotifications/Plugin.java @@ -0,0 +1,29 @@ +package grzesiek11.aliucordplugins.forcenotifications; + +import android.content.Context; +import com.aliucord.annotations.AliucordPlugin; +import com.aliucord.patcher.Hook; +import com.discord.utilities.fcm.NotificationRenderer; +import com.discord.utilities.fcm.NotificationClient; +import com.discord.utilities.fcm.NotificationData; + +@AliucordPlugin(requiresRestart = false) +public class Plugin extends com.aliucord.entities.Plugin { + @Override + public void start(Context context) throws Throwable { + this.patcher.patch( + NotificationRenderer.class.getDeclaredMethod("displayInApp", Context.class, NotificationData.class), + new Hook(param -> { + var this_ = (NotificationRenderer) param.thisObject; + var notificationData = (NotificationData) param.args[1]; + + this_.display(context, notificationData, NotificationClient.INSTANCE.getSettings$app_productionGoogleRelease()); + }) + ); + } + + @Override + public void stop(Context context) { + patcher.unpatchAll(); + } +} diff --git a/plugins/TextEmoji/Plugin.java b/plugins/TextEmoji/Plugin.java new file mode 100644 index 0000000..75a43b1 --- /dev/null +++ b/plugins/TextEmoji/Plugin.java @@ -0,0 +1,58 @@ +package grzesiek11.aliucordplugins.textemoji; + +import android.content.Context; +import com.aliucord.annotations.AliucordPlugin; +import com.aliucord.patcher.Hook; +import com.aliucord.patcher.InsteadHook; +import com.discord.api.message.reaction.MessageReaction; +import com.discord.stores.StoreEmoji; +import com.discord.utilities.textprocessing.node.EmojiNode; +import com.discord.views.ReactionView; +import java.util.regex.Pattern; + +@AliucordPlugin(requiresRestart = false) +public class Plugin extends com.aliucord.entities.Plugin { + // This regex is not possible to match + private static final Pattern UNMATCHABLE_PATTERN = Pattern.compile("$a"); + + @Override + public void start(Context context) throws Throwable { + // Text emoji in message contents + // Could also be done by patching com.discord.utilities.textprocessing.Rules$PATTERN_UNICODE_EMOJI$2.invoke + // together with com.discord.utilities.textprocessing.Rules.replaceEmojiSurrogates, + // but I figured this is simpler. + patcher.patch( + StoreEmoji.class.getDeclaredMethod("getUnicodeEmojisPattern"), + InsteadHook.returnConstant(Plugin.UNMATCHABLE_PATTERN) + ); + + // Text emoji in reactions + patcher.patch( + EmojiNode.Companion.class.getDeclaredMethod("from", int.class, EmojiNode.EmojiIdAndType.class), + new Hook(param -> { + var emojiIdAndType = param.args[1]; + if (emojiIdAndType instanceof EmojiNode.EmojiIdAndType.Unicode) { + param.setResult(null); + } + }) + ); + + // Fix opacity in text emoji reactions + // This is a Discord bug that makes text emoji in reactions weirdly + // transparent, it's also present without the plugin, but it's only + // noticeable with emoji that don't have image replacements. + patcher.patch( + ReactionView.class.getDeclaredMethod("a", MessageReaction.class, long.class, boolean.class), + new Hook(param -> { + var this_ = (ReactionView) param.thisObject; + var reactionEmojiText = this_.o.e; + reactionEmojiText.setTextColor(0xffffffff); + }) + ); + } + + @Override + public void stop(Context context) { + patcher.unpatchAll(); + } +} diff --git a/plugins/sed/Command.java b/plugins/sed/Command.java new file mode 100644 index 0000000..7ed7dc7 --- /dev/null +++ b/plugins/sed/Command.java @@ -0,0 +1,149 @@ +package grzesiek11.aliucordplugins.sed; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class Command { + private String replace; + private Pattern findRegex; + private boolean gFlag; + + private Command(String replace, Pattern findRegex, boolean gFlag) { + this.replace = replace; + this.findRegex = findRegex; + this.gFlag = gFlag; + } + + public static Optional parse(String commandString, boolean advanced) { + var findStringBuilder = new StringBuilder(); + var replaceStringBuilder = new StringBuilder(); + boolean gFlag = false; + var state = ParserState.COMMAND; + boolean escape = false; + int charIndex = 0; + loop: while (true) { + Optional ch; + if (charIndex < commandString.length()) { + ch = Optional.of(commandString.charAt(charIndex)); + } else { + ch = Optional.empty(); + } + + switch (state) { + case COMMAND: { + if (ch.isPresent() && ch.get() == 's') { + state = ParserState.FIND_SLASH; + ++charIndex; + break; + } else { + return Optional.empty(); + } + } + + case FIND_SLASH: { + if (ch.isPresent() && ch.get() == '/') { + state = ParserState.FIND; + ++charIndex; + break; + } else { + return Optional.empty(); + } + } + + case FIND: { + if (!ch.isPresent()) { + return Optional.empty(); + } + if (!escape) { + if (ch.get() == '\\') { + escape = true; + if (!advanced) { + findStringBuilder.append(ch.get()); + ++charIndex; + break; + } + } else if (ch.get() == '/') { + state = ParserState.REPLACE; + ++charIndex; + break; + } + } else { + escape = false; + } + findStringBuilder.append(ch.get()); + ++charIndex; + break; + } + + case REPLACE: { + if (!ch.isPresent()) { + state = ParserState.END; + break; + } + if (!escape) { + if (ch.get() == '\\' && advanced) { + escape = true; + } else if (ch.get() == '/' && advanced) { + state = ParserState.FLAGS; + ++charIndex; + break; + } + } else { + escape = false; + } + replaceStringBuilder.append(ch.get()); + ++charIndex; + break; + } + + case FLAGS: { + if (ch.isPresent()) { + if (ch.get() == 'g') { + gFlag = true; + state = ParserState.END; + ++charIndex; + break; + } else { + return Optional.empty(); + } + } else { + state = ParserState.END; + } + } + + case END: { + if (!ch.isPresent()) { + break loop; + } else { + return Optional.empty(); + } + } + } + } + + var findString = findStringBuilder.toString(); + var replaceString = replaceStringBuilder.toString(); + + String findRegexString; + String replace; + if (advanced) { + findRegexString = findString; + replace = replaceString; + } else { + findRegexString = Pattern.quote(findString); + replace = Matcher.quoteReplacement(replaceString); + } + var findRegex = Pattern.compile(findRegexString); + return Optional.of(new Command(replace, findRegex, gFlag)); + } + + public String replace(String text) { + var findMatcher = this.findRegex.matcher(text); + if (this.gFlag) { + return findMatcher.replaceAll(this.replace); + } else { + return findMatcher.replaceFirst(this.replace); + } + } +} diff --git a/plugins/sed/ParserState.java b/plugins/sed/ParserState.java new file mode 100644 index 0000000..6f3e57e --- /dev/null +++ b/plugins/sed/ParserState.java @@ -0,0 +1,10 @@ +package grzesiek11.aliucordplugins.sed; + +enum ParserState { + COMMAND, + FIND_SLASH, + FIND, + REPLACE, + FLAGS, + END; +} diff --git a/plugins/sed/Plugin.java b/plugins/sed/Plugin.java new file mode 100644 index 0000000..d34f55a --- /dev/null +++ b/plugins/sed/Plugin.java @@ -0,0 +1,74 @@ +package grzesiek11.aliucordplugins.sed; + +import android.content.Context; +import com.aliucord.annotations.AliucordPlugin; +import com.aliucord.api.SettingsAPI; +import com.aliucord.patcher.PreHook; +import com.aliucord.utils.ReflectUtils; +import com.discord.stores.StoreMessagesHolder; +import com.discord.stores.StoreStream; +import com.discord.widgets.chat.input.models.ApplicationCommandData; +import com.discord.widgets.chat.input.WidgetChatInput$configureSendListeners$2; +import java.util.List; +import kotlin.jvm.functions.Function1; + +@AliucordPlugin(requiresRestart = false) +public class Plugin extends com.aliucord.entities.Plugin { + static final String ADVANCED_MODE_SETTING_KEY = "advanced_mode"; + static final boolean ADVANCED_MODE_SETTING_DEFAULT = false; + + @Override + public void start(Context context) throws Throwable { + var settings = new SettingsAPI("Sed"); + this.settingsTab = new SettingsTab(Settings.class, SettingsTab.Type.PAGE).withArgs(settings); + + var storeMessages = StoreStream.getMessages(); + var storeMessagesHolder = (StoreMessagesHolder) ReflectUtils.getField(storeMessages, "holder"); + var storeChannelsSelected = StoreStream.getChannelsSelected(); + var storeUsers = StoreStream.getUsers(); + + patcher.patch( + WidgetChatInput$configureSendListeners$2.class.getDeclaredMethod("invoke", List.class, ApplicationCommandData.class, Function1.class), + new PreHook(param -> { + var this_ = (WidgetChatInput$configureSendListeners$2) param.thisObject; + var input = this_.$chatInput.getText(); + var command = Command.parse(input, settings.getBool(Plugin.ADVANCED_MODE_SETTING_KEY, Plugin.ADVANCED_MODE_SETTING_DEFAULT)); + if (!command.isPresent()) { + return; + } + var validationResultCallback = (Function1) param.args[2]; + validationResultCallback.invoke(Boolean.TRUE); + + var selectedChannelId = storeChannelsSelected.getSelectedChannel().k(); + var currentUserId = storeUsers.getMe().getId(); + var optionalMessage = storeMessagesHolder + .getMessagesForChannel(selectedChannelId) + .descendingMap() + .values() + .stream() + .filter(m -> m.getAuthor().getId() == currentUserId) + .findFirst(); + if (!optionalMessage.isPresent()) { + param.setResult(null); + return; + } + + var message = optionalMessage.get(); + var content = message.getContent(); + var newContent = command.get().replace(content); + if (!newContent.isEmpty()) { + storeMessages.editMessage(message.getId(), selectedChannelId, newContent, message.getAllowedMentions()); + } else { + storeMessages.deleteMessage(storeMessages.getMessage(selectedChannelId, message.getId())); + } + + param.setResult(null); + }) + ); + } + + @Override + public void stop(Context context) { + patcher.unpatchAll(); + } +} diff --git a/plugins/sed/Settings.java b/plugins/sed/Settings.java new file mode 100644 index 0000000..0b2bfff --- /dev/null +++ b/plugins/sed/Settings.java @@ -0,0 +1,31 @@ +package grzesiek11.aliucordplugins.sed; + +import android.os.Bundle; +import android.view.View; +import com.aliucord.api.SettingsAPI; +import com.aliucord.fragments.SettingsPage; +import com.aliucord.Utils; +import com.discord.views.CheckedSetting; + +public class Settings extends SettingsPage { + private SettingsAPI settings; + + Settings(SettingsAPI settings) { + this.settings = settings; + } + + @Override + public void onViewCreated(View view, Bundle bundle) { + super.onViewCreated(view, bundle); + var context = this.requireContext(); + + this.setActionBarTitle("Sed"); + + var advancedModeSetting = Utils.createCheckedSetting(context, CheckedSetting.ViewType.SWITCH, "Advanced mode", "Enables advanced features like regular expressions"); + advancedModeSetting.setChecked(this.settings.getBool(Plugin.ADVANCED_MODE_SETTING_KEY, Plugin.ADVANCED_MODE_SETTING_DEFAULT)); + advancedModeSetting.setOnCheckedListener(checked -> { + this.settings.setBool(Plugin.ADVANCED_MODE_SETTING_KEY, checked); + }); + this.addView(advancedModeSetting); + } +}