├── LICENSE ├── README.md ├── index.js ├── manifest.json └── style.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Juby210 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # message-link-embed 2 | Powercord plugin. Make message links show an embed like invites or store links. 3 | 4 | ![preview](https://i.imgur.com/0B1ts0P.png) 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Plugin } = require('powercord/entities') 2 | const { findInReactTree } = require('powercord/util') 3 | const { getModule, getModuleByDisplayName, http: { get }, constants: { Endpoints }, React, FluxDispatcher } = require('powercord/webpack') 4 | const { inject, uninject } = require('powercord/injector') 5 | 6 | const cache = {}, suppressed = [] 7 | let lastFetch = 0 8 | 9 | const { parse } = getModule(['parse', 'parseTopic'], false) 10 | const { getChannel } = getModule(['getChannel', 'getDMFromUserId'], false) 11 | const { getMessage } = getModule(['getMessages'], false) 12 | const { getUserAvatarURL } = getModule(['getUserAvatarURL'], false) 13 | const User = getModule(m => m.prototype && m.prototype.tag, false) 14 | const Timestamp = getModule(m => m.prototype && m.prototype.toDate && m.prototype.month, false) 15 | 16 | const isMLEmbed = e => typeof e?.author?.name[1]?.props?.__mlembed !== 'undefined' 17 | const isVideo = attachment => !!attachment.video || /\.(?:mp4|mov|webm)$/.test(attachment.url) 18 | 19 | const re = /https?:\/\/([^\s]*\.)?discord(app)?\.com\/channels\/(\d{17,19}|@me)\/\d{17,19}\/\d{17,19}/ 20 | 21 | module.exports = class MessageLinksEmbed extends Plugin { 22 | async startPlugin() { 23 | this.loadStylesheet('style.css') 24 | 25 | const MessageContent = await getModule(m => m.type && m.type.displayName == 'MessageContent') 26 | inject('mlembed-message', MessageContent, 'type', ([{ message }], res) => { 27 | const children = res.props.children.find(c => Array.isArray(c)) 28 | if (suppressed.includes(message.id) || !children || (message.embeds[0] && isMLEmbed(message.embeds[0]))) return res 29 | this.processLinks( 30 | message, 31 | children.filter(c => c.type?.displayName == "MaskedLink" && re.test(c.props.href)).map(c => c.props.href) 32 | ) 33 | 34 | return res 35 | }) 36 | MessageContent.type.displayName = 'MessageContent' 37 | 38 | const _this = this 39 | const { jumpToMessage } = await getModule(['jumpToMessage']) 40 | const Attachment = await getModuleByDisplayName('Attachment') 41 | 42 | const Embed = await getModuleByDisplayName('Embed') 43 | inject('mlembed', Embed.prototype, 'render', function (args) { 44 | if (!this.props.embed || !isMLEmbed(this.props.embed)) return args 45 | 46 | const msg = this.props.embed.author.name[1].props.__mlembed // hack 47 | const { renderAll } = this 48 | this.renderAll = function () { 49 | const res = renderAll.apply(this) 50 | 51 | const c = findInReactTree(res.author, c => c.href) 52 | if (c) { 53 | c.onClick = e => { 54 | e.preventDefault() 55 | const linkArray = c.href.split('/') 56 | jumpToMessage(linkArray[5], linkArray[6]) 57 | } 58 | } 59 | 60 | if (!this.props.embed._attachment) return res 61 | res.media = React.createElement(Attachment, { className: 'mle-attachment', ...this.props.embed._attachment }) 62 | 63 | return res 64 | } 65 | this.props.onSuppressEmbed = () => { 66 | suppressed.push(msg.embedmessage.id) 67 | const m = getMessage(msg.embedmessage.channel_id, msg.embedmessage.id) || { embeds: [] } 68 | _this.updateMessageEmbeds(msg.embedmessage.id, msg.embedmessage.channel_id, m.embeds.filter(e => !isMLEmbed(e))) 69 | } 70 | if (this.props.embed.__mlembed) return args // we changed embed props before, so we don't need to change it again 71 | 72 | let attachment 73 | if (msg.attachments[0] && msg.attachments[0].width) attachment = msg.attachments[0] 74 | if (msg.embeds[0]) { 75 | const embed = msg.embeds[0] 76 | if (embed.type == 'image') attachment = embed.image || embed.thumbnail 77 | else if (embed.type == 'video' || embed.type == 'gifv') attachment = embed 78 | } 79 | if (attachment) { 80 | if (!attachment.proxyURL) attachment.proxyURL = attachment.proxy_url 81 | if (isVideo(attachment)) { 82 | if (attachment.provider) this.props.embed = { 83 | ...this.props.embed, 84 | video: attachment.video, 85 | thumbnail: attachment.thumbnail, 86 | url: attachment.video.url 87 | }; else { 88 | if (attachment.video) attachment = attachment.video 89 | if (attachment.height > 400) attachment.height = 400 90 | if (attachment.width > 400) attachment.width = 400 91 | this.props.embed = { 92 | ...this.props.embed, 93 | video: attachment, 94 | thumbnail: { url: attachment.proxyURL + '?format=jpeg', height: attachment.height, width: attachment.width }, 95 | url: attachment.url 96 | } 97 | } 98 | } else { 99 | this.props.embed.image = attachment 100 | msg.attachments.forEach(a => { 101 | if (a.width && !isVideo(a)) { 102 | if (!this.props.embed.images) this.props.embed.images = [] 103 | this.props.embed.images.push(a) 104 | } 105 | }) 106 | msg.embeds.forEach(e => { 107 | if (e.type == 'image') { 108 | if (!this.props.embed.images) this.props.embed.images = [] 109 | this.props.embed.images.push(e.image || e.thumbnail) 110 | } 111 | }) 112 | if (this.props.embed.images && this.props.embed.images.length == 1) delete this.props.embed.images 113 | } 114 | } else if (msg.attachments[0] && msg.attachments[0].hasOwnProperty('size')) this.props.embed._attachment = msg.attachments[0] 115 | this.props.embed.__mlembed = true 116 | 117 | return args 118 | }, true) 119 | } 120 | 121 | pluginWillUnload() { 122 | uninject('mlembed-message') 123 | uninject('mlembed') 124 | } 125 | 126 | // queue based on https://stackoverflow.com/questions/53540348/js-async-await-tasks-queue 127 | getMsgWithQueue = (() => { 128 | let pending = Promise.resolve() 129 | 130 | const run = async (channelId, messageId) => { 131 | try { 132 | await pending 133 | } finally { 134 | return this.getMsg(channelId, messageId) 135 | } 136 | } 137 | 138 | return (channelId, messageId) => (pending = run(channelId, messageId)) 139 | })() 140 | 141 | async getMsg(channelId, messageId) { 142 | let message = getMessage(channelId, messageId) || cache[messageId] 143 | if (!message) { 144 | if (lastFetch > Date.now() - 2500) await new Promise(r => setTimeout(r, 2500)) 145 | try { 146 | const data = await get({ 147 | url: Endpoints.MESSAGES(channelId), 148 | query: { 149 | limit: 1, 150 | around: messageId 151 | }, 152 | retries: 2 153 | }) 154 | lastFetch = Date.now() 155 | message = data.body.find(m => m.id == messageId) 156 | if (!message) return 157 | message.author = new User(message.author) 158 | message.timestamp = new Timestamp(message.timestamp) 159 | } catch(e) { return } 160 | } 161 | cache[messageId] = message 162 | return message 163 | } 164 | 165 | async processLinks(message, links = []) { 166 | const embeds = [] 167 | for (let i = 0; i < links.length; i++) { 168 | const linkArray = links[i].split('/') 169 | const msg = await this.getMsgWithQueue(linkArray[5], linkArray[6]) 170 | if (!msg) continue; 171 | const avatarUrl = getUserAvatarURL(msg.author); 172 | embeds.push({ 173 | author: { 174 | proxy_icon_url: avatarUrl, 175 | icon_url: avatarUrl, 176 | name: [ msg.author.tag, React.createElement(() => null, { __mlembed: { ...msg, embedmessage: message } }) ], // hack 177 | url: links[i] 178 | }, 179 | color: msg.colorString ? parseInt(msg.colorString.substr(1), 16) : msg.embeds.find(e => e.color)?.color, 180 | description: msg.content || msg.embeds.find(e => e.description)?.description || '', 181 | footer: { text: parse(`<#${msg.channel_id}>`) }, 182 | timestamp: msg.timestamp, 183 | type: 'rich' 184 | }) 185 | } 186 | if (!embeds.length) return 187 | 188 | this.updateMessageEmbeds(message.id, message.channel_id, [ ...embeds, ...message.embeds ]) 189 | } 190 | 191 | updateMessageEmbeds(id, cid, embeds) { 192 | FluxDispatcher.dispatch({ type: 'MESSAGE_UPDATE', message: { 193 | channel_id: cid, 194 | guild_id: getChannel(cid).guild_id, 195 | id, embeds 196 | }}) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Message Link Embed", 3 | "description": "Make message links show an embed like invites or store links.", 4 | "author": "Juby210#0577", 5 | "version": "0.1.9", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .mle-attachment .fileNameLink-9GuxCo { 2 | margin-right: 50px; 3 | } 4 | --------------------------------------------------------------------------------