├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .hooks └── pre-commit ├── .scannerwork ├── .sonar_lock └── report-task.txt ├── AliuEval ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── aliueval │ └── AliuEval.kt ├── AnimateApngs ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── animateapngs │ └── AnimateApngs.java ├── BetterSpotify ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── betterspotify │ ├── BetterSpotify.kt │ ├── SpotifyApi.kt │ └── models │ └── PlayerInfo.kt ├── Brainfuck ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── brainfuck │ └── Brainfuck.java ├── CheckLinks ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── checklinks │ ├── CheckLinks.kt │ └── Models.kt ├── DedicatedPluginSettings ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── dps │ ├── DedicatedPluginSettings.java │ ├── DragAndDropHelper.kt │ └── PluginsAdapter.kt ├── EmojiReplacer ├── README.md ├── build.gradle.kts ├── makezip.js └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── emojireplacer │ ├── EmojiReplacer.kt │ ├── Settings.kt │ └── Util.kt ├── EmojiUtility ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── emojiutility │ ├── Commands.kt │ ├── EmojiDownloader.java │ ├── EmojiUtility.java │ ├── Patches.java │ ├── Settings.java │ └── clonemodal │ ├── Adapter.java │ ├── Modal.java │ └── ViewHolder.java ├── FixEmotes ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── fixemotes │ └── FixEmotes.kt ├── GatewayLog ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── gatewaylog │ └── GatewayLog.kt ├── Hastebin ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── hastebin │ ├── HasteResponse.java │ ├── Hastebin.java │ └── PluginSettings.java ├── JsEval ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── jseval │ ├── CodeTextView.kt │ ├── JsEval.kt │ ├── TerminalButton.kt │ ├── TerminalHistoryAdapter.kt │ └── TerminalPage.kt ├── LICENSE ├── MessageLinkEmbeds ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugs │ └── messagelinkembeds │ └── MessageLinkEmbeds.kt ├── PlayableEmbeds ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── betterplatformembeds │ └── PlayableEmbeds.kt ├── PluginDownloader ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── plugindownloader │ ├── Modal.java │ ├── PDUtil.java │ ├── Plugin.java │ └── PluginDownloader.java ├── README.md ├── ShowBlockedMessages ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── showblockedmessages │ └── ShowBlockedMessages.kt ├── TapTap ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── taptap │ ├── TapTap.java │ └── TapTapSettings.kt ├── Template ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── template │ └── Template.kt ├── TextFilePreview ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugins │ └── textfilepreview │ ├── AttachmentPreviewWidget.kt │ ├── LeSettings.kt │ └── TextFilePreview.kt ├── Themer ├── Firefly.json ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── dev │ └── vendicated │ └── aliucordplugs │ └── themer │ ├── Constants.kt │ ├── Legacy.kt │ ├── Patches.kt │ ├── ResourceManager.kt │ ├── Theme.kt │ ├── ThemeLoader.kt │ ├── Themer.kt │ ├── Util.kt │ └── settings │ ├── ThemeCard.kt │ ├── ThemeLayout.kt │ ├── ThemerSettings.kt │ └── editor │ ├── ThemeEditor.kt │ └── tabs │ ├── FormInputTabs.kt │ ├── Tab.kt │ └── color │ ├── ColorAdapter.kt │ ├── ColorPickerListener.kt │ ├── ColorTab.kt │ ├── ColorTuple.kt │ ├── ColorViewHolder.kt │ └── NewColorDialog.kt ├── UrbanDictionary ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── urbandictionary │ ├── ApiResponse.kt │ └── UrbanDictionary.kt ├── ViewProfileImages ├── README.md ├── build.gradle.kts ├── demo.gif └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── dev │ └── vendicated │ └── aliucordplugs │ └── viewprofileimages │ └── ViewProfileImages.java ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── licenseHeader.txt ├── licenser.sh └── new.sh ├── settings.gradle.kts └── uwu /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | max_line_length = 150 5 | end_of_line = lf 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | concurrency: 4 | group: "build" 5 | cancel-in-progress: true 6 | 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-20.04 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@master 21 | with: 22 | path: "src" 23 | 24 | - name: Checkout builds 25 | uses: actions/checkout@master 26 | with: 27 | ref: "builds" 28 | path: "builds" 29 | 30 | - name: Setup JDK 11 31 | uses: actions/setup-java@v2 32 | with: 33 | java-version: 11 34 | distribution: zulu 35 | cache: gradle 36 | 37 | - name: Setup Android SDK 38 | uses: android-actions/setup-android@v2 39 | 40 | - name: Build Plugins 41 | run: | 42 | cd $GITHUB_WORKSPACE/src 43 | chmod +x gradlew 44 | ./gradlew make generateUpdaterJson 45 | cp **/build/*.zip $GITHUB_WORKSPACE/builds 46 | cp build/updater.json $GITHUB_WORKSPACE/builds 47 | 48 | - name: Push builds 49 | run: | 50 | cd $GITHUB_WORKSPACE/builds 51 | git config --local user.email "actions@github.com" 52 | git config --local user.name "GitHub Actions" 53 | git add . 54 | git commit -m "Build $GITHUB_SHA" || true # do not error if nothing to commit 55 | git push 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | **/build 12 | Test 13 | OwO 14 | sonar-project.properties 15 | # unfinished 16 | SnowflakeLookup 17 | -------------------------------------------------------------------------------- /.hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | 4 | scripts/licenser.sh 5 | -------------------------------------------------------------------------------- /.scannerwork/.sonar_lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vendicated/AliucordPlugins/a1decb53c13219145ef2336c818d5e2b348f61cc/.scannerwork/.sonar_lock -------------------------------------------------------------------------------- /.scannerwork/report-task.txt: -------------------------------------------------------------------------------- 1 | projectKey=AliucordPlugins 2 | serverUrl=http://localhost:9000 3 | serverVersion=9.1.0.47736 4 | dashboardUrl=http://localhost:9000/dashboard?id=AliucordPlugins 5 | ceTaskId=AX2Io6lgKu2Z6sTfyIcy 6 | ceTaskUrl=http://localhost:9000/api/ce/task?id=AX2Io6lgKu2Z6sTfyIcy 7 | -------------------------------------------------------------------------------- /AliuEval/README.md: -------------------------------------------------------------------------------- 1 | # Template 2 | 3 | This is the Template I generate all my plugins from. Not much to see here :) 4 | -------------------------------------------------------------------------------- /AliuEval/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.0" 2 | description = "Punch 6pak for making me update 15 plugins" 3 | -------------------------------------------------------------------------------- /AliuEval/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AliuEval/src/main/kotlin/dev/vendicated/aliucordplugins/aliueval/AliuEval.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.aliueval 12 | 13 | import android.content.Context 14 | import com.aliucord.Http 15 | import com.aliucord.annotations.AliucordPlugin 16 | import com.aliucord.api.CommandsAPI 17 | import com.aliucord.entities.Plugin 18 | import com.aliucord.utils.* 19 | import dalvik.system.DexClassLoader 20 | import java.io.File 21 | import java.util.* 22 | 23 | class Code(val code: String) 24 | class CompileError(val stdout: String, val stderr: String) 25 | 26 | @AliucordPlugin 27 | class AliuEval : Plugin() { 28 | override fun start(ctx: Context) { 29 | commands.registerCommand("eval", "evaluate kotlin", CommandsAPI.requiredMessageOption) { 30 | try { 31 | val code = it.getRequiredString("message") 32 | val outFile = File(ctx.codeCacheDir, "eval.dex") 33 | Http.Request("https://aliueval.vendicated.dev", "POST").use { 34 | it.executeWithJson(Code(code)).saveToFile(outFile) 35 | } 36 | val cl = DexClassLoader(outFile.absolutePath, ctx.codeCacheDir.absolutePath, null, this.javaClass.classLoader) 37 | val clazz = cl.loadClass("dev.vendicated.aliucordeval.Eval") 38 | val instance = ReflectUtils.invokeConstructorWithArgs(clazz, patcher, settings, commands) 39 | val ret = ReflectUtils.invokeMethod(instance, "main") 40 | try { 41 | CommandsAPI.CommandResult("```json\n${GsonUtils.toJson(ret)}```", null, false) 42 | } catch (th: Throwable) { 43 | CommandsAPI.CommandResult("```${Objects.toString(ret)}```", null, false) 44 | } 45 | } catch (th: Throwable) { 46 | val msg = if (th is Http.HttpException && th.statusCode == 400) { 47 | val err = GsonUtils.fromJson(th.req.conn.errorStream.use { IOUtils.readAsText(it) }, CompileError::class.java) 48 | "STDOUT: ```\n${err.stdout + " "}```\nSTDERR: ```\n${err.stderr}```" 49 | } else "```\n${th.message}```" 50 | CommandsAPI.CommandResult(msg, null, false) 51 | } 52 | } 53 | } 54 | 55 | override fun stop(context: Context) { 56 | patcher.unpatchAll() 57 | commands.unregisterAll() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AnimateApngs/README.md: -------------------------------------------------------------------------------- 1 | # AnimateApngs - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/AnimateApngs.zip?raw=true) 2 | 3 | Simple plugin that makes apngs animate just like gifs 4 | 5 | ## Limitations 6 | 7 | Only images hosted on cdn.discordapp.com or media.discordapp.net links will be animated. Discord's proxy server does not serve apngs, 8 | so to animate proxied images the proxy would have to be stripped which would expose your IP to the image host. Thus third party image links can not be animated. 9 | -------------------------------------------------------------------------------- /AnimateApngs/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.4" 2 | description = "Makes apngs embeds animate properly" 3 | -------------------------------------------------------------------------------- /AnimateApngs/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AnimateApngs/src/main/java/dev/vendicated/aliucordplugs/animateapngs/AnimateApngs.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.animateapngs; 12 | 13 | import android.content.Context; 14 | import android.net.Uri; 15 | import android.widget.ImageView; 16 | 17 | import com.aliucord.Http; 18 | import com.aliucord.Utils; 19 | import com.aliucord.annotations.AliucordPlugin; 20 | import com.aliucord.entities.Plugin; 21 | import com.aliucord.patcher.Hook; 22 | import com.discord.api.message.embed.EmbedType; 23 | import com.discord.embed.RenderableEmbedMedia; 24 | import com.discord.widgets.chat.list.InlineMediaView; 25 | import com.discord.widgets.media.WidgetMedia; 26 | 27 | import java.util.regex.Pattern; 28 | 29 | @AliucordPlugin 30 | public class AnimateApngs extends Plugin { 31 | private void initApng(ImageView view, String mediaUrl, Integer w, Integer h) { 32 | final var url = mediaUrl 33 | // Strip proxy but only if they're Discord domains. 34 | .replaceFirst("https://images-ext-.*?\\.discordapp\\.net/external/.*?/https/(?:media|cdn)\\.discordapp\\.(?:net|com)", "https://cdn.discordapp.com") 35 | // Media server serves them as regular PNGs, only CDN serves actual APNGs. 36 | .replace("media.discordapp.net", "cdn.discordapp.com"); 37 | 38 | Utils.threadPool.execute(() -> { 39 | try (var is = new Http.Request(url).execute().stream()) { 40 | // You cannot consume the stream twice so just yolo it and don't use Apng.Companion.isApng first, eh whatever. 41 | var drawable = b.l.a.a.a(is, w, h); 42 | if (view != null) 43 | Utils.mainThread.post(() -> { 44 | view.setImageDrawable(drawable); 45 | drawable.start(); 46 | }); 47 | } catch (Throwable ignored) { } 48 | }); 49 | } 50 | 51 | @Override 52 | public void start(Context ctx) throws Throwable { 53 | int previewResId = Utils.getResId("inline_media_image_preview", "id"); 54 | int mediaResId = Utils.getResId("media_image", "id"); 55 | 56 | var updateUI = InlineMediaView.class.getDeclaredMethod("updateUI", RenderableEmbedMedia.class, String.class, EmbedType.class, Integer.class, Integer.class, String.class); 57 | patcher.patch(updateUI, new Hook(param -> { 58 | var media = (RenderableEmbedMedia) param.args[0]; 59 | if (media == null || !media.a.endsWith(".png")) return; 60 | 61 | // Media server serves them as regular PNGs, only CDN serves actual APNGs. 62 | var url = media.a.replace("media.discordapp.net", "cdn.discordapp.com"); 63 | var binding = InlineMediaView.access$getBinding$p((InlineMediaView) param.thisObject); 64 | var view = (ImageView) binding.getRoot().findViewById(previewResId); 65 | initApng(view, url, media.b, media.c); 66 | })); 67 | 68 | 69 | var pattern = Pattern.compile("\\.png(?:\\?width=(\\d+)&height=(\\d+))?"); 70 | 71 | var uriField = WidgetMedia.class.getDeclaredField("imageUri"); 72 | uriField.setAccessible(true); 73 | var getFormattedUrl = WidgetMedia.class.getDeclaredMethod("getFormattedUrl", Context.class, Uri.class); 74 | getFormattedUrl.setAccessible(true); 75 | 76 | patcher.patch(WidgetMedia.class.getDeclaredMethod("configureMediaImage"), new Hook(param -> { 77 | try { 78 | var widgetMedia = (WidgetMedia) param.thisObject; 79 | var url = (String) getFormattedUrl.invoke(widgetMedia, widgetMedia.requireContext(), uriField.get(widgetMedia)); 80 | if (url == null) return; 81 | var match = pattern.matcher(url); 82 | if (match.find()) { 83 | String w = match.group(1); 84 | String h = match.group(2); 85 | var view = (ImageView) WidgetMedia.access$getBinding$p(widgetMedia).getRoot().findViewById(mediaResId); 86 | initApng(view, url, w != null ? Integer.parseInt(w) : null, h != null ? Integer.parseInt(h) : null); 87 | } 88 | } catch (Throwable ignored) { } 89 | })); 90 | } 91 | 92 | @Override 93 | public void stop(Context context) { 94 | patcher.unpatchAll(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /BetterSpotify/README.md: -------------------------------------------------------------------------------- 1 | # BetterSpotify - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/BetterSpotify.zip?raw=true) 2 | 3 | Better Spotify integration. 4 | 5 | Features: 6 | - Listen Along - You MUST have spotify running for listen along to work! so play a song first then use it 7 | - Commands to send current song or album in chat (sendSpotifySong/sendSpotifyAlbum) 8 | 9 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/886711483227058216/Screenshot_20210912-223432.jpg) 10 | 11 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/896146433131028510/Screenshot_20211008-232535.jpg) 12 | -------------------------------------------------------------------------------- /BetterSpotify/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.1.5" 2 | description = "Better Spotify Integration - Listen along with your friends!" 3 | 4 | aliucord.changelog.set(""" 5 | # 1.1.4 6 | * Remove spotify embeds. They are now part of my new plugin PlayableEmbeds, which also adds playable youtube embeds. 7 | 8 | # 1.1.2 9 | * Make spotify embeds scrollable <:blobcatcozy:859801776232202280> 10 | 11 | # 1.1.0 12 | * add sendSpotifySong and sendSpotifyAlbum commands 13 | * add rich spotify embeds with play button like on desktop 14 | """.trimIndent()) 15 | -------------------------------------------------------------------------------- /BetterSpotify/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /BetterSpotify/src/main/kotlin/dev/vendicated/aliucordplugins/betterspotify/SpotifyApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.betterspotify 12 | 13 | import com.aliucord.Http 14 | import com.aliucord.Utils 15 | import com.aliucord.utils.ReflectUtils 16 | import com.aliucord.utils.RxUtils 17 | import com.aliucord.utils.RxUtils.await 18 | import com.aliucord.utils.RxUtils.createActionSubscriber 19 | import com.aliucord.utils.RxUtils.subscribe 20 | import com.discord.stores.StoreStream 21 | import com.discord.utilities.platform.Platform 22 | import com.discord.utilities.rest.RestAPI 23 | import com.discord.utilities.spotify.SpotifyApiClient 24 | import dev.vendicated.aliucordplugins.betterspotify.models.PlayerInfo 25 | import java.util.concurrent.TimeUnit 26 | import kotlin.math.abs 27 | 28 | private const val baseUrl = "https://api.spotify.com/v1/me/player" 29 | 30 | // The spotify api gives me fucking brain damage i swear to god 31 | // You can either specify album or playlist uris as "context_uri" String or track uris as "uris" array 32 | @Suppress("Unused") 33 | class SongBody(val uris: List, val position_ms: Int = 0) 34 | 35 | object SpotifyApi { 36 | val client: SpotifyApiClient by lazy { 37 | ReflectUtils.getField(StoreStream.getSpotify(), "spotifyApiClient") as SpotifyApiClient 38 | } 39 | 40 | private var token: String? = null 41 | private fun getToken(): String? { 42 | if (token == null) { 43 | token = RestAPI.AppHeadersProvider.INSTANCE.spotifyToken 44 | ?: try { 45 | val accountId = ReflectUtils.getField(client, "spotifyAccountId") 46 | val (res, err) = RestAPI.api 47 | .getConnectionAccessToken(Platform.SPOTIFY.name.lowercase(), accountId as String) 48 | .await() 49 | err?.let { throw it } 50 | res!!.accessToken 51 | } catch (th: Throwable) { 52 | null 53 | } 54 | } 55 | return token 56 | } 57 | 58 | private var didTokenRefresh = false 59 | private fun request(endpoint: String, method: String = "PUT", data: Any? = null, cb: ((Http.Response) -> Unit)? = null) { 60 | Utils.threadPool.execute { 61 | val token = getToken() ?: run { 62 | Utils.showToast("Failed to get Spotify token from Discord. Make sure your spotify is running.") 63 | return@execute 64 | } 65 | 66 | try { 67 | Http.Request("$baseUrl/$endpoint", method) 68 | .setHeader("Authorization", "Bearer $token") 69 | .use { 70 | val res = 71 | if (data != null) 72 | it.executeWithJson(data) 73 | else 74 | it 75 | .setHeader("Content-Type", "application/json") 76 | .execute() 77 | 78 | res.assertOk() 79 | cb?.invoke(res) 80 | } 81 | } catch (th: Throwable) { 82 | if (th is Http.HttpException) { 83 | when (th.statusCode) { 84 | 401 -> { 85 | if (!didTokenRefresh) { 86 | didTokenRefresh = true 87 | SpotifyApiClient.`access$refreshSpotifyToken`(client) 88 | this.token = null 89 | RxUtils.timer(5, TimeUnit.SECONDS).subscribe( 90 | createActionSubscriber({ 91 | request(endpoint, method, data, cb) 92 | }) 93 | ) 94 | } else { 95 | BetterSpotify.stopListening(skipToast = true) 96 | logger.errorToast("Got \"Unauthorized\" Error. Try relinking Spotify") 97 | } 98 | return@execute 99 | } 100 | 404 -> { 101 | BetterSpotify.stopListening(skipToast = true) 102 | logger.errorToast("Failed to play. Make sure your Spotify is running", th) 103 | return@execute 104 | } 105 | } 106 | logger.errorToast("Failed to play that song :( Check the debug log", th) 107 | } 108 | } 109 | } 110 | } 111 | 112 | fun getPlayerInfo(cb: (PlayerInfo) -> Unit) { 113 | request("", "GET", cb = { 114 | cb.invoke(it.json(PlayerInfo::class.java)) 115 | }) 116 | } 117 | 118 | fun playSong(id: String, position_ms: Int) { 119 | request("play", "PUT", SongBody(listOf("spotify:track:$id"), position_ms)) 120 | } 121 | 122 | fun pause() { 123 | request("pause", "PUT") 124 | } 125 | 126 | fun resume() { 127 | request("play", "PUT") 128 | } 129 | 130 | fun seek(position_ms: Int) { 131 | getPlayerInfo { 132 | if (!it.is_playing) 133 | playSong(it.item.id, position_ms) 134 | else if (abs(it.progress_ms - position_ms) > 5000) 135 | request("seek?position_ms=$position_ms") 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /BetterSpotify/src/main/kotlin/dev/vendicated/aliucordplugins/betterspotify/models/PlayerInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.betterspotify.models 12 | 13 | class PlayerInfo( 14 | val progress_ms: Int, 15 | val is_playing: Boolean, 16 | val currently_playing_type: String, 17 | val shuffle_state: Boolean, 18 | val repeat_state: String, 19 | val device: Device, 20 | val item: Item 21 | ) { 22 | class Device(val name: String, val volume_percent: Int) 23 | class Item(val id: String, val name: String, val type: String, val uri: String) 24 | } 25 | -------------------------------------------------------------------------------- /Brainfuck/README.md: -------------------------------------------------------------------------------- 1 | # Brainfuck - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/Brainfuck.zip?raw=true) 2 | 3 | Brainfuck is an extremely minimal esoteric programming language with only 8 instructions: 4 | - `>` 5 | - `<` 6 | - `+` 7 | - `-` 8 | - `.` 9 | - `,` 10 | - `[` 11 | - `]` 12 | 13 | [More info (Wikipedia)](https://en.wikipedia.org/wiki/Brainfuck) 14 | 15 | This plugin includes both a command to interpret brainfuck (`/brainfuck`) and a command to convert plain text to brainfuck (`/tobrainfuck`), because who doesn't want to send 16 | ```bf 17 | >+++++++++[<+++++++++>-]<++++++.>+++++[<+++++>-]<-.++++++++.>+++++++++[<--------->-]<++++++.>+++[<--->-] 18 | <---.>++++++[<++++++>-]<+++++.>++++++[<------>-]<-----.>+++++++++[<+++++++++>-]<++.++.---.>++++[<---->-] 19 | <+++.>++++++++[<-------->-]<-----.>++++++++[<++++++++>-]<+.>+++[<+++>-]<+++.>+++++++++[<--------->-]<+++ 20 | +.>+++++++++[<+++++++++>-]<++++++.>+++++[<----->-]<+++.++.++++++++.>++++[<++++>-]<--. 21 | ``` 22 | in the middle of a conversation!!!! 23 | 24 | What does that say? Install the plugin and find out ;) 25 | 26 | ### Why? 27 | 28 | ![BECAUSE I'M CRAAAAAAAAAZY!!!!!](https://i.kym-cdn.com/photos/images/original/000/635/947/adf.png) 29 | -------------------------------------------------------------------------------- /Brainfuck/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.1" 2 | description = "Evaluates and converts to brainfuck" 3 | -------------------------------------------------------------------------------- /Brainfuck/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CheckLinks/README.md: -------------------------------------------------------------------------------- 1 | # CheckLinks - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/CheckLinks.zip?raw=true) 2 | 3 | Simple plugin that checks links using the VirusTotal api and shows the result in the "Are you sure you want to open this link" modal 4 | 5 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/873292910102208592/Screenshot_20210806-215403_Aliucord.png) 6 | -------------------------------------------------------------------------------- /CheckLinks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.8" 2 | description = "Checks links via the VirusTotal api" 3 | 4 | aliucord.changelog.set(""" 5 | Fix 6 | """.trimIndent()) 7 | -------------------------------------------------------------------------------- /CheckLinks/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CheckLinks/src/main/java/dev/vendicated/aliucordplugs/checklinks/CheckLinks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.checklinks 12 | 13 | import android.annotation.SuppressLint 14 | import android.content.Context 15 | import android.text.SpannableString 16 | import android.text.Spanned 17 | import android.text.method.LinkMovementMethod 18 | import android.text.style.ClickableSpan 19 | import android.text.style.URLSpan 20 | import android.view.View 21 | import android.widget.* 22 | import androidx.viewbinding.ViewBinding 23 | import com.aliucord.* 24 | import com.aliucord.Http.QueryBuilder 25 | import com.aliucord.annotations.AliucordPlugin 26 | import com.aliucord.entities.Plugin 27 | import com.aliucord.fragments.SettingsPage 28 | import com.aliucord.patcher.Hook 29 | import com.aliucord.utils.DimenUtils 30 | import com.discord.app.AppDialog 31 | import com.lytefast.flexinput.R 32 | import dev.vendicated.aliucordplugs.checklinks.* 33 | import java.lang.reflect.Method 34 | import java.util.* 35 | 36 | class MoreInfoModal(private val data: Map) : SettingsPage() { 37 | override fun onViewBound(view: View) { 38 | super.onViewBound(view) 39 | setActionBarTitle("URL info") 40 | 41 | val ctx = view.context 42 | val p = DimenUtils.defaultPadding 43 | val p2 = p / 2 44 | 45 | TableLayout(ctx).let { table -> 46 | for ((key, value) in data.toList().sortedBy { (_, value) -> value.result }.reversed()) { 47 | TableRow(ctx).let { row -> 48 | TextView(ctx, null, 0, R.i.UiKit_TextView).apply { 49 | text = key 50 | setPadding(p, p2, p, p2) 51 | row.addView(this) 52 | } 53 | TextView(ctx, null, 0, R.i.UiKit_TextView).apply { 54 | text = value.result 55 | setPadding(p, p2, p, p2) 56 | row.addView(this) 57 | } 58 | table.addView(row) 59 | } 60 | } 61 | addView(table) 62 | } 63 | } 64 | } 65 | 66 | private fun makeReq(url: String, method: String, contentType: String): Http.Request { 67 | val chars = ('A'..'Z') + ('a'..'z') + ('0'..'9') 68 | val s = CharArray(10) { chars.random() }.joinToString("") 69 | 70 | return Http.Request(url, method).apply { 71 | setHeader("Content-Type", contentType) 72 | setHeader("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:83.0) Firefox") 73 | setHeader("X-Tool", "vt-ui-main") 74 | setHeader("X-VT-Anti-Abuse-Header", s) // Can be anything for some reason 75 | setHeader("Accept-Ianguage", "en-US,en;q=0.9,es;q=0.8") // yes upper case i lol 76 | } 77 | } 78 | 79 | private fun checkLink(url: String): Map { 80 | // Look up url in cache first 81 | QueryBuilder("https://www.virustotal.com/ui/search").run { 82 | append("limit", "20") 83 | append("relationships[comment]", "author,item") 84 | append("query", url) 85 | 86 | makeReq(this.toString(), "GET", "application/json") 87 | .execute() 88 | .json(CachedUrlInfo::class.java) 89 | .let { res -> 90 | if (res.data.isNotEmpty()) return@checkLink res.data[0].attributes.last_analysis_results 91 | } 92 | } 93 | 94 | // no cached data, make full request for url 95 | 96 | // R.h.ster url to get an ID 97 | val idInfo = 98 | makeReq("https://www.virustotal.com/ui/urls", "POST", "application/x-www-form-urlencoded") 99 | .executeWithUrlEncodedForm(mapOf("url" to url)) 100 | .json(UrlIdInfo::class.java) 101 | 102 | // Request analysis with that ID 103 | return makeReq( 104 | "https://www.virustotal.com/ui/analyses/" + idInfo.data.id, 105 | "GET", 106 | "application/json" 107 | ) 108 | .execute() 109 | .json(NewUrlInfo::class.java) 110 | .data.attributes.results 111 | } 112 | 113 | 114 | @AliucordPlugin 115 | class CheckLinks : Plugin() { 116 | @SuppressLint("SetTextI18n") 117 | override fun start(ctx: Context) { 118 | var getBinding: Method? = null 119 | 120 | val dialogTextId = Utils.getResId("masked_links_body_text", "id") 121 | 122 | patcher.patch( 123 | b.a.a.g.a::class.java.getMethod("onViewBound", View::class.java), 124 | Hook { param -> 125 | val dialog = param.thisObject as AppDialog 126 | val url = dialog.arguments?.getString("WIDGET_SPOOPY_LINKS_DIALOG_URL") 127 | ?: return@Hook 128 | 129 | if (getBinding == null) { 130 | b.a.a.g.a::class.java.declaredMethods.find { 131 | ViewBinding::class.java.isAssignableFrom(it.returnType) 132 | }?.let { 133 | Logger("CheckLinks").info("Found obfuscated getBinding(): ${it.name}()") 134 | getBinding = it 135 | } ?: run { 136 | Logger("CheckLinks").error("Couldn't find obfuscated getBinding()", null) 137 | return@Hook 138 | } 139 | } 140 | val binding = getBinding!!.invoke(dialog) as ViewBinding 141 | val text = binding.root.findViewById(dialogTextId) 142 | text.text = "Checking URL $url..." 143 | 144 | Utils.threadPool.execute { 145 | var content: String 146 | var data: Map? = null 147 | try { 148 | data = checkLink(url) 149 | 150 | val counts = IntArray(4) 151 | data.values.forEach { v -> 152 | when (v.result) { 153 | "clean" -> counts[0]++ 154 | "phishing" -> counts[1]++ 155 | "malicious" -> counts[2]++ 156 | else -> counts[3]++ 157 | } 158 | } 159 | 160 | val malicious = counts[1] + counts[2] 161 | content = 162 | if (malicious > 0) 163 | "URL $url is ${if (malicious > 2) "likely" else "possibly"} malicious. $malicious engines flagged it as malicious." 164 | else 165 | "URL $url is either safe or too new to be flagged." 166 | } catch (th: Throwable) { 167 | Logger("[CheckLinks]").error("Failed to check link $url", th) 168 | content = "Failed to check URL $url. Proceed at your own risk." 169 | } 170 | 171 | if (data != null) content += "\n\nMore Info" 172 | 173 | SpannableString(content).run { 174 | val urlIdx = content.indexOf(url) 175 | setSpan(URLSpan(url), urlIdx, urlIdx + url.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) 176 | 177 | data?.let { 178 | setSpan(object : ClickableSpan() { 179 | override fun onClick(view: View) { 180 | Utils.openPageWithProxy(view.context, MoreInfoModal(it)) 181 | } 182 | }, content.length - 9, content.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) 183 | } 184 | 185 | Utils.mainThread.post { 186 | text.movementMethod = LinkMovementMethod.getInstance() 187 | text.text = this 188 | } 189 | } 190 | } 191 | }) 192 | } 193 | 194 | override fun stop(context: Context) { 195 | patcher.unpatchAll() 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /CheckLinks/src/main/java/dev/vendicated/aliucordplugs/checklinks/Models.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.checklinks 12 | 13 | class Entry(val result: String) 14 | 15 | class CachedUrlInfo(val data: List) { 16 | class Data(val attributes: Attributes) { 17 | class Attributes(val last_analysis_results: Map) 18 | } 19 | } 20 | 21 | class NewUrlInfo(val data: Data) { 22 | class Data(val attributes: Attributes) { 23 | class Attributes(val results: Map) 24 | } 25 | } 26 | 27 | class UrlIdInfo(val data: Data) { 28 | class Data(val id: String) 29 | } 30 | -------------------------------------------------------------------------------- /DedicatedPluginSettings/README.md: -------------------------------------------------------------------------------- 1 | # DedicatedPluginSettings - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/DedicatedPluginSettings.zip?raw=true) 2 | 3 | Adds a dedicated plugin settings category in the settings page right below Aliucord's settings. 4 | 5 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/869345405861756948/Screenshot_20210727-002617_Aliucord.png) 6 | 7 | 8 | ### Submitting a plugin icon 9 | 10 | If you would like to submit an icon for your plugin (Must be a discord drawable), please just dm me on Discord or something. 11 | 12 | Alternatively, you may declare a field `pluginIcon` of type Drawable on your plugin and that will be used, e.g.: 13 | 14 | ```java 15 | public class DedicatedPluginSettings extends Plugin { 16 | private Drawable pluginIcon; 17 | 18 | public void load(Context ctx) { 19 | pluginIcon = ContextCompat.getDrawable(ctx, R$d.ic_theme_24dp); 20 | } 21 | } 22 | ``` 23 | -------------------------------------------------------------------------------- /DedicatedPluginSettings/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.1.5" 2 | description = "Adds a dedicated plugin settings category to the settings page" 3 | 4 | aliucord.changelog.set( 5 | """ 6 | # 1.1.4 7 | * Make entries reorderable via drag & drop 8 | * Add reset button 9 | 10 | # 1.1.3 11 | * Fix a small bug I missed 12 | 13 | # 1.1.2 14 | * Plugin section is now a dropdown that you can open/close 15 | * New Customize Option: You can now hide plugins from the list 16 | 17 | # 1.1.0 18 | * Add more icons 19 | 20 | # 1.0.5 21 | * Fix crash when changing device orientation 22 | 23 | # 1.0.4 24 | * Improve implementation 25 | """.trimIndent() 26 | ) 27 | -------------------------------------------------------------------------------- /DedicatedPluginSettings/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /DedicatedPluginSettings/src/main/java/dev/vendicated/aliucordplugs/dps/DragAndDropHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.dps 12 | 13 | import androidx.recyclerview.widget.ItemTouchHelper 14 | import androidx.recyclerview.widget.RecyclerView 15 | import java.util.* 16 | 17 | class DragAndDropHelper : ItemTouchHelper.Callback() { 18 | var isEnabled = false 19 | 20 | override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { 21 | val flags = if (isEnabled) ItemTouchHelper.UP or ItemTouchHelper.DOWN else 0 22 | return makeMovementFlags(flags, 0) 23 | } 24 | 25 | override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { 26 | val fromPos = viewHolder.adapterPosition 27 | val toPos = target.adapterPosition 28 | Collections.swap((recyclerView.adapter as PluginsAdapter).data, fromPos, toPos) 29 | recyclerView.adapter!!.notifyItemMoved(fromPos, toPos) 30 | return true 31 | } 32 | 33 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { } 34 | } 35 | -------------------------------------------------------------------------------- /EmojiReplacer/README.md: -------------------------------------------------------------------------------- 1 | # EmojiReplacer 2 | 3 | Unfinished. 4 | -------------------------------------------------------------------------------- /EmojiReplacer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "0.0.1" 2 | description = "Punch 6pak for making me update 15 plugins" 3 | 4 | aliucord.excludeFromUpdaterJson.set(true) 5 | -------------------------------------------------------------------------------- /EmojiReplacer/makezip.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require("util"); 2 | const _exec = promisify(require("child_process").exec); 3 | const fs = require("fs/promises"); 4 | const path = require("path"); 5 | 6 | async function main() { 7 | if (process.platform !== "linux") handleErr("This script only works on Linux."); 8 | const input = process.argv[2]; 9 | const shouldResize = process.argv[3] === "true"; 10 | const resolution = process.argv[4] || "72"; 11 | 12 | await checkCommandExists("zip", "unzip"); 13 | if (shouldResize) await checkCommandExists("convert"); 14 | 15 | if (!input || !input.endsWith(".zip")) { 16 | handleErr(`Usage: ${process.argv[0]} ${process.argv[1]} [EMOJI_ZIP] [--resize RESOLUTION]`); 17 | } 18 | 19 | if (!(await exists(input))) handleErr("No such file: " + input); 20 | 21 | const workdir = await exec("mktemp -d"); 22 | const extracted = path.join(workdir, "extracted"); 23 | await fs.mkdir(extracted); 24 | await exec(`unzip -q ${input} -d ${extracted}`); 25 | 26 | const output = path.join(workdir, "output"); 27 | await fs.mkdir(output); 28 | 29 | const data = require("./emojis.json"); 30 | 31 | async function processEmoji({ surrogates, diversityChildren, parent }) { 32 | await Promise.all(diversityChildren?.map(c => processEmoji({ ...c, parent: surrogates })) ?? []); 33 | const codepoints = Array.from(surrogates).map(c => c.codePointAt(0).toString(16).padStart(4, "0")); 34 | let name = codepoints.join("-") + ".png"; 35 | if (!(await exists(extracted, name))) { 36 | name = codepoints.filter(c => c !== "fe0f").join("-") + ".png"; 37 | if (!(await exists(extracted, name))) { 38 | console.warn("Didn't find emoji " + surrogates); 39 | return; 40 | } 41 | } 42 | const outputName = `${parent ? `${parent}_` : ""}${surrogates}.png`; 43 | const inputFile = path.join(extracted, name); 44 | const outputFile = path.join(output, outputName); 45 | if (shouldResize) await exec(`convert ${inputFile} -resize ${resolution}x${resolution} ${outputFile}`); 46 | else await fs.rename(inputFile, outputFile); 47 | } 48 | 49 | await Promise.all(Object.values(data).flat().map(processEmoji)); 50 | const interface = require("readline").createInterface({ 51 | input: process.stdin, 52 | output: process.stdout, 53 | }); 54 | 55 | console.log("\nDONE! Please paste the license below then CTRL+C:\n"); 56 | const lines = []; 57 | interface.on("line", line => lines.push(line)); 58 | interface.on("close", async () => { 59 | await fs.writeFile(path.join(output, "LICENSE.txt"), lines.join("\n")); 60 | await exec(`cd ${output} && zip -D -q -r ${path.join(__dirname, "output.zip")} *`); 61 | }); 62 | interface.prompt(); 63 | } 64 | 65 | async function checkCommandExists(...commands) { 66 | return Promise.all( 67 | commands.map(async cmd => { 68 | await _exec(`which ${cmd}`).catch(() => handleErr(`${cmd} not found. Please install it and try again`)); 69 | }) 70 | ); 71 | } 72 | 73 | function exists(directory, file = "") { 74 | file = path.join(directory, file); 75 | return fs 76 | .access(file) 77 | .then(() => true) 78 | .catch(() => false); 79 | } 80 | 81 | function handleErr(err) { 82 | console.error(err); 83 | process.exit(1); 84 | } 85 | 86 | function exec(command) { 87 | console.info("Running " + command); 88 | return _exec(command) 89 | .then(({ stdout, stderr }) => { 90 | if (stderr) handleErr(stderr); 91 | if (stdout) console.log(stdout); 92 | return stdout.trim(); 93 | }) 94 | .catch(handleErr); 95 | } 96 | 97 | main(); 98 | -------------------------------------------------------------------------------- /EmojiReplacer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /EmojiReplacer/src/main/kotlin/dev/vendicated/aliucordplugins/emojireplacer/EmojiReplacer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.emojireplacer 12 | 13 | import android.content.Context 14 | import com.aliucord.Logger 15 | import com.aliucord.annotations.AliucordPlugin 16 | import com.aliucord.entities.Plugin 17 | import com.aliucord.patcher.Hook 18 | import com.aliucord.settings.delegate 19 | import com.discord.models.domain.emoji.ModelEmojiUnicode 20 | import com.discord.stores.StoreStream 21 | 22 | val logger = Logger("EmojiReplacer") 23 | @AliucordPlugin 24 | class EmojiReplacer : Plugin() { 25 | private val activePack : String by settings.delegate("none") 26 | 27 | init { 28 | settingsTab = SettingsTab(Settings::class.java) 29 | } 30 | 31 | override fun start(ctx: Context) { 32 | patcher.patch(ModelEmojiUnicode::class.java.getDeclaredMethod("getImageUri", String::class.java, Context::class.java), Hook { param -> 33 | val emoji = StoreStream.getEmojis().unicodeEmojiSurrogateMap[param.args[0]] ?: return@Hook 34 | logger.debug(emoji.toString()) 35 | }) 36 | } 37 | 38 | private val emojis = HashMap() 39 | 40 | override fun stop(context: Context) { 41 | patcher.unpatchAll() 42 | commands.unregisterAll() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /EmojiReplacer/src/main/kotlin/dev/vendicated/aliucordplugins/emojireplacer/Util.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.emojireplacer 12 | 13 | import android.content.Context 14 | import java.io.File 15 | import java.io.FileNotFoundException 16 | import java.util.zip.ZipFile 17 | 18 | fun File.extract(outputDir: File) { 19 | if (!outputDir.exists() && !outputDir.mkdirs()) throw RuntimeException("Failed to create directory $outputDir") 20 | 21 | ZipFile(this).use { zip -> 22 | zip.entries().asSequence().forEach { 23 | val isDir = it.isDirectory 24 | val file = File(outputDir, it.name) 25 | val dir = if (isDir) file else file.parentFile 26 | if (!dir.isDirectory && !dir.mkdirs()) throw RuntimeException("Failed to create directory $outputDir") 27 | if (!isDir) { 28 | zip.getInputStream(it).use { zis -> zis.copyTo(file.outputStream()) } 29 | } 30 | } 31 | } 32 | } 33 | 34 | val Context.emojiDir 35 | get() = File(filesDir, "emoji-replacer") 36 | 37 | val Context.installedPacks: Array 38 | get() = emojiDir.run { 39 | if (!exists() && !mkdirs()) throw FileNotFoundException() 40 | listFiles()!! 41 | } 42 | 43 | 44 | val File.folderSize: Long 45 | get() { 46 | var size = 0L 47 | for (file in listFiles()!!) { 48 | size += if (file.isDirectory) file.folderSize 49 | else file.length() 50 | } 51 | return size 52 | } 53 | -------------------------------------------------------------------------------- /EmojiUtility/README.md: -------------------------------------------------------------------------------- 1 | # EmojiUtility - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/EmojiUtility.zip?raw=true) 2 | 3 | Adds lots of emoji utilities. All features are enabled by default but toggleable in the plugin settings. 4 | 5 | Emojis are saved to Download/Emojis/ 6 | 7 | 8 | ## Features 9 | 10 | - New buttons in emoji widget (info sheet that opens if you click on one): 11 | - Copy emoji url 12 | - Download emoji 13 | - Clone emoji to other servers 14 | - New Commands: 15 | - download: Save all specified emotes, all emotes from the current server or all emotes of all servers you're on 16 | - TODO: wordreact: React with letter emojis matching the specified word / sentence 17 | - Hide unusable emojis 18 | - Keep reaction modal open after reacting if reaction button was long pressed 19 | 20 | ## Screenshots 21 | 22 | ### New Buttons in emoji widget 23 | 24 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/870753374679740457/Screenshot_20210730-2138093.jpg) 25 | 26 | ### Clone Modal 27 | 28 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/870753374964944916/Screenshot_20210730-2138202.jpg) 29 | 30 | ### Commands 31 | 32 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/870753911886188544/Screenshot_20210730-214505_Aliucord.png) 33 | 34 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/870753375245975644/Screenshot_20210730-2140022.jpg) 35 | -------------------------------------------------------------------------------- /EmojiUtility/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.2.2" 2 | description = "Adds lots of utility for emojis" 3 | 4 | aliucord.changelog.set( 5 | """ 6 | # 1.2.2 7 | * Fix opening emoji sheet from reaction sheet for built-in emojis 8 | 9 | # 1.2.1 10 | * "Hide unusable emotes" now also hides unavailable emotes (due to Boost running out or similar) 11 | 12 | # 1.2.0 13 | * You can now view or download reaction emojis by opening the Reaction Sheet and long pressing the desired emoji 14 | """.trimIndent() 15 | ) 16 | -------------------------------------------------------------------------------- /EmojiUtility/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /EmojiUtility/src/main/java/dev/vendicated/aliucordplugs/emojiutility/EmojiUtility.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.emojiutility; 12 | 13 | import android.content.Context; 14 | 15 | import com.aliucord.Logger; 16 | import com.aliucord.annotations.AliucordPlugin; 17 | import com.aliucord.api.SettingsAPI; 18 | import com.aliucord.entities.Plugin; 19 | 20 | @AliucordPlugin 21 | public class EmojiUtility extends Plugin { 22 | public static boolean useHumanNames; 23 | public static final Logger logger = new Logger("EmojiUtility"); 24 | public static SettingsAPI mSettings; 25 | 26 | public EmojiUtility() { 27 | settingsTab = new SettingsTab(Settings.class, SettingsTab.Type.BOTTOM_SHEET).withArgs(settings, patcher, commands); 28 | } 29 | 30 | @Override 31 | public void start(Context ctx) throws Throwable { 32 | mSettings = settings; 33 | useHumanNames = settings.getBool("humanNames", true); 34 | Patches.init(settings, patcher); 35 | Commands.registerAll(commands); 36 | } 37 | 38 | @Override 39 | public void stop(Context context) { 40 | patcher.unpatchAll(); 41 | commands.unregisterAll(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /EmojiUtility/src/main/java/dev/vendicated/aliucordplugs/emojiutility/Settings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.emojiutility; 12 | 13 | import android.content.Context; 14 | import android.content.DialogInterface; 15 | import android.os.Bundle; 16 | import android.os.Environment; 17 | import android.text.Editable; 18 | import android.text.TextWatcher; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.widget.LinearLayout; 22 | 23 | import androidx.annotation.NonNull; 24 | 25 | import com.aliucord.Utils; 26 | import com.aliucord.api.*; 27 | import com.aliucord.utils.DimenUtils; 28 | import com.aliucord.views.TextInput; 29 | import com.aliucord.widgets.BottomSheet; 30 | import com.discord.views.CheckedSetting; 31 | 32 | import java.io.File; 33 | 34 | import kotlin.jvm.functions.Function1; 35 | 36 | public class Settings extends BottomSheet { 37 | private final SettingsAPI settings; 38 | private final PatcherAPI patcher; 39 | private final CommandsAPI commands; 40 | 41 | public Settings(SettingsAPI settings, PatcherAPI patcher, CommandsAPI commands) { 42 | this.settings = settings; 43 | this.patcher = patcher; 44 | this.commands = commands; 45 | } 46 | 47 | @Override 48 | public void onViewCreated(View view, Bundle bundle) { 49 | super.onViewCreated(view, bundle); 50 | 51 | var ctx = requireContext(); 52 | addInput(ctx, "Maximum Download Thread Count", "threadCount", "10", true, s -> { 53 | try { 54 | int i = Integer.parseInt(s); 55 | return i > 0 && i <= 100; 56 | } catch (NumberFormatException ignored) {} 57 | return false; 58 | }); 59 | addInput(ctx, "Download Dir", "downloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/Emojis", false, s -> { 60 | var file = new File(s); 61 | return file.isDirectory() && file.canWrite(); 62 | }); 63 | addCheckedSetting(ctx, "Human readable file names", "Name downloads NAME.png instead of ID.png. Downloading twice will lead to duplicate downloads", "humanNames"); 64 | addCheckedSetting(ctx, "Add extra buttons to emoji widget", "copy url, save & clone", "extraButtons"); 65 | addCheckedSetting(ctx, "Keep reaction emoji picker open", "long press the react button","keepOpen"); 66 | addCheckedSetting(ctx, "Hide unusable emojis", null, "hideUnusable"); 67 | addCheckedSetting(ctx, "Register download commands", null, "registerCommands"); 68 | } 69 | 70 | @Override 71 | public void onCancel(@NonNull DialogInterface dialog) { 72 | super.onCancel(dialog); 73 | 74 | if (settings.getBool("registerCommands", true)) 75 | Commands.registerAll(commands); 76 | else 77 | commands.unregisterAll(); 78 | 79 | EmojiUtility.useHumanNames = settings.getBool("humanNames", true); 80 | try { 81 | Patches.init(settings, patcher); 82 | } catch (Throwable th) { 83 | EmojiUtility.logger.error("Something went wrong while initialising the patches :(", th); 84 | } 85 | } 86 | 87 | private void addCheckedSetting(Context ctx, String title, String subtitle, String setting) { 88 | var cs = Utils.createCheckedSetting(ctx, CheckedSetting.ViewType.SWITCH, title, subtitle); 89 | cs.setChecked(settings.getBool(setting, true)); 90 | cs.setOnCheckedListener(checked -> settings.setBool(setting, checked)); 91 | addView(cs); 92 | } 93 | 94 | private void addInput(Context ctx, String title, String setting, String def, boolean isInt, Function1 validate) { 95 | var input = new TextInput(ctx); 96 | var params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 97 | params.setMargins(DimenUtils.getDefaultPadding(), DimenUtils.getDefaultPadding() / 2, DimenUtils.getDefaultPadding(), DimenUtils.getDefaultPadding() / 2); 98 | input.setLayoutParams(params); 99 | input.setHint(title); 100 | var editText = input.getEditText(); 101 | editText.setText(isInt ? Integer.toString(settings.getInt(setting, Integer.parseInt(def))) : settings.getString(setting, def)); 102 | editText.addTextChangedListener(new TextWatcher() { 103 | @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 104 | @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } 105 | @Override public void afterTextChanged(Editable e) { 106 | var s = e.toString(); 107 | if (!validate.invoke(s)) input.setHint(title + " [INVALID]"); 108 | else { 109 | if (isInt) { 110 | settings.setInt(setting, Integer.parseInt(s)); 111 | } else { 112 | settings.setString(setting, s); 113 | } 114 | input.setHint(title); 115 | } 116 | } 117 | }); 118 | addView(input); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /EmojiUtility/src/main/java/dev/vendicated/aliucordplugs/emojiutility/clonemodal/Adapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.emojiutility.clonemodal; 12 | 13 | import android.view.*; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.recyclerview.widget.RecyclerView; 17 | 18 | import com.aliucord.Utils; 19 | import com.discord.models.guild.Guild; 20 | import com.discord.utilities.extensions.SimpleDraweeViewExtensionsKt; 21 | import com.lytefast.flexinput.R; 22 | 23 | import java.util.List; 24 | 25 | public class Adapter extends RecyclerView.Adapter { 26 | private static final int layoutId = Utils.getResId("widget_user_profile_adapter_item_server", "layout"); 27 | 28 | private final List guilds; 29 | private final Modal modal; 30 | 31 | public Adapter(Modal modal, List guilds) { 32 | this.guilds = guilds; 33 | this.modal = modal; 34 | } 35 | 36 | @Override 37 | public int getItemCount() { 38 | return guilds.size(); 39 | } 40 | 41 | @NonNull 42 | @Override 43 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 44 | var layout = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); 45 | return new ViewHolder(this, (ViewGroup) layout); 46 | } 47 | 48 | @Override 49 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 50 | var guild = guilds.get(position); 51 | 52 | if (guild.getIcon() != null) { 53 | var color = holder.icon.getContext().getColor(R.c.primary_dark_600); 54 | SimpleDraweeViewExtensionsKt.setGuildIcon(holder.icon, false, guild, 0, null, color, null, null, true, null); 55 | holder.iconText.setVisibility(View.GONE); 56 | } else { 57 | holder.icon.setVisibility(View.GONE); 58 | holder.iconText.setText(guild.getShortName()); 59 | } 60 | 61 | holder.name.setText(guild.getName()); 62 | } 63 | 64 | public void onClick(int position) { 65 | var guild = guilds.get(position); 66 | modal.clone(guild); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /EmojiUtility/src/main/java/dev/vendicated/aliucordplugs/emojiutility/clonemodal/Modal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.emojiutility.clonemodal; 12 | 13 | import android.util.Base64; 14 | import android.view.View; 15 | 16 | import androidx.recyclerview.widget.LinearLayoutManager; 17 | import androidx.recyclerview.widget.RecyclerView; 18 | 19 | import com.aliucord.*; 20 | import com.aliucord.fragments.SettingsPage; 21 | import com.aliucord.utils.RxUtils; 22 | import com.aliucord.wrappers.GuildEmojiWrapper; 23 | import com.discord.api.permission.Permission; 24 | import com.discord.models.guild.Guild; 25 | import com.discord.restapi.RestAPIParams; 26 | import com.discord.stores.StoreGuilds; 27 | import com.discord.stores.StoreStream; 28 | import com.discord.utilities.permissions.PermissionUtils; 29 | import com.discord.utilities.rest.RestAPI; 30 | 31 | import java.io.ByteArrayOutputStream; 32 | import java.io.IOException; 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | 36 | import dev.vendicated.aliucordplugs.emojiutility.EmojiUtility; 37 | 38 | public class Modal extends SettingsPage { 39 | private static final Map emojiLimits = new HashMap<>(); 40 | static { 41 | emojiLimits.put(0, 50); 42 | emojiLimits.put(1, 100); 43 | emojiLimits.put(2, 150); 44 | emojiLimits.put(3, 250); 45 | } 46 | 47 | private final Map guildPerms = StoreStream.getPermissions().getGuildPermissions(); 48 | private final StoreGuilds guildStore = StoreStream.getGuilds(); 49 | 50 | private final String name; 51 | private final String url; 52 | private final long id; 53 | private final boolean isAnimated; 54 | 55 | public Modal(String url, String name, long id, boolean isAnimated) { 56 | this.url = url; 57 | this.name = name; 58 | this.id = id; 59 | this.isAnimated = isAnimated; 60 | } 61 | 62 | @Override 63 | public void onViewBound(View view) { 64 | super.onViewBound(view); 65 | 66 | setActionBarTitle("Clone Emoji"); 67 | setActionBarSubtitle(name); 68 | 69 | var ctx = view.getContext(); 70 | 71 | setPadding(0); 72 | 73 | var recycler = new RecyclerView(ctx); 74 | recycler.setLayoutManager(new LinearLayoutManager(ctx, RecyclerView.VERTICAL, false)); 75 | var adapter = new Adapter(this, CollectionUtils.filter(guildStore.getGuilds().values(), this::isCandidate)); 76 | recycler.setAdapter(adapter); 77 | 78 | addView(recycler); 79 | } 80 | 81 | private boolean isCandidate(Guild guild) { 82 | var perms = guildPerms.get(guild.getId()); 83 | if (!PermissionUtils.can(Permission.MANAGE_EMOJIS_AND_STICKERS, perms)) return false; 84 | int usedSlots = 0; 85 | for (var emoji : guild.getEmojis()) { 86 | if (GuildEmojiWrapper.getId(emoji) == id) return false; 87 | if (GuildEmojiWrapper.isAnimated(emoji) == isAnimated) usedSlots++; 88 | } 89 | var slots = emojiLimits.get(guild.getPremiumTier()); 90 | return slots != null && usedSlots < slots; 91 | } 92 | 93 | private String imageToDataUri() { 94 | try { 95 | var res = new Http.Request(url).execute(); 96 | try (var baos = new ByteArrayOutputStream()) { 97 | res.pipe(baos); 98 | var b64 = Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT); 99 | return String.format("data:image/%s;base64,%s", isAnimated ? "gif" : "png", b64); 100 | } 101 | } catch (IOException ex) { EmojiUtility.logger.error(ex); return null; } 102 | } 103 | 104 | public void clone(Guild guild) { 105 | Utils.threadPool.execute(() -> { 106 | var api = RestAPI.getApi(); 107 | var uri = imageToDataUri(); 108 | if (uri == null) { 109 | EmojiUtility.logger.errorToast("Something went wrong while preparing the image"); 110 | return; 111 | } 112 | var obs = api.postGuildEmoji(guild.getId(), new RestAPIParams.PostGuildEmoji(name, uri)); 113 | var res = RxUtils.await(obs); 114 | if (res.getSecond() == null) 115 | EmojiUtility.logger.infoToast(String.format("Successfully cloned %s to %s", name, guild.getName())); 116 | else 117 | EmojiUtility.logger.errorToast("Something went wrong while cloning this emoji", res.getSecond()); 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /EmojiUtility/src/main/java/dev/vendicated/aliucordplugs/emojiutility/clonemodal/ViewHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.emojiutility.clonemodal; 12 | 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.TextView; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.recyclerview.widget.RecyclerView; 19 | 20 | import com.aliucord.Utils; 21 | import com.facebook.drawee.view.SimpleDraweeView; 22 | 23 | public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 24 | private static final int iconId = Utils.getResId("user_profile_adapter_item_server_image", "id"); 25 | private static final int iconTextId = Utils.getResId("user_profile_adapter_item_server_text", "id"); 26 | private static final int serverNameId = Utils.getResId("user_profile_adapter_item_server_name", "id"); 27 | private static final int identityBarrierId = Utils.getResId("guild_member_identity_barrier", "id"); 28 | private static final int serverAvatarId = Utils.getResId("guild_member_avatar", "id"); 29 | private static final int serverNickId = Utils.getResId("user_profile_adapter_item_user_display_name", "id"); 30 | 31 | private final Adapter adapter; 32 | 33 | public final SimpleDraweeView icon; 34 | public final TextView iconText; 35 | public final TextView name; 36 | 37 | public ViewHolder(Adapter adapter, @NonNull ViewGroup layout) { 38 | super(layout); 39 | this.adapter = adapter; 40 | 41 | icon = layout.findViewById(iconId); 42 | iconText = layout.findViewById(iconTextId); 43 | name = layout.findViewById(serverNameId); 44 | 45 | // Hide server profile stuff 46 | layout.findViewById(identityBarrierId).setVisibility(View.GONE); 47 | layout.findViewById(serverAvatarId).setVisibility(View.GONE); 48 | layout.findViewById(serverNickId).setVisibility(View.GONE); 49 | 50 | layout.setOnClickListener(this); 51 | } 52 | 53 | @Override public void onClick(View view) { 54 | adapter.onClick(getAdapterPosition()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /FixEmotes/README.md: -------------------------------------------------------------------------------- 1 | # FixEmotes - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/FixEmotes.zip?raw=true) 2 | 3 | Fixes some emotes being rendered unusable if you have two emotes with the same name but different casing 4 | -------------------------------------------------------------------------------- /FixEmotes/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.1" 2 | description = "Fixes some emotes being unusable if you have two emotes with the same name but different casing" 3 | 4 | aliucord.changelog.set(""" 5 | # 1.0.1 6 | - Fix for Discord 120.11 7 | - Should now 100% accurately replace emotes with the correct emoji (and not a random emoji with that same name) 8 | """.trimIndent()) 9 | -------------------------------------------------------------------------------- /FixEmotes/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /FixEmotes/src/main/kotlin/dev/vendicated/aliucordplugins/fixemotes/FixEmotes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.fixemotes 12 | 13 | import android.content.Context 14 | import com.aliucord.annotations.AliucordPlugin 15 | import com.aliucord.entities.Plugin 16 | import com.aliucord.patcher.PreHook 17 | import com.aliucord.patcher.after 18 | import com.discord.restapi.RestAPIParams 19 | import com.discord.widgets.chat.input.emoji.EmojiPickerViewModel 20 | 21 | @AliucordPlugin 22 | class FixEmotes : Plugin() { 23 | private val brokenEmoteRegex = "(?("handleStoreState", EmojiPickerViewModel.StoreState::class.java) { param -> 28 | (param.args[0] as? EmojiPickerViewModel.StoreState.Emoji)?.let { 29 | storeState = it 30 | } 31 | } 32 | 33 | val ctor = RestAPIParams.Message::class.java.declaredConstructors.firstOrNull { 34 | !it.isSynthetic 35 | } ?: throw IllegalStateException("Didn't find RestAPIParams.Message ctor") 36 | 37 | patcher.patch( 38 | ctor, 39 | PreHook 40 | { param -> 41 | param.args[0] = param.args[0].toString().replace(brokenEmoteRegex) { 42 | storeState?.emojiSet?.customEmojis?.forEach { (_, g) -> 43 | g.forEach { e -> 44 | if (e.getCommand("" /* not used, only there to implement method lmao */) == it.value) { 45 | return@replace e.messageContentReplacement 46 | } 47 | } 48 | } 49 | it.value 50 | } 51 | }) 52 | } 53 | 54 | override fun stop(context: Context) { 55 | patcher.unpatchAll() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /GatewayLog/README.md: -------------------------------------------------------------------------------- 1 | # GatewayLog - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/GatewayLog.zip?raw=true) 2 | 3 | Logs all gateway messages to log files in Aliucord folder. 4 | 5 | Only useful for developers, also stores your token in plain text world readable file, so don't install unless you know what you're doing. 6 | -------------------------------------------------------------------------------- /GatewayLog/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.0" 2 | description = "Logs all gateway messages to log files in the Aliucord folder for debugging purposes" 3 | 4 | aliucord.excludeFromUpdaterJson.set(true) 5 | -------------------------------------------------------------------------------- /GatewayLog/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GatewayLog/src/main/kotlin/dev/vendicated/aliucordplugins/gatewaylog/GatewayLog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.gatewaylog 12 | 13 | import android.content.Context 14 | import com.aliucord.Constants 15 | import com.aliucord.annotations.AliucordPlugin 16 | import com.aliucord.entities.Plugin 17 | import com.aliucord.utils.ReflectUtils 18 | import com.discord.gateway.GatewaySocketLogger 19 | import com.discord.stores.StoreStream 20 | import com.discord.utilities.logging.AppGatewaySocketLogger 21 | import java.io.File 22 | import java.io.FileOutputStream 23 | 24 | @AliucordPlugin 25 | class GatewayLog : Plugin() { 26 | private lateinit var inbound: FileOutputStream 27 | private lateinit var outbound: FileOutputStream 28 | 29 | override fun start(ctx: Context) { 30 | val base = File(Constants.BASE_PATH) 31 | inbound = FileOutputStream(File(base, "gateway_inbound.txt")) 32 | outbound = FileOutputStream(File(base, "gateway_outbound.txt")) 33 | 34 | ReflectUtils.setField(ReflectUtils.getField(StoreStream.getGatewaySocket(), "socket")!!, "gatewaySocketLogger", object : GatewaySocketLogger { 35 | override fun getLogLevel(): GatewaySocketLogger.LogLevel { 36 | return GatewaySocketLogger.LogLevel.VERBOSE 37 | } 38 | override fun logInboundMessage(str: String) { 39 | inbound.write((str + "\n").toByteArray()) 40 | } 41 | override fun logMessageInflateFailed(th: Throwable) { 42 | logger.error("Error inflating gateway message (Exception comes from Discord)", th) 43 | } 44 | override fun logOutboundMessage(str: String) { 45 | outbound.write((str + "\n").toByteArray()) 46 | } 47 | }) 48 | } 49 | 50 | override fun stop(context: Context) { 51 | ReflectUtils.setField(ReflectUtils.getField(StoreStream.getGatewaySocket(), "socket")!!, "gatewaySocketLogger", (AppGatewaySocketLogger.Companion).instance) 52 | inbound.close() 53 | outbound.close() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Hastebin/README.md: -------------------------------------------------------------------------------- 1 | # Hastebin - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/Hastebin.zip?raw=true) 2 | 3 | Create pastes on your favourite Hastebin Mirror 4 | 5 | You can customise the hastebin mirror in the plugin settings 6 | -------------------------------------------------------------------------------- /Hastebin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.4" 2 | description = "Create pastes on hastebin" 3 | -------------------------------------------------------------------------------- /Hastebin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Hastebin/src/main/java/dev/vendicated/aliucordplugs/hastebin/HasteResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.hastebin; 12 | 13 | public class HasteResponse { 14 | public String key; 15 | } 16 | -------------------------------------------------------------------------------- /Hastebin/src/main/java/dev/vendicated/aliucordplugs/hastebin/Hastebin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.hastebin; 12 | 13 | import android.content.Context; 14 | 15 | import com.aliucord.Http; 16 | import com.aliucord.Utils; 17 | import com.aliucord.annotations.AliucordPlugin; 18 | import com.aliucord.api.CommandsAPI; 19 | import com.aliucord.entities.Plugin; 20 | import com.discord.api.commands.ApplicationCommandType; 21 | 22 | import java.io.IOException; 23 | import java.util.Arrays; 24 | 25 | @AliucordPlugin 26 | public class Hastebin extends Plugin { 27 | public Hastebin() { 28 | super(); 29 | settingsTab = new SettingsTab(PluginSettings.class).withArgs(settings); 30 | } 31 | 32 | @Override 33 | public void start(Context context) { 34 | var arguments = Arrays.asList( 35 | Utils.createCommandOption(ApplicationCommandType.STRING, "text", "The text to upload", null, true), 36 | Utils.createCommandOption(ApplicationCommandType.BOOLEAN, "send", "Whether the message should be visible for everyone") 37 | ); 38 | 39 | commands.registerCommand( 40 | "haste", 41 | "Create pastes on hastebin", 42 | arguments, 43 | ctx -> { 44 | var text = ctx.getRequiredString("text"); 45 | var send = ctx.getBoolOrDefault("send", false); 46 | 47 | String result; 48 | String mirror = settings.getString("mirror", "https://haste.powercord.dev") + "/"; 49 | 50 | try { 51 | HasteResponse res = Http.simpleJsonPost(mirror + "documents", text, HasteResponse.class); 52 | result = mirror + res.key; 53 | } catch (IOException ex) { 54 | send = false; 55 | result = String.format("Error while uploading to hastebin:\n```\n%s```Consider changing hastebin mirror if this keeps happening", ex.getMessage()); 56 | } 57 | 58 | return new CommandsAPI.CommandResult(result, null, send); 59 | } 60 | ); 61 | } 62 | 63 | @Override 64 | public void stop(Context context) { 65 | commands.unregisterAll(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Hastebin/src/main/java/dev/vendicated/aliucordplugs/hastebin/PluginSettings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugs.hastebin; 12 | 13 | import android.annotation.SuppressLint; 14 | import android.text.Editable; 15 | import android.text.TextWatcher; 16 | import android.view.View; 17 | 18 | import com.aliucord.Utils; 19 | import com.aliucord.api.SettingsAPI; 20 | import com.aliucord.fragments.SettingsPage; 21 | import com.aliucord.views.Button; 22 | import com.aliucord.views.TextInput; 23 | 24 | import java.util.regex.Pattern; 25 | 26 | 27 | @SuppressLint("SetTextI18n") 28 | public final class PluginSettings extends SettingsPage { 29 | private static final Pattern re = Pattern.compile("https?://(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}"); 30 | 31 | private final SettingsAPI settings; 32 | 33 | public PluginSettings(SettingsAPI settings) { 34 | this.settings = settings; 35 | } 36 | 37 | @Override 38 | public void onViewBound(View view) { 39 | super.onViewBound(view); 40 | 41 | setActionBarTitle("Hastebin"); 42 | 43 | var ctx = requireContext(); 44 | 45 | var input = new TextInput(ctx); 46 | input.setHint("Hastebin Mirror"); 47 | 48 | var editText = input.getEditText(); 49 | 50 | var button = new Button(ctx); 51 | button.setText("Totally wrong text oh no i better update this"); 52 | button.setOnClickListener(v -> { 53 | settings.setString("mirror", editText.getText().toString().replaceFirst("/+$", "")); 54 | Utils.showToast("Saved!"); 55 | close(); 56 | }); 57 | 58 | editText.setMaxLines(1); 59 | editText.setText(settings.getString("mirror", "https://haste.powercord.dev")); 60 | editText.addTextChangedListener(new TextWatcher() { 61 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 62 | public void onTextChanged(CharSequence s, int start, int before, int count) { } 63 | public void afterTextChanged(Editable s) { 64 | if (!isValid(s.toString())) { 65 | button.setAlpha(0.5f); 66 | button.setClickable(false); 67 | } else { 68 | button.setAlpha(1f); 69 | button.setClickable(true); 70 | } 71 | } 72 | }); 73 | 74 | addView(input); 75 | addView(button); 76 | } 77 | 78 | private boolean isValid(String s) { 79 | return re.matcher(s).matches(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /JsEval/README.md: -------------------------------------------------------------------------------- 1 | # Template - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/DeezNuts.zip?raw=true) 2 | 3 | This is the Template I generate all my plugins from. Not much to see here :) 4 | -------------------------------------------------------------------------------- /JsEval/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.0" 2 | description = "Punch 6pak for making me update 15 plugins" 3 | -------------------------------------------------------------------------------- /JsEval/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /JsEval/src/main/kotlin/dev/vendicated/aliucordplugins/jseval/CodeTextView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.jseval 12 | 13 | import android.annotation.SuppressLint 14 | import android.content.Context 15 | import android.widget.TextView 16 | import androidx.core.content.res.ResourcesCompat 17 | import com.aliucord.Constants 18 | import com.lytefast.flexinput.R 19 | 20 | @SuppressLint("AppCompatCustomView") 21 | class CodeTextView(ctx : Context) : TextView(ctx, null, 0, R.i.UiKit_TextView) { 22 | init { 23 | typeface = ResourcesCompat.getFont(ctx, Constants.Fonts.sourcecodepro_semibold) 24 | setTextIsSelectable(true) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /JsEval/src/main/kotlin/dev/vendicated/aliucordplugins/jseval/JsEval.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.jseval 12 | 13 | import android.content.Context 14 | import com.aliucord.annotations.AliucordPlugin 15 | import com.aliucord.entities.Plugin 16 | 17 | @AliucordPlugin 18 | class JsEval : Plugin() { 19 | override fun start(ctx: Context) { 20 | settingsTab = SettingsTab(TerminalPage::class.java) 21 | } 22 | 23 | override fun stop(context: Context) { 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /JsEval/src/main/kotlin/dev/vendicated/aliucordplugins/jseval/TerminalButton.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.jseval 12 | 13 | import android.annotation.SuppressLint 14 | import android.content.Context 15 | import android.graphics.Color 16 | import android.graphics.Paint 17 | import android.graphics.drawable.ShapeDrawable 18 | import android.graphics.drawable.shapes.RectShape 19 | import android.widget.LinearLayout 20 | import androidx.appcompat.widget.AppCompatImageButton 21 | import androidx.core.content.ContextCompat 22 | import com.aliucord.utils.DimenUtils.dp 23 | 24 | @SuppressLint("ViewConstructor") 25 | class TerminalButton(ctx: Context, drawableId: Int, onClick: OnClickListener) : AppCompatImageButton(ctx) { 26 | init { 27 | layoutParams = LinearLayout.LayoutParams(30.dp, 30.dp).apply { 28 | setMargins(6.dp, 0, 6.dp, 0) 29 | } 30 | setPadding(6.dp, 6.dp, 6.dp, 6.dp) 31 | 32 | background = ShapeDrawable().apply { 33 | shape = RectShape() 34 | paint.color = Color.GRAY 35 | paint.strokeWidth = 2f 36 | paint.style = Paint.Style.STROKE 37 | } 38 | 39 | ContextCompat.getDrawable(ctx, drawableId)!!.run { 40 | mutate() 41 | setTint(Color.WHITE) 42 | setImageDrawable(this) 43 | } 44 | setOnClickListener(onClick) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /JsEval/src/main/kotlin/dev/vendicated/aliucordplugins/jseval/TerminalHistoryAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.jseval 12 | 13 | import android.annotation.SuppressLint 14 | import android.graphics.Color 15 | import android.view.Gravity 16 | import android.view.ViewGroup 17 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 18 | import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 19 | import android.widget.LinearLayout 20 | import android.widget.TextView 21 | import androidx.core.content.ContextCompat 22 | import androidx.recyclerview.widget.RecyclerView 23 | import com.lytefast.flexinput.R 24 | 25 | abstract class HistoryItem(val viewType: Int) 26 | enum class ViewType { 27 | RESULT, EMPTY 28 | } 29 | data class ResultItem(val input: String, val output: String, val isError: Boolean) : HistoryItem(ViewType.RESULT.ordinal) 30 | data class EmptyItem(val text: String, val isError: Boolean) : HistoryItem(ViewType.EMPTY.ordinal) 31 | 32 | class TerminalHistoryAdapter : RecyclerView.Adapter() { 33 | private val data = ArrayList() 34 | 35 | override fun getItemViewType(position: Int) = data[position].viewType 36 | 37 | override fun getItemCount() = data.size 38 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 39 | when (viewType) { 40 | ViewType.RESULT.ordinal -> ResultEntry.create(parent) 41 | ViewType.EMPTY.ordinal -> EmptyEntry.create(parent) 42 | else -> throw NotImplementedError() 43 | } 44 | 45 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 46 | when (val item = data[position]) { 47 | is ResultItem -> { 48 | (holder as ResultEntry).bindView(item.input, item.output, item.isError) 49 | } 50 | is EmptyItem -> { 51 | (holder as EmptyEntry).bindView(item.text, item.isError) 52 | } 53 | } 54 | } 55 | 56 | fun append(item: HistoryItem): Int { 57 | data.add(item) 58 | notifyItemInserted(data.size - 1) 59 | return data.size - 1 60 | } 61 | 62 | @SuppressLint("NotifyDataSetChanged") 63 | fun clear() { 64 | data.clear() 65 | notifyDataSetChanged() 66 | } 67 | } 68 | 69 | class EmptyEntry(private val textView: TextView) : RecyclerView.ViewHolder(textView) { 70 | fun bindView(text: String, isError: Boolean) { 71 | textView.text = text 72 | textView.setTextColor(if (isError) Color.RED else Color.WHITE) 73 | } 74 | 75 | companion object { 76 | fun create(parent: ViewGroup): EmptyEntry { 77 | val textView = CodeTextView(parent.context).apply { 78 | layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) 79 | // textAlignment = View.TEXT_ALIGNMENT_CENTER 80 | } 81 | return EmptyEntry(textView) 82 | } 83 | } 84 | } 85 | 86 | class ResultEntry(layout: LinearLayout, private val promptView: TextView, private val resultView: TextView) : RecyclerView.ViewHolder(layout) { 87 | fun bindView(input: String, output: String, isError: Boolean) { 88 | promptView.text = input 89 | resultView.text = output 90 | resultView.setTextColor(if (isError) Color.RED else Color.WHITE) 91 | } 92 | 93 | companion object { 94 | fun create(parent: ViewGroup): ResultEntry { 95 | val ctx = parent.context 96 | val fullWidthParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) 97 | val layout = LinearLayout(ctx).apply { 98 | orientation = LinearLayout.VERTICAL 99 | layoutParams = fullWidthParams 100 | } 101 | val promptView = CodeTextView(ctx).apply { 102 | ContextCompat.getDrawable(ctx, R.e.ic_arrow_right_24dp)!!.run { 103 | mutate() 104 | setTint(Color.WHITE) 105 | setCompoundDrawablesRelativeWithIntrinsicBounds(this, null, null, null) 106 | } 107 | layoutParams = fullWidthParams 108 | this.gravity = Gravity.CENTER_VERTICAL 109 | } 110 | val resultView = CodeTextView(ctx).apply { 111 | layoutParams = fullWidthParams 112 | // Add drawable with same colour as background to properly align text 113 | ContextCompat.getDrawable(ctx, R.e.ic_arrow_right_24dp)!!.run { 114 | mutate() 115 | setTint(Color.BLACK) 116 | setCompoundDrawablesRelativeWithIntrinsicBounds(this, null, null, null) 117 | } 118 | } 119 | return ResultEntry(layout, promptView, resultView).also { 120 | layout.addView(promptView) 121 | layout.addView(resultView) 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /MessageLinkEmbeds/README.md: -------------------------------------------------------------------------------- 1 | # MessageLinkEmbeds - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/MessageLinkEmbeds.zip?raw=true) 2 | 3 | Embeds discord message links 4 | 5 | ![Screenshot](https://cdn.discordapp.com/attachments/852332951542956052/859502950963740743/Screenshot_20210629-203614_Aliucord.png) -------------------------------------------------------------------------------- /MessageLinkEmbeds/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.4.5" 2 | description = "Embeds message links" 3 | 4 | aliucord.changelog.set( 5 | """ 6 | # 1.4.4 & 1.4.5 7 | * Fix for new Discord versions 8 | 9 | # 1.4.3 10 | * Ignore message links 11 | 12 | # 1.4.2 13 | * Hopefully fix duplicate embeds 14 | 15 | # 1.4.0 16 | * Add support for files & embed fields 17 | 18 | # 1.3.0 19 | * Make message links jump directly in Discord instead of being launched like regular URLs 20 | 21 | """.trimIndent() 22 | ) 23 | -------------------------------------------------------------------------------- /MessageLinkEmbeds/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PlayableEmbeds/README.md: -------------------------------------------------------------------------------- 1 | # PlayableEmbeds - [Download](https://github.com/Vendicated/AliucordPlugins/blob/builds/PlayableEmbeds.zip?raw=true) 2 | 3 | Makes Youtube and Spotify embeds playable directly inside Discord. 4 | 5 | 6 | https://user-images.githubusercontent.com/45497981/142783818-deaea9b6-3a66-4627-a7b7-dc0a30c0ef49.mp4 7 | -------------------------------------------------------------------------------- /PlayableEmbeds/build.gradle.kts: -------------------------------------------------------------------------------- 1 | version = "1.0.2" 2 | description = "Makes Spotify and Youtube Embeds playable" 3 | 4 | aliucord.changelog.set(""" 5 | # 1.0.2 6 | * Youtube embed now supports timestamps 7 | """.trimIndent()) 8 | -------------------------------------------------------------------------------- /PlayableEmbeds/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PlayableEmbeds/src/main/kotlin/dev/vendicated/aliucordplugins/betterplatformembeds/PlayableEmbeds.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Ven's Aliucord Plugins 3 | * Copyright (C) 2021 Vendicated 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | */ 10 | 11 | package dev.vendicated.aliucordplugins.betterplatformembeds 12 | 13 | import android.annotation.SuppressLint 14 | import android.content.Context 15 | import android.graphics.Color 16 | import android.view.* 17 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 18 | import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 19 | import android.webkit.WebView 20 | import android.widget.LinearLayout 21 | import androidx.cardview.widget.CardView 22 | import androidx.constraintlayout.widget.ConstraintLayout 23 | import com.aliucord.Utils 24 | import com.aliucord.annotations.AliucordPlugin 25 | import com.aliucord.entities.Plugin 26 | import com.aliucord.patcher.* 27 | import com.aliucord.wrappers.embeds.MessageEmbedWrapper.Companion.rawProvider 28 | import com.aliucord.wrappers.embeds.MessageEmbedWrapper.Companion.url 29 | import com.aliucord.wrappers.embeds.ProviderWrapper.Companion.name 30 | import com.discord.widgets.chat.list.adapter.WidgetChatListAdapterItemEmbed 31 | import com.facebook.drawee.view.SimpleDraweeView 32 | import com.google.android.material.card.MaterialCardView 33 | import java.util.* 34 | 35 | class ScrollableWebView(ctx: Context) : WebView(ctx) { 36 | @SuppressLint("ClickableViewAccessibility") 37 | override fun onTouchEvent(event: MotionEvent?): Boolean { 38 | requestDisallowInterceptTouchEvent(true) 39 | return super.onTouchEvent(event) 40 | } 41 | } 42 | 43 | @AliucordPlugin 44 | class PlayableEmbeds : Plugin() { 45 | private val webviewMap = WeakHashMap() 46 | private val widgetId = View.generateViewId() 47 | private val spotifyUrlRe = Regex("https://open\\.spotify\\.com/(\\w+)/(\\w+)") 48 | private val youtubeUrlRe = 49 | Regex("(?:https?://)?(?:(?:www|m)\\.)?(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/"+ 50 | "(?:embed/|v/|watch\\?v=|watch\\?.+&v=|shorts/))((\\w|-){11})"+ 51 | "(?:(?:\\?|&)(?:star)?t=(\\d+))?(?:\\S+)?") 52 | 53 | override fun start(_context: Context) { 54 | patcher.after("configureUI", WidgetChatListAdapterItemEmbed.Model::class.java) { 55 | val model = it.args[0] as WidgetChatListAdapterItemEmbed.Model 56 | val embed = model.embedEntry.embed 57 | val holder = it.thisObject as WidgetChatListAdapterItemEmbed 58 | val layout = holder.itemView as ConstraintLayout 59 | 60 | layout.findViewById(widgetId)?.let { v -> 61 | if (webviewMap[v] == embed.url) return@after 62 | (v.parent as ViewGroup).removeView(v) 63 | } 64 | val url = embed.url ?: return@after; 65 | when (embed.rawProvider?.name) { 66 | "YouTube" -> addYoutubeEmbed(layout, url) 67 | "Spotify" -> addSpotifyEmbed(layout, url) 68 | } 69 | } 70 | } 71 | 72 | private fun addYoutubeEmbed(layout: ViewGroup, url: String) { 73 | val ctx = layout.context 74 | 75 | val (_, videoId, _, timestamp) = youtubeUrlRe.find(url, 0).groupValues 76 | 77 | val cardView = layout.findViewById(Utils.getResId("embed_image_container", "id")) 78 | val chatListItemEmbedImage = cardView.findViewById(Utils.getResId("chat_list_item_embed_image", "id")) 79 | val playButton = cardView.findViewById(Utils.getResId("chat_list_item_embed_image_icons", "id")) 80 | playButton.visibility = View.GONE 81 | chatListItemEmbedImage.visibility = View.GONE 82 | 83 | val webView = ScrollableWebView(ctx).apply { 84 | id = widgetId 85 | setBackgroundColor(Color.TRANSPARENT) 86 | // val maxImgWidth = EmbedResourceUtils.INSTANCE.computeMaximumImageWidthPx(ctx) 87 | // val (width, height) = EmbedResourceUtils.INSTANCE.calculateScaledSize(1280, 720, maxImgWidth, maxImgWidth, ctx.resources, maxImgWidth / 2) 88 | layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) 89 | @SuppressLint("SetJavaScriptEnabled") 90 | settings.javaScriptEnabled = true 91 | 92 | cardView.addView(this) 93 | } 94 | webviewMap[webView] = url 95 | 96 | webView.run { 97 | loadData( 98 | """ 99 | 100 | 101 | 120 | 121 | 122 |
123 |