diff --git a/canary/SlashCommandsFix/build.gradle.kts b/canary/SlashCommandsFix/build.gradle.kts new file mode 100644 index 0000000..eff7cf2 --- /dev/null +++ b/canary/SlashCommandsFix/build.gradle.kts @@ -0,0 +1,42 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +version = "7.16.0" +description = "Beta backport of SlashCommandsFix" + +aliucord { + changelog.set(""" + # 7.16.0 + * Initial port >w< thanks @jedenastka + """.trimIndent()) + + excludeFromUpdaterJson.set(false) +} + +//apply(plugin = "com.gradleup.shadow") +apply(plugin = "com.github.johnrengelman.shadow") // remove when gradle 8 + +val shadowDir = File(buildDir, "intermediates/shadowed") + +tasks.register("relocateJar") { + val javaTask = tasks.findByName("compileDebugJavaWithJavac")!! + val kotlinTask = tasks.findByName("compileDebugKotlin")!! + from(javaTask.outputs, kotlinTask.outputs) + relocate("com.aliucord.coreplugins.slashcommandsfix", "moe.lava.corenary.slashcommandsfix") + archiveClassifier.set("shadowed") + destinationDirectory.set(File(buildDir, "intermediates")) +} + +tasks.register("copyShadowed") { + val reloc = tasks.findByName("relocateJar")!! as ShadowJar + dependsOn(reloc) + from(zipTree(reloc.archiveFile)) + into(shadowDir) +} + +project.afterEvaluate { + tasks.compileDex { + val copyShadowed = tasks.findByName("copyShadowed")!! as Sync + dependsOn(copyShadowed) + input.setFrom(shadowDir) + } +} diff --git a/canary/SlashCommandsFix/src/main/AndroidManifest.xml b/canary/SlashCommandsFix/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6defd2a --- /dev/null +++ b/canary/SlashCommandsFix/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplication.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplication.java new file mode 100644 index 0000000..d1778d9 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplication.java @@ -0,0 +1,39 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import com.discord.models.user.User; +import com.discord.stores.StoreStream; +import java.util.Optional; + +class ApiApplication { + public final long id; + public final String name; + public final String icon; + public final ApiPermissions permissions; + public final Long botId; + + public ApiApplication() { + this.id = 0; + this.name = null; + this.icon = null; + this.permissions = null; + this.botId = null; + } + + public Application toModel() { + Permissions permissions = null; + if (this.permissions != null) { + permissions = this.permissions.toModel(Optional.empty()); + } else { + permissions = new Permissions(null, null, null, null); + } + var usersStore = StoreStream.getUsers(); + Optional botUser = Optional.ofNullable(this.botId).map(userId -> usersStore.getUsers().get(userId)); + return new Application(this.id, this.name, this.icon, permissions, botUser); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplicationCommand.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplicationCommand.java new file mode 100644 index 0000000..62e129b --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplicationCommand.java @@ -0,0 +1,59 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import com.discord.models.commands.ApplicationCommand; +import com.discord.stores.StoreApplicationCommandsKt; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +class ApiApplicationCommand { + public final long id; + public final long applicationId; + public final String name; + public final String description; + public final List options; + public final ApiPermissions permissions; + public final Long defaultMemberPermissions; + public final Long guildId; + public final String version; + public final int type; + + public ApiApplicationCommand() { + this.id = 0; + this.applicationId = 0; + this.name = null; + this.description = null; + this.options = null; + this.permissions = null; + this.defaultMemberPermissions = null; + this.guildId = null; + this.version = null; + this.type = 0; + } + + public RemoteApplicationCommand toModel() { + var apiOptions = this.options; + if (apiOptions == null) { + apiOptions = new ArrayList<>(); + } + var options = apiOptions + .stream() + .map(option -> StoreApplicationCommandsKt.toSlashCommandOption(option)) + .collect(Collectors.toList()); + Permissions permissions = null; + var defaultMemberPermissions = Optional.ofNullable(this.defaultMemberPermissions); + if (this.permissions != null) { + permissions = this.permissions.toModel(defaultMemberPermissions); + } else { + permissions = new Permissions(null, null, null, defaultMemberPermissions); + } + return new RemoteApplicationCommand(String.valueOf(this.id), this.applicationId, this.name, this.description, options, permissions, this.guildId, this.version, this.type); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplicationIndex.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplicationIndex.java new file mode 100644 index 0000000..188b6c1 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiApplicationIndex.java @@ -0,0 +1,34 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +class ApiApplicationIndex { + public List applications; + public List applicationCommands; + + public ApiApplicationIndex() { + this.applications = null; + this.applicationCommands = null; + } + + public ApplicationIndex toModel() { + var applications = new HashMap(); + for (var application: this.applications) { + applications.put(application.id, application.toModel()); + } + var applicationCommands = new HashMap(); + for (var applicationCommand: this.applicationCommands) { + applicationCommands.put(applicationCommand.id, applicationCommand.toModel()); + } + + return new ApplicationIndex(applications, applicationCommands); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiGuildApplicationCommandIndexUpdate.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiGuildApplicationCommandIndexUpdate.java new file mode 100644 index 0000000..91c5b4e --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiGuildApplicationCommandIndexUpdate.java @@ -0,0 +1,15 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +class ApiGuildApplicationCommandIndexUpdate { + public long guildId; + + public ApiGuildApplicationCommandIndexUpdate() { + this.guildId = 0; + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiPermissions.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiPermissions.java new file mode 100644 index 0000000..2bbe6e8 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApiPermissions.java @@ -0,0 +1,26 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.Map; +import java.util.Optional; + +class ApiPermissions { + public Boolean user; + public Map roles; + public Map channels; + + public ApiPermissions() { + this.user = null; + this.roles = null; + this.channels = null; + } + + public Permissions toModel(Optional defaultMemberPermissions) { + return new Permissions(Optional.ofNullable(user), roles, channels, defaultMemberPermissions); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Application.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Application.java new file mode 100644 index 0000000..67ecf2b --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Application.java @@ -0,0 +1,21 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import com.aliucord.Logger; +import com.discord.models.user.User; +import com.discord.utilities.user.UserUtils; +import java.util.Optional; + +class Application extends com.discord.models.commands.Application { + public Permissions permissions_; + + public Application(long id, String name, String icon, Permissions permissions, Optional botUser) { + super(id, name, icon, null, -1, botUser.map(user -> UserUtils.INSTANCE.synthesizeApiUser(user)).orElse(null), false); + this.permissions_ = permissions; + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndex.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndex.java new file mode 100644 index 0000000..a3fab96 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndex.java @@ -0,0 +1,44 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.lang.IllegalAccessException; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class ApplicationIndex { + public Map applications; + public Map applicationCommands; + + public ApplicationIndex(Map applications, Map applicationCommands) { + this.applications = applications; + this.applicationCommands = applicationCommands; + } + + public ApplicationIndex(List applicationIndexes) { + this.applications = new HashMap(); + this.applicationCommands = new HashMap(); + for (var applicationIndex: applicationIndexes) { + this.applications.putAll(applicationIndex.applications); + this.applicationCommands.putAll(applicationIndex.applicationCommands); + } + } + + public void populateCommandCounts(Field applicationCommandCountField) throws IllegalAccessException { + var applicationCommandCounts = new HashMap(); + for (var applicationCommand: this.applicationCommands.values()) { + var count = applicationCommandCounts.getOrDefault(applicationCommand.getApplicationId(), 0); + count += 1; + applicationCommandCounts.put(applicationCommand.getApplicationId(), count); + } + for (var application: this.applications.values()) { + applicationCommandCountField.setInt(application, applicationCommandCounts.getOrDefault(application.getId(), 0)); + } + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexCache.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexCache.java new file mode 100644 index 0000000..87022ce --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexCache.java @@ -0,0 +1,23 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +class ApplicationIndexCache { + public Map guild; + public Map dm; + public Optional user; + + public ApplicationIndexCache() { + this.guild = new HashMap<>(); + this.dm = new HashMap<>(); + this.user = Optional.empty(); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSource.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSource.java new file mode 100644 index 0000000..84b11a4 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSource.java @@ -0,0 +1,16 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.Optional; + +interface ApplicationIndexSource { + String getEndpoint(); + Optional getFromCache(ApplicationIndexCache cache); + void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index); + void removeFromCache(ApplicationIndexCache cache); +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceDm.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceDm.java new file mode 100644 index 0000000..3fb0c94 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceDm.java @@ -0,0 +1,39 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.Optional; + +class ApplicationIndexSourceDm implements ApplicationIndexSource { + long channelId; + + public ApplicationIndexSourceDm(long channelId) { + this.channelId = channelId; + } + + @Override + public String getEndpoint() { + return String.format("/channels/%d/application-command-index", this.channelId); + } + + @Override + public Optional getFromCache(ApplicationIndexCache cache) { + return Optional.ofNullable( + cache.dm.get(this.channelId) + ); + } + + @Override + public void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index) { + cache.dm.put(this.channelId, index); + } + + @Override + public void removeFromCache(ApplicationIndexCache cache) { + cache.dm.remove(this.channelId); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceGuild.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceGuild.java new file mode 100644 index 0000000..5588134 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceGuild.java @@ -0,0 +1,40 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.Map; +import java.util.Optional; + +class ApplicationIndexSourceGuild implements ApplicationIndexSource { + long guildId; + + public ApplicationIndexSourceGuild(long guildId) { + this.guildId = guildId; + } + + @Override + public String getEndpoint() { + return String.format("/guilds/%d/application-command-index", this.guildId); + } + + @Override + public Optional getFromCache(ApplicationIndexCache cache) { + return Optional.ofNullable( + cache.guild.get(this.guildId) + ); + } + + @Override + public void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index) { + cache.guild.put(this.guildId, index); + } + + @Override + public void removeFromCache(ApplicationIndexCache cache) { + cache.guild.remove(this.guildId); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceUser.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceUser.java new file mode 100644 index 0000000..19cbf66 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ApplicationIndexSourceUser.java @@ -0,0 +1,33 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import java.util.Optional; + +class ApplicationIndexSourceUser implements ApplicationIndexSource { + public ApplicationIndexSourceUser() {} + + @Override + public String getEndpoint() { + return "/users/@me/application-command-index"; + } + + @Override + public Optional getFromCache(ApplicationIndexCache cache) { + return cache.user; + } + + @Override + public void insertIntoCache(ApplicationIndexCache cache, ApplicationIndex index) { + cache.user = Optional.of(index); + } + + @Override + public void removeFromCache(ApplicationIndexCache cache) { + cache.user = Optional.empty(); + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ConflictCheck.kt b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ConflictCheck.kt new file mode 100644 index 0000000..7b3bf0c --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/ConflictCheck.kt @@ -0,0 +1,53 @@ +package com.aliucord.coreplugins.slashcommandsfix + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import com.aliucord.* +import com.aliucord.fragments.ConfirmDialog +import java.io.File +import kotlin.system.exitProcess + +object ConflictCheck { + @SuppressLint("SetTextI18n") + @JvmStatic + fun run(context: Context): Boolean { + val hasFix = PluginManager.plugins.containsKey("SlashCommandsFix") + val hasForcedFix = PluginManager.plugins.containsKey("ForceSlashCommandsFixNOW") + val fromStorage = Main.settings.getBool("AC_from_storage", false) + + if (hasFix) { + Logger("SlashCommandsFixBeta").warn("conflict detected") + if (hasForcedFix || fromStorage) { + Utils.threadPool.execute { + Thread.sleep(5000) // wait for app to load guh + Utils.mainThread.post { + val dialog = ConfirmDialog() + dialog + .setTitle("SlashCommandsFix Conflict") + .setDescription("You have another variant of SlashCommandsFix installed. Do you want to disable it?") + .setIsDangerous(true) + .setOnOkListener { + File(context.codeCacheDir, "Aliucord.zip").delete() + if (fromStorage) + Main.settings.setBool("AC_from_storage", false) + if (hasForcedFix) + PluginManager.disablePlugin("ForceSlashCommandsFixNOW") + val ctx = it.context + val intent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName) + Utils.appActivity.startActivity(Intent.makeRestartActivityTask(intent!!.component)) + exitProcess(0) + } + .apply { isCancelable = false } + .show(Utils.appActivity.supportFragmentManager, "SlashCommandsFix conflict") + } + } + } else { + Logger("SlashCommandsFixBeta").warn("removing myself... bye!") + File("${Constants.PLUGINS_PATH}/SlashCommandsFixBeta.zip").delete() + } + } + + return hasFix + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Patches.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Patches.java new file mode 100644 index 0000000..3bbeba7 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Patches.java @@ -0,0 +1,315 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import android.content.Context; +import com.aliucord.api.GatewayAPI; +import com.aliucord.Http; +import com.aliucord.Logger; +import com.aliucord.patcher.InsteadHook; +import com.aliucord.patcher.Patcher; +import com.aliucord.patcher.PreHook; +import com.aliucord.Utils; +import com.aliucord.utils.GsonUtils; +import com.discord.api.channel.Channel; +import com.discord.models.commands.Application; +import com.discord.models.commands.ApplicationCommand; +import com.discord.models.commands.ApplicationCommandKt; +import com.discord.models.commands.ApplicationCommandLocalSendData; +import com.discord.stores.BuiltInCommandsProvider; +import com.discord.stores.StoreApplicationCommands; +import com.discord.stores.StoreApplicationCommands$requestApplicationCommands$1; +import com.discord.stores.StoreApplicationCommands$requestApplicationCommandsQuery$1; +import com.discord.stores.StoreApplicationInteractions; +import com.discord.stores.StoreChannelsSelected; +import com.discord.stores.StoreStream; +import com.discord.utilities.error.Error; +import com.discord.utilities.messagesend.MessageResult; +import com.discord.utilities.permissions.PermissionUtils; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; + +final class Patches { + private ApplicationIndexCache applicationIndexCache; + private Logger logger; + private Method handleGuildApplicationsUpdateMethod; + private Method handleDiscoverCommandsUpdateMethod; + private Method handleQueryCommandsUpdateMethod; + private Field applicationCommandCountField; + private Field storeApplicationCommandsQueryField; + private Field errorResponseErrorField; + private Field skemaErrorSubErrorsField; + private Field skemaErrorErrorsField; + private Field skemaErrorItemCodeField; + private Field skemaErrorItemMessageField; + private Field storeApplicationCommandsBuiltInCommandsProviderField; + + Patches(Logger logger) throws Throwable { + this.logger = logger; + this.applicationIndexCache = new ApplicationIndexCache(); + } + + @SuppressWarnings("unchecked") + public void loadPatches(Context context) throws Throwable { + this.handleGuildApplicationsUpdateMethod = StoreApplicationCommands.class.getDeclaredMethod("handleGuildApplicationsUpdate", List.class); + this.handleGuildApplicationsUpdateMethod.setAccessible(true); + this.handleDiscoverCommandsUpdateMethod = StoreApplicationCommands.class.getDeclaredMethod("handleDiscoverCommandsUpdate", List.class); + this.handleDiscoverCommandsUpdateMethod.setAccessible(true); + this.handleQueryCommandsUpdateMethod = StoreApplicationCommands.class.getDeclaredMethod("handleQueryCommandsUpdate", List.class); + this.handleQueryCommandsUpdateMethod.setAccessible(true); + this.applicationCommandCountField = Application.class.getDeclaredField("commandCount"); + this.applicationCommandCountField.setAccessible(true); + this.storeApplicationCommandsQueryField = StoreApplicationCommands.class.getDeclaredField("query"); + this.storeApplicationCommandsQueryField.setAccessible(true); + this.errorResponseErrorField = Error.Response.class.getDeclaredField("skemaError"); + this.errorResponseErrorField.setAccessible(true); + this.skemaErrorSubErrorsField = Error.SkemaError.class.getDeclaredField("subErrors"); + this.skemaErrorSubErrorsField.setAccessible(true); + this.skemaErrorErrorsField = Error.SkemaError.class.getDeclaredField("errors"); + this.skemaErrorErrorsField.setAccessible(true); + this.skemaErrorItemCodeField = Error.SkemaErrorItem.class.getDeclaredField("code"); + this.skemaErrorItemCodeField.setAccessible(true); + this.skemaErrorItemMessageField = Error.SkemaErrorItem.class.getDeclaredField("message"); + this.skemaErrorItemMessageField.setAccessible(true); + this.storeApplicationCommandsBuiltInCommandsProviderField = StoreApplicationCommands.class.getDeclaredField("builtInCommandsProvider"); + this.storeApplicationCommandsBuiltInCommandsProviderField.setAccessible(true); + + var storeApplicationCommands = StoreStream.getApplicationCommands(); + var storeChannelsSelected = StoreStream.getChannelsSelected(); + var storeUsers = StoreStream.getUsers(); + var storePermissions = StoreStream.getPermissions(); + var storeGuilds = StoreStream.getGuilds(); + + // Browsing commands (when just a '/' is typed) + Patcher.addPatch( + StoreApplicationCommands$requestApplicationCommands$1.class.getDeclaredMethod("invoke"), + new PreHook(param -> { + var this_ = (StoreApplicationCommands$requestApplicationCommands$1) param.thisObject; + + if (this_.$guildId == null) { + return; + } + + var applicationIndexSource = Patches.applicationIndexSourceFromContext(this_.$guildId, storeChannelsSelected); + try { + this.passCommandData(this_.this$0, applicationIndexSource, RequestSource.BROWSE); + } catch (Exception e) { + throw new RuntimeException(e); + } + + param.setResult(null); + }) + ); + + // Completing commands + Patcher.addPatch( + StoreApplicationCommands$requestApplicationCommandsQuery$1.class.getDeclaredMethod("invoke"), + new PreHook(param -> { + var this_ = (StoreApplicationCommands$requestApplicationCommandsQuery$1) param.thisObject; + + if (this_.$guildId == null) { + return; + } + + var applicationIndexSource = Patches.applicationIndexSourceFromContext(this_.$guildId, storeChannelsSelected); + try { + storeApplicationCommandsQueryField.set(this_.this$0, this_.$query); + this.passCommandData(this_.this$0, applicationIndexSource, RequestSource.QUERY); + } catch (Exception e) { + throw new RuntimeException(e); + } + + param.setResult(null); + }) + ); + + // Command permission check + Patcher.addPatch( + ApplicationCommandKt.class.getDeclaredMethod("hasPermission", ApplicationCommand.class, long.class, List.class), + new InsteadHook(param -> { + var applicationCommand = (ApplicationCommand) param.args[0]; + var roleIds = (List) param.args[2]; + + if (!(applicationCommand instanceof RemoteApplicationCommand)) { + // Allow all builtin commands + return true; + } + var remoteApplicationCommand = (RemoteApplicationCommand) applicationCommand; + + var channel = storeChannelsSelected.getSelectedChannel(); + var guildId = channel.i(); + + if (guildId == 0) { + // Allow all commands in DMs + return true; + } + + var applicationId = remoteApplicationCommand.getApplicationId(); + var isUser = this.requestApplicationIndex(new ApplicationIndexSourceUser()) + .applications + .containsKey(applicationId); + if (isUser) { + // Allow all user application commands + return true; + } + var application = this.requestApplicationIndex(new ApplicationIndexSourceGuild(guildId)) + .applications + .get(applicationId); + if (application == null) { + // Discord requested checking a command from the previous guild - ignore + // Some such requests are still processed (if the command exists in both guilds), but it's not an issue as the result doesn't matter for them anyways. + return false; + } + var user = storeUsers.getMe(); + var memberPermissions = storePermissions.getGuildPermissions() + .get(guildId); + var guild = storeGuilds.getGuild(guildId); + + var applicationPermission = application.permissions_.checkFor(roleIds, channel, guild, memberPermissions, user, true); + var commandPermission = remoteApplicationCommand.permissions_.checkFor(roleIds, channel, guild, memberPermissions, user, applicationPermission); + + return commandPermission; + }) + ); + + // Command error handling + Patcher.addPatch( + StoreApplicationInteractions.class.getDeclaredMethod("handleApplicationCommandResult", MessageResult.class, ApplicationCommandLocalSendData.class, Function0.class, Function1.class), + new PreHook(param -> { + var result = (MessageResult) param.args[0]; + var localSendData = (ApplicationCommandLocalSendData) param.args[1]; + + if (result instanceof MessageResult.UnknownFailure) { + boolean invalidCommandVersion = false; + + try { + var errorResponse = ((MessageResult.UnknownFailure) result) + .getError() + .getResponse(); + var error = this.errorResponseErrorField.get(errorResponse); + var subErrors = ((Map) skemaErrorSubErrorsField.get(error)); + var dataErrors = (List) skemaErrorErrorsField.get(subErrors.get("data")); + + for (var dataError: dataErrors) { + var errorCode = (String) this.skemaErrorItemCodeField.get(dataError); + if (errorCode.equals("INTERACTION_APPLICATION_COMMAND_INVALID_VERSION")) { + ApplicationIndexSource applicationIndexSource = null; + var guildId = localSendData.component3(); + if (guildId != null) { + applicationIndexSource = new ApplicationIndexSourceGuild(guildId); + } else { + var channelId = localSendData.component2(); + applicationIndexSource = new ApplicationIndexSourceDm(channelId); + } + this.cleanApplicationIndexCache(applicationIndexSource); + + var errorMessage = (String) this.skemaErrorItemMessageField.get(dataError); + Utils.showToast(errorMessage); + + break; + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }) + ); + + GatewayAPI.onEvent("GUILD_APPLICATION_COMMAND_INDEX_UPDATE", ApiGuildApplicationCommandIndexUpdate.class, guildApplicationCommandIndexUpdate -> { + this.cleanApplicationIndexCache(new ApplicationIndexSourceGuild(guildApplicationCommandIndexUpdate.guildId)); + return null; + }); + } + + private void passCommandData(StoreApplicationCommands storeApplicationCommands, Optional applicationIndexSource, RequestSource requestSource) throws Exception { + var applicationIndexes = new ArrayList(); + if (applicationIndexSource.isPresent()) { + applicationIndexes.add(this.requestApplicationIndex(applicationIndexSource.get())); + } + applicationIndexes.add(this.requestApplicationIndex(new ApplicationIndexSourceUser())); + var applicationIndex = new ApplicationIndex(applicationIndexes); + applicationIndex + .applicationCommands + .entrySet() + .removeIf(applicationCommand -> applicationCommand.getValue().type != RemoteApplicationCommand.TYPE_CHAT_INPUT); + applicationIndex.populateCommandCounts(this.applicationCommandCountField); + + var applications = new ArrayList(applicationIndex.applications.values()); + Collections.sort(applications, (left, right) -> left.getName().compareTo(right.getName())); + applications.add(((BuiltInCommandsProvider) this.storeApplicationCommandsBuiltInCommandsProviderField.get(storeApplicationCommands)).getBuiltInApplication()); + this.handleGuildApplicationsUpdateMethod.invoke(storeApplicationCommands, applications); + + switch (requestSource) { + case BROWSE: + this.handleDiscoverCommandsUpdateMethod.invoke(storeApplicationCommands, new ArrayList(applicationIndex.applicationCommands.values())); + break; + + case QUERY: + this.handleQueryCommandsUpdateMethod.invoke(storeApplicationCommands, new ArrayList(applicationIndex.applicationCommands.values())); + break; + } + } + + private ApplicationIndex requestApplicationIndex(ApplicationIndexSource source) { + // Reuse application index from cache + var applicationIndex = source.getFromCache(applicationIndexCache); + if (!applicationIndex.isPresent()) { + try { + // Request application index from API + applicationIndex = Optional.of( + Http.Request.newDiscordRNRequest(source.getEndpoint()) + .execute() + .json(GsonUtils.getGsonRestApi(), ApiApplicationIndex.class) + .toModel() + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + + source.insertIntoCache(applicationIndexCache, applicationIndex.get()); + } + return applicationIndex.get(); + } + + private void cleanApplicationIndexCache(ApplicationIndexSource source) { + source.removeFromCache(applicationIndexCache); + } + + private static Optional applicationIndexSourceFromContext(long guildId, StoreChannelsSelected storeChannelsSelected) { + Optional applicationIndexSource = Optional.empty(); + // guildId being 0 means this is a DM or a DM group + if (guildId != 0) { + applicationIndexSource = Optional.of(new ApplicationIndexSourceGuild(guildId)); + } else { + // Only create a DM index source for bots + var channel = storeChannelsSelected.getSelectedChannel(); + var channelType = channel.D(); + if (channelType == Channel.DM) { + var user = channel.z().get(0); + var userIsBot = Optional.ofNullable(user.e()) + .orElse(false); + if (userIsBot) { + var channelId = channel.k(); + applicationIndexSource = Optional.of(new ApplicationIndexSourceDm(channelId)); + } + } + } + + return applicationIndexSource; + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Permissions.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Permissions.java new file mode 100644 index 0000000..d80d0e7 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/Permissions.java @@ -0,0 +1,84 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import com.discord.api.channel.Channel; +import com.discord.api.permission.Permission; +import com.discord.models.guild.Guild; +import com.discord.models.user.MeUser; +import com.discord.utilities.permissions.PermissionUtils; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +class Permissions { + public Optional user; + public Map roles; + public Map channels; + public Optional defaultMemberPermissions; + + public Permissions(Optional user, Map roles, Map channels, Optional defaultMemberPermissions) { + this.user = Optional.ofNullable(user).orElse(Optional.empty()); + this.roles = Optional.ofNullable(roles).orElse(new HashMap<>()); + this.channels = Optional.ofNullable(channels).orElse(new HashMap<>()); + this.defaultMemberPermissions = Optional.ofNullable(defaultMemberPermissions).orElse(Optional.empty()); + } + + public boolean checkFor(List roleIds, Channel channel, Guild guild, long memberPermissions, MeUser user, boolean defaultPermission) { + var guildId = guild.component7(); + var defaultChannelPermissionId = guildId - 1; + var defaultChannelPermission = this.channels.getOrDefault(defaultChannelPermissionId, defaultPermission); + var channelType = channel.D(); + var channelId = channel.k(); + var permissionChannelId = channelId; + // Threads inherit permissions from their parent channels + if (channelType == Channel.ANNOUNCEMENT_THREAD || channelType == Channel.PUBLIC_THREAD || channelType == Channel.PRIVATE_THREAD) { + var channelParentId = channel.u(); + permissionChannelId = channelParentId; + } + var channelPermission = Optional.ofNullable(this.channels.get(permissionChannelId)) + .orElse(defaultChannelPermission); + var defaultMemberPermission = this.defaultMemberPermissions + .map( + defaultMemberPermissions -> defaultMemberPermissions != 0 + && PermissionUtils.canAndIsElevated( + defaultMemberPermissions, + memberPermissions, + user.getMfaEnabled(), + guild.getMfaLevel() + ) + ) + .orElse(defaultPermission); + var everyoneRoleId = guildId; + var defaultRolePermission = this.roles.getOrDefault(everyoneRoleId, defaultMemberPermission); + var rolePermission = this.calculateRolePermission(roleIds, defaultRolePermission); + var userPermission = this.user.orElse(defaultMemberPermission); + var administratorPermission = PermissionUtils.canAndIsElevated( + Permission.ADMINISTRATOR, + memberPermissions, + user.getMfaEnabled(), + guild.getMfaLevel() + ); + + return administratorPermission || (channelPermission && (userPermission || rolePermission)); + } + + private boolean calculateRolePermission(List roleIds, boolean defaultPermission) { + var calculatedRolePermission = defaultPermission; + for (var roleId: roleIds) { + var rolePermission = this.roles.get(roleId); + if (rolePermission != null) { + calculatedRolePermission = rolePermission; + if (rolePermission) { + break; + } + } + } + return calculatedRolePermission; + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/RemoteApplicationCommand.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/RemoteApplicationCommand.java new file mode 100644 index 0000000..995a856 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/RemoteApplicationCommand.java @@ -0,0 +1,23 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import com.discord.models.commands.ApplicationCommandOption; +import java.util.List; + +class RemoteApplicationCommand extends com.discord.models.commands.RemoteApplicationCommand { + public Permissions permissions_; + public int type; + + public static final int TYPE_CHAT_INPUT = 1; + + public RemoteApplicationCommand(String id, long applicationId, String name, String description, List options, Permissions permissions, Long guildId, String version, int type) { + super(id, applicationId, name, description, options, guildId, version, null, null, null); + this.permissions_ = permissions; + this.type = type; + } +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/RequestSource.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/RequestSource.java new file mode 100644 index 0000000..1422bf5 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/RequestSource.java @@ -0,0 +1,12 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +enum RequestSource { + BROWSE, + QUERY; +} diff --git a/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java new file mode 100644 index 0000000..eeef992 --- /dev/null +++ b/canary/SlashCommandsFix/src/main/java/com/aliucord/coreplugins/slashcommandsfix/SlashCommandsFix.java @@ -0,0 +1,34 @@ +/* + * This file is part of Aliucord, an Android Discord client mod. + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.coreplugins.slashcommandsfix; + +import android.content.Context; + +import com.aliucord.annotations.AliucordPlugin; +import com.aliucord.entities.Plugin; + +import de.robv.android.xposed.XposedBridge; + +@AliucordPlugin +public final class SlashCommandsFix extends Plugin { + public SlashCommandsFix() { + super(); + } + + @Override + public void start(Context context) throws Throwable { + if (ConflictCheck.run(context)) return; + + XposedBridge.makeClassInheritable(com.discord.models.commands.Application.class); + XposedBridge.makeClassInheritable(com.discord.models.commands.RemoteApplicationCommand.class); + + new Patches(this.logger).loadPatches(context); + } + + @Override + public void stop(Context context) {} +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e22cef1..a005551 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ rootProject.name = "Awoocord" -val canaryPlugins = arrayOf("ComponentsV2") +val canaryPlugins = arrayOf("ComponentsV2", "SlashCommandsFix") include( "AlignThreads",