/** * @name BugReportHelper * @displayName BugReportHelper * @source https://github.com/l0c4lh057/BetterDiscordStuff/blob/master/Plugins/BugReportHelper/BugReportHelper.plugin.js * @patreon https://www.patreon.com/l0c4lh057 * @authorId 226677096091484160 * @invite YzzeuJPpyj */ /*@cc_on @if (@_jscript) // Offer to self-install for clueless users that try to run this directly. var shell = WScript.CreateObject("WScript.Shell"); var fs = new ActiveXObject("Scripting.FileSystemObject"); var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\BetterDiscord\plugins"); var pathSelf = WScript.ScriptFullName; // Put the user at ease by addressing them in the first person shell.Popup("It looks like you've mistakenly tried to run me directly. \n(Don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30); if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) { shell.Popup("I'm in the correct folder already.", 0, "I'm already installed", 0x40); } else if (!fs.FolderExists(pathPlugins)) { shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10); } else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) { fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true); // Show the user where to put plugins in the future shell.Exec("explorer " + pathPlugins); shell.Popup("I'm installed!", 0, "Successfully installed", 0x40); } WScript.Quit(); @else@*/ module.exports = (() => { const config = { info: { name: "BugReportHelper", authors: [ { name: "l0c4lh057", discord_id: "226677096091484160", github_username: "l0c4lh057", twitter_username: "l0c4lh057" } ], version: "1.0.4", description: "Makes it easier for you to report issues by adding a help button in some support channels (e.g. that on my server). Using that to report issues will (hopefully) give all the information needed for fixing the problem.", github: "https://github.com/l0c4lh057/BetterDiscordStuff/blob/master/Plugins/BugReportHelper/", github_raw: "https://raw.githubusercontent.com/l0c4lh057/BetterDiscordStuff/master/Plugins/BugReportHelper/BugReportHelper.plugin.js" }, defaultConfig: [ { type: "switch", id: "showHelpButton", name: "Show Help Button", note: "Shows a help button next to emoji picker and GIF button in channels supported by this plugin", value: true }, { type: "switch", id: "automaticallyShowHelpPopup", name: "Automatically Show Help Popup", note: "Opens the help modal automatically when visiting a support channel if not shown in the past seven days", value: true } ], changelog: [ { title: "Fixed", type: "fixed", items: ["No longer crashing when you have plugins using the new meta attributes instead of the functions"] } ] }; return !global.ZeresPluginLibrary ? class { constructor(){ this._config = config; } getName(){ return config.info.name; } getAuthor(){ return config.info.authors.map(a => a.name).join(", "); } getDescription(){ return config.info.description + " **Install [ZeresPluginLibrary](https://betterdiscord.app/Download?id=9) and restart discord to use this plugin!**"; } getVersion(){ return config.info.version; } load(){ BdApi.showConfirmationModal("Library plugin is needed", [`The library plugin needed for ${config.info.name} is missing. Please click Download Now to install it.`], { confirmText: "Download", cancelText: "Cancel", onConfirm: () => { require("request").get("https://rauenzi.github.io/BDPluginLibrary/release/0PluginLibrary.plugin.js", async (error, response, body) => { if (error) return require("electron").shell.openExternal("https://betterdiscord.app/Download?id=9"); await new Promise(r => require("fs").writeFile(require("path").join(BdApi.Plugins.folder, "0PluginLibrary.plugin.js"), body, r)); }); } } ); } start(){} stop(){} } : (([Plugin, Api]) => { const plugin = (Plugin, Api) => { const { WebpackModules, PluginUtilities, DiscordModules } = Api; const { React } = DiscordModules; const SelectedChannelStore = WebpackModules.getByProps("getChannelId"); const Textbox = WebpackModules.find(m => m.defaultProps && m.defaultProps.type == "text"); const Dropdown = WebpackModules.find(m => m.prototype && !m.prototype.handleClick && m.prototype.render && m.prototype.render.toString().includes("default.select")); const DeprecatedModal = WebpackModules.getByDisplayName("DeprecatedModal"); const FormTitle = WebpackModules.getByDisplayName("FormTitle"); const Button = WebpackModules.getByProps("Button").Button; const Modals = WebpackModules.getByProps("openModal"); let stateCache = {}; /** @type {Object.} */ let knownIssues = {}; /** @type {Object.} */ let supportChannels = {}; let popupLastShownTime = {}; /** * @typedef KnownIssue * @type {{title: string, ?solution: (string | string[]), ?version: (string | RegExp)}} */ const classNames = { sectionTitle: WebpackModules.getByProps("clickable", "themed", "title").title, input: WebpackModules.getByProps("error", "inputDefault").inputDefault, textArea: WebpackModules.getByProps("resizeable", "textArea").textArea, resizeable: WebpackModules.getByProps("resizeable", "textArea").resizeable, scrollbar: WebpackModules.getByProps("scrollbar", "scrollbarDefault").scrollbarDefault, error: WebpackModules.getByProps("error", "inputDefault").error, colorStandard: WebpackModules.getByProps("colorBrand", "colorStandard").colorStandard, container: WebpackModules.getByProps("body", "container", "content").container, content: WebpackModules.getByProps("body", "container", "content").content } const isPowercord = ()=>!!window.powercord; const issueOther = {title: "Other"}; class PluginInfo { constructor(name, version, author, instance){ this.name = name; this.version = version; this.author = author; this.instance = instance; } get authors(){ return this.author.split(",").map(author => author.trim()); } } /** @returns {PluginInfo} */ const convertPluginClass = plugin=>{ if(!plugin) return null; if((plugin.plugin || plugin.instance) && plugin.name && plugin.modified && plugin.filename){ return new PluginInfo(plugin.id, plugin.version, plugin.author, plugin.plugin || plugin.instance); }else{ if(typeof plugin.getName !== "function" || typeof plugin.getVersion !== "function" || typeof plugin.getAuthor !== "function") return null; return new PluginInfo(plugin.getName(), plugin.getVersion(), plugin.getAuthor(), plugin); } } /** @returns {PluginInfo[]} */ const getAllPlugins = ()=>BdApi.Plugins.getAll().map(convertPluginClass).filter(pl=>pl); /** @returns {PluginInfo} */ const getPlugin = name=>convertPluginClass(BdApi.Plugins.get(name)); return class BugReportHelper extends Plugin { onStart(){ // add empty script to prevent other plugins that load the remote version from loading that if(!document.getElementById("0b53rv3r5cr1p7")){ const el = document.createElement("div"); el.style.display = "none"; el.id = "0b53rv3r5cr1p7"; document.body.appendChild(el); }else{ if(global.__l0c4lh057s_secret_stuff && typeof global.__l0c4lh057s_secret_stuff.stopActivity === "function"){ // if remote version already loaded, stop it global.__l0c4lh057s_secret_stuff.stopActivity(); }else{ // if it is not loaded yet add a listener for when it will be loaded document.getElementById("0b53rv3r5cr1p7").addEventListener("load", ()=>window.setTimeout(global.__l0c4lh057s_secret_stuff.stopActivity, 1000)); } } this.loadIssuesAndSupportChannels().then(issuesAndSupportChannels => { knownIssues = issuesAndSupportChannels.knownIssues; supportChannels = issuesAndSupportChannels.supportChannels; }); stateCache = {}; popupLastShownTime = PluginUtilities.loadData(this.getName(), "popupLastShownTime", {}); this.onSwitch(); } onStop(){ const button = document.getElementById("l0c4lh057-issue-helper"); if(button) button.remove(); } onSwitch(){ const channelId = SelectedChannelStore.getChannelId(); const authors = supportChannels[channelId]; if(authors === undefined) return; const showInfo = ()=>{ popupLastShownTime[channelId] = Date.now(); PluginUtilities.saveData(this.getName(), "popupLastShownTime", popupLastShownTime); this.showGeneralInformation(authors); } if(this.settings.showHelpButton && !document.getElementById("l0c4lh057-issue-helper")){ let module1 = WebpackModules.getByProps("buttons","textArea","textAreaSlate"); let module2 = WebpackModules.getByProps("active","button","buttonWrapper"); let module3 = WebpackModules.getByProps("button","colorBrand","lookBlank","grow"); let module4 = WebpackModules.getByProps("button","buttonContainer","channelTextArea") let buttons = document.getElementsByClassName(module1.buttons)[0]; let button = document.createElement("button"); button.id = "l0c4lh057-issue-helper"; button.classList.add(...module4.buttonContainer.split(" "), ...module3.button.split(" "), ...module3.lookBlank.split(" "), ...module3.colorBrand.split(" "), ...module3.grow.split(" ")); button.innerHTML = `
`; button.addEventListener("click", showInfo); buttons.insertAdjacentElement("afterbegin", button); } if(this.settings.automaticallyShowHelpPopup){ const lastShown = popupLastShownTime[channelId] || 0; if((lastShown + 7*24*60*60*1000) > Date.now()) return; showInfo(); } } showGeneralInformation(authors){ if(!Array.isArray(authors)) authors = [authors]; let steps = [ "Reload discord (CTRL+R) - that will fix most of your problems.", "If you have a problem with AccountSwitcher, try removing and then saving the account again.", "Check the support channel for related issues. If it already got reported there is no need to write a full report. Just refer to the previous report and give some information and screenshots of the error messages you get.", "(Check GitHub for related issues)" ]; if(authors.every(author=>author!=="l0c4lh057")||!getPlugin("AccountSwitcher")) steps = steps.filter(step => !step.includes("AccountSwitcher")); Api.Modals.showModal( "How to ask for support", React.createElement( "div", { className: classNames.colorStandard }, "Before asking for support you should make sure that you performed the following steps:", React.createElement( "ol", { style:{ marginLeft: 20, marginTop: 10, marginBottom: 10, listStyleType: "decimal" } }, steps.map(step => React.createElement("li", {}, step)) ), React.createElement( "b", {}, "If you want to reopen this modal later just click the HELP button in the chatbox next to the gif, emoji and gift button.", React.createElement("br", {}), React.createElement("br", {}), "Don't worry about closing the modal if you need to gather additional information, all inputs should be saved during this session." ) ), { confirmText: "I did all of the above", onConfirm: ()=>this.showIssueTemplate(authors) } ); } showIssueTemplate(authors){ let closeModal = ()=>{}; const SectionTitle = props=>React.createElement( "div", {className: classNames.sectionTitle}, props.title ); const PluginSelector = props=>React.createElement( "div", {}, React.createElement(SectionTitle, {title: "Plugin"}), React.createElement( Dropdown, { clearable: false, searchable: false, options: props.plugins.map(pl=>({label:`${pl.name} v${pl.version}`, value: pl})), value: props.selectedPlugin, onChange: e=>props.selectPlugin(e.value) } ), !BdApi.Plugins.isEnabled(props.selectedPlugin.name) && React.createElement("b", {}, "The plugin is not enabled. Please make sure the issue still persists after enabling the plugin!") ); const KnownIssueList = props=>props.knownIssues.length > 0 && React.createElement( "div", {}, React.createElement(SectionTitle, {title: "Known Issues"}), React.createElement( Dropdown, { clearable: false, searchable: false, options: [...props.knownIssues, issueOther].map(issue=>({label: issue.title, value: issue})), value: props.selectedKnownIssue, onChange: e=>props.selectKnownIssue(e.value) } ) ); const Input = props=>React.createElement(Textbox, { value: props.element, placeholder: props.getPlaceholder(props.index), error: typeof props.isValid === "function" && !props.isValid(props.element), onChange: value=>props.onChange(props.index, value) }); const InputList = props=>React.createElement( "div", {}, React.createElement(SectionTitle, {title: props.name}), React.createElement( "div", {}, ...props.elements.map((element,index) => React.createElement(Input, {element, index, onChange: props.onChange, getPlaceholder: props.getPlaceholder, isValid: props.isValid})), React.createElement(Input, {element: "", index: props.elements.length, onChange: props.onChange, getPlaceholder: props.getPlaceholder, isValid: props.isValid}) ) ); const OneLineInput = props=>React.createElement( "div", {}, React.createElement(SectionTitle, {title: props.name}), React.createElement(Textbox, { error: props.missing, placeholder: props.placeholder, value: props.value, onChange: props.onChange }) ); const MultiLineInput = props=>React.createElement( "div", {}, React.createElement(SectionTitle, {title: props.name}), React.createElement("textarea", { className: [classNames.input, classNames.textArea, classNames.resizeable, classNames.scrollbar].join(" ") + (props.missing ? " " + classNames.error : ""), style: {minWidth: "-webkit-fill-available", maxWidth: "-webkit-fill-available", height: 100, minHeight: 43}, placeholder: props.placeholder, value: props.value, onChange: props.onChange }) ); const Alert = class extends React.Component { /** @param {{authors: string[], channelId: string}} props */ constructor(props){ super(props); const availablePlugins = getAllPlugins().filter(pl=>props.authors.includes("*") || pl.authors.some(plAuthor => props.authors.includes(plAuthor))); if(stateCache[props.channelId] && availablePlugins.some(pl=>pl.name===stateCache[props.channelId].selectedPlugin.name)){ this.state = Object.assign(stateCache[props.channelId], { // updating available plugins in case a plugin got installed or removed availablePlugins, // updating the selected plugin in case it got updated in the meantime selectedPlugin: availablePlugins.find(pl=>pl.name===stateCache[props.channelId].selectedPlugin.name) }); }else{ /** * @type {{authors: string, availablePlugins: PluginInfo[], selectPlugin: PluginInfo, knownIssues: KnownIssue[], selectedKnownIssue: KnownIssue, title: string, description: string, steps: string[], expectedBehavior: string, additionalContext: string, screenshots: string[]}} */ this.state = { authors: props.authors, availablePlugins, selectedPlugin: availablePlugins[0], knownIssues: (knownIssues[(availablePlugins[0]||{name:""}).name]||[]).filter(i => i.version === undefined || (typeof i.version==="string" ? i.version === availablePlugins[0].version : i.version.test(availablePlugins[0].version))), selectedKnownIssue: issueOther, title: "", description: "", steps: [], expectedBehavior: "", additionalContext: "", screenshots: [], descriptionMissing: false, titleMissing: false }; } } render(){ const hasPlugin = !!this.state.selectedPlugin; const issueIsKnown = this.state.selectedKnownIssue.title !== "Other"; let contentElement; if(!hasPlugin){ contentElement = React.createElement( "div", {className: classNames.colorStandard}, React.createElement("b", {}, "You don't have any plugin made by this author") ); }else{ contentElement = React.createElement( "div", { className: classNames.colorStandard, style: {textAlign: "start"} }, React.createElement(PluginSelector, { plugins: this.state.availablePlugins, selectedPlugin: this.state.selectedPlugin, selectPlugin: this.selectPlugin.bind(this) }), React.createElement(KnownIssueList, { knownIssues: this.state.knownIssues, selectedKnownIssue: this.state.selectedKnownIssue, selectKnownIssue: this.selectKnownIssue.bind(this) }), !issueIsKnown && React.createElement(OneLineInput, { name: "Title", placeholder: "Short issue description", value: this.state.title, missing: this.state.titleMissing, onChange: value=>this.setState({title: value, titleMissing: false}, ()=>stateCache[this.props.channelId]=this.state) }), !issueIsKnown && React.createElement(MultiLineInput, { name: "Description", placeholder: "A clear and concise description of what the bug is.", value: this.state.description, missing: this.state.descriptionMissing, onChange: e=>this.setState({description: e.target.value, descriptionMissing: false}, ()=>stateCache[this.props.channelId]=this.state) }), !issueIsKnown && React.createElement(InputList, { name: "Steps to reproduce the issue", elements: this.state.steps, getPlaceholder: i=>`Step ${i+1}`, onChange: this.editStep.bind(this) }), !issueIsKnown && React.createElement(MultiLineInput, { name: "Expected behavior", placeholder: "A clear and concise description of what you expect to happen.", value: this.state.expectedBehavior, onChange: e=>this.setState({expectedBehavior: e.target.value}, ()=>stateCache[this.props.channelId]=this.state) }), React.createElement(MultiLineInput, { name: "Additional context", placeholder: "Add any other context about the problem here.", value: this.state.additionalContext, onChange: e=>this.setState({additionalContext: e.target.value}, ()=>stateCache[this.props.channelId]=this.state) }), React.createElement(InputList, { name: "Screenshots", elements: this.state.screenshots, getPlaceholder: i=>"Link to a screenshot", isValid: this.isScreenshotLinkValid.bind(this), onChange: this.setScreenshot.bind(this) }) ); } return React.createElement( DeprecatedModal, { size: DeprecatedModal.Sizes.LARGE, tag: "form", className: classNames.container }, React.createElement( DeprecatedModal.Content, { className: classNames.content }, React.createElement( "div", {}, React.createElement( FormTitle, {tag: "h2"}, "Submit Issue" ) ), contentElement ), React.createElement( DeprecatedModal.Footer, {}, React.createElement( Button, { onClick: hasPlugin ? this.handleSubmitClick.bind(this) : closeModal }, hasPlugin ? "Submit" : "Close" ) ) ); } selectPlugin(plugin){ this.setState({ selectedPlugin: plugin, knownIssues: (knownIssues[plugin.name] || []).filter(i => i.version === undefined || (typeof i.version==="string" ? i.version === plugin.version : i.version.test(plugin.version))), selectedKnownIssue: issueOther }, ()=>stateCache[this.props.channelId]=this.state); } selectKnownIssue(issue){ this.setState({selectedKnownIssue: issue}, ()=>{ if(issue.solution){ delete stateCache[this.props.channelId]; closeModal(); }else{ stateCache[this.props.channelId]=this.state; } }); if(issue.solution){ const body = !Array.isArray(issue.solution) ? issue.solution : React.createElement( "ol", { style: { marginLeft: 20, marginTop: 10, marginBottom: 10, listStyleType: "decimal" } }, issue.solution.map(step=>React.createElement("li", {}, step)) ); Api.Modals.showModal("Solution", React.createElement("div", {className: classNames.colorStandard}, body)); } } editStep(index, value){ let state = this.state; state.steps[index] = value; state.steps = state.steps.filter(step=>step.length>0); this.setState(state, ()=>stateCache[this.props.channelId]=this.state); } setScreenshot(index, value){ let state = this.state; state.screenshots[index] = value; state.screenshots = state.screenshots.filter(link=>link.length>0) this.setState(state, ()=>stateCache[this.props.channelId]=this.state); } isScreenshotLinkValid(link){ return link===""||/^https?:\/\/[^\.]+\..*[^\.]\/.+/.test(link.trim()); } handleSubmitClick(e){ let submit = true; if(this.state.selectedKnownIssue.title === "Other"){ if(!this.state.title.trim()){ submit = false; this.setState({titleMissing: true}, ()=>stateCache[this.props.channelId]=this.state); } if(!this.state.description.trim()){ submit = false; this.setState({descriptionMissing: true}, ()=>stateCache[this.props.channelId]=this.state); } } if(this.state.screenshots.some(link=>!this.isScreenshotLinkValid(link))){ submit = false; } if(submit){ this.sendMessage(); delete stateCache[this.props.channelId]; } } getMessage(){ return this.getTitle() + this.getDescription() + this.getSteps() + this.getExpectedBehavior() + this.getInformation() + this.getAdditionalContext() + this.getScreenshots(); } sendMessage(){ // TODO: check for 2000 character limit WebpackModules.getByProps("sendMessage").sendMessage(this.props.channelId, {content: this.getMessage()}); closeModal(); } getTitle(){ if(this.state.selectedKnownIssue.title !== "Other") return "**[" + this.state.selectedPlugin.name + "] Issue: " + this.state.selectedKnownIssue.title + "**"; else return "**[" + this.state.selectedPlugin.name + "] Issue: " + this.state.title.trim().replace(/ {2,}/g, " ") + "**"; } getDescription(){ if(this.state.selectedKnownIssue.title !== "Other") return ""; let description = this.state.description.split("\n").map(l=>l.trim()).join("\n").trim().replace(/\n{3,}/g, "\n\n"); if(description.length > 0) return "\n\n**Bug description**\n" + description; else return ""; } getSteps(){ if(this.state.selectedKnownIssue.title !== "Other") return ""; let steps = this.state.steps.map(step=>step.trim()).filter(step=>step); if(steps.length > 0) return "\n\n**Steps to reproduce**\n" + steps.map((step,i)=>(i+1)+". "+step).join("\n"); else return ""; } getExpectedBehavior(){ if(this.state.selectedKnownIssue.title !== "Other") return ""; let behavior = this.state.expectedBehavior.split("\n").map(l=>l.trim()).join("\n").trim().replace(/\n{3,}/g, "\n\n"); if(behavior) return "\n\n**Expected Behavior**\n" + behavior; else return ""; } getInformation(){ return `\n\n**Information** - Versions: \\* Plugin: ${this.state.selectedPlugin.version} \\* BD: ${isPowercord() ? `Powercord ${window.powercord.gitInfos.revision.substring(0, 7)} (bdCompat ${window.powercord.pluginManager.get('bdCompat').manifest.version})` : BdApi.getBDData("version")} \\* ZLibrary: ${(getPlugin("ZeresPluginLibrary")||{version:"not installed"}).version} \\* Release channel: ${WebpackModules.getByProps("releaseChannel").releaseChannel} \\* Build ID: ${GLOBAL_ENV.SENTRY_TAGS.buildId} - OS: ${(os=>os==="win32"?"Windows":os==="darwin"?"MacOS":os==="linux"?"Linux":os)(require("os").platform())} - Compact mode: ${WebpackModules.getByProps("customStatus","renderSpoilers","messageDisplayCompact").messageDisplayCompact?"yes":"no"} - Plugin enabled: ${BdApi.Plugins.isEnabled(this.state.selectedPlugin.name)?"yes":"no"}` + (this.state.selectedPlugin.name==="AccountSwitcher"&&this.state.selectPlugin.author==="l0c4lh057"?`\n- Encryption enabled: ${this.state.selectedPlugin.instance.settings.encrypted?"yes":"no"}`:""); } getAdditionalContext(){ let context = this.state.additionalContext.split("\n").map(l=>l.trim()).join("\n").trim().replace(/\n{3,}/g, "\n\n"); if(context) return "\n\n**Additional context**\n" + context; else return ""; } getScreenshots(){ let screenshots = this.state.screenshots.map(l=>l.trim()).filter(l=>l); if(screenshots.length > 0) return "\n\n**Screenshots**\n" + screenshots.map(l=>"- "+l).join("\n"); return ""; } }; let modalId = Modals.openModal(props => { return React.createElement( WebpackModules.getByProps("ModalRoot").ModalRoot, { size: "large", transitionState: props.transitionState }, React.createElement(Alert, {authors, channelId: WebpackModules.getByProps("getChannelId").getChannelId()}) ); }); closeModal = ()=>Modals.closeModal(modalId); } async loadIssuesAndSupportChannels(){ return new Promise((resolve, reject) => { require("request").get("https://raw.githubusercontent.com/l0c4lh057/BetterDiscordStuff/master/Plugins/BugReportHelper/data.json", (error, response, body) => { if(error) reject(error); else resolve(JSON.parse(body)); }); }); } getSettingsPanel(){ return this.buildSettingsPanel().getElement(); } } }; return plugin(Plugin, Api); })(global.ZeresPluginLibrary.buildPlugin(config)); })();