Compare commits

...

334 Commits

Author SHA1 Message Date
62b2acebe6 Add support for Flatpak for Git updating (#274)
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-02 16:55:53 +01:00
41dddc9eee feat(plugin): ShikiCodeblocks (#267)
Co-authored-by: ArjixWasTaken <53124886+ArjixWasTaken@users.noreply.github.com>
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-02 16:43:37 +01:00
4760af7f0e add ViewRaw plugin & MiniPopover API (#275)
Co-authored-by: Vendicated <vendicated@riseup.net>
2022-12-02 16:38:52 +01:00
06d32ae414 browser: remove firefox extension id 2022-12-02 14:24:23 +01:00
2564ab73f5 ci: unlisted firefox builds for now 2022-12-02 14:21:44 +01:00
5e97cc0fc3 QuickCss: Hide MenuBar; explicitly enable contextIsolation
Closes #260
2022-12-02 14:11:20 +01:00
b9e9d9bd64 Add --vanilla flag, strip csp on mainFrame only 2022-12-02 14:10:40 +01:00
daf3a1dcac Try to make firefox publish work 2022-12-01 19:43:57 +01:00
Ven
f1fb79d2c5 Fix workflow 2022-12-01 19:22:49 +01:00
0ff6d3dd41 Add Firefox extension build (#277) 2022-12-01 19:16:09 +01:00
734054ff68 feat(Settings): Allow moving Vencord section to different places 2022-12-01 03:38:17 +01:00
f94cbfb2f4 Add basic themes tab 2022-12-01 03:01:44 +01:00
fc09460d82 feat(plugin): add ServerListIndicators (#272) 2022-11-29 00:25:07 +01:00
e884738f42 MemberCount: Fix misleading count, add tooltip 2022-11-28 23:01:09 +01:00
c583bad6bf the shiggy wiggy (#270) 2022-11-28 18:59:42 +00:00
36b787812e Add MemberCount plugin 2022-11-28 19:29:46 +01:00
Ven
836ae72076 Delete report.md 2022-11-28 16:00:54 +01:00
d0a40bc0ed chore: update deps 2022-11-28 15:59:15 +01:00
3b4879f9d9 perf(settings): Cache proxies 2022-11-28 15:44:53 +01:00
a0a1a4d139 enforce path aliases with eslint 2022-11-28 13:59:53 +01:00
Ven
bad96b7887 Path aliases, better lazyWebpack (#268) 2022-11-28 13:37:55 +01:00
7a4402f142 BlurNSFW: Support videos 2022-11-28 01:08:58 +01:00
3e9672c6b8 oop 2022-11-28 00:58:26 +01:00
a9fee6248e BlurNSFW: Add amount setting 2022-11-28 00:55:50 +01:00
f0ee16f173 [skip ci] update genPluginList 2022-11-28 00:45:41 +01:00
3db3c63b42 BlurNsfw plugin 2022-11-28 00:42:42 +01:00
4fc41c8c0b fix: add predicate to updater menu item (#266)
* fix: add predicate to updater menu item

* dont include Updater in web builds

* i can spell
2022-11-27 16:07:31 +01:00
47c181beec [skip ci] Add support for DVM installation path. (#265) 2022-11-27 14:32:54 +01:00
c4fc01c7ff [skip ci] feat(ci): test web builds (#262) 2022-11-25 23:51:36 +01:00
Ven
5a94201578 Megu blowing up main :blobcatcozyscared: 🚎 2022-11-25 23:41:02 +01:00
6b55dee9fb feat(settings): new settings design (#261) 2022-11-25 22:38:55 +00:00
a85ec594a7 [skip ci] docs docs docs 2022-11-25 19:25:35 +01:00
c2c6c9fccb CallTimer: Fix lag 2022-11-25 18:28:15 +01:00
b60f6cb18d WhoReacted: Make more reliable & don't spam api 2022-11-25 18:07:29 +01:00
bb398970ef HideAttachments: Fix embeds
Closes #259
2022-11-25 18:06:31 +01:00
50a96e8047 CallTimer: Fix typo 2022-11-25 16:16:07 +01:00
c5b5b754e2 CallTimer 2022-11-25 15:59:47 +01:00
0f644dff73 loadingQuotes quote fix (#255) 2022-11-24 14:26:38 +01:00
6210d3a597 Make ReviewDB Look More Native (#256) 2022-11-24 14:26:18 +01:00
e7573382fe fix(betterNotes): add restart needed for hide notes patch (#258) 2022-11-24 14:02:11 +01:00
f4d7a1f4fb New Plugin: BetterNotesBox 2022-11-24 02:02:15 +01:00
5dd0a3a746 New Plugin: HideAttachments 2022-11-24 01:00:13 +01:00
Ven
c9fac8ffff fix tags 2022-11-23 20:04:25 +01:00
f93607fc66 add new quotes to loadingQuotes (#254)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-23 20:00:19 +01:00
63ffb5bebc feat(messageTags): Add message quick reply plugin (#241) 2022-11-23 19:56:20 +01:00
jd
2788d264d4 feat(plugin): Urban Dictionary (#222) 2022-11-23 14:30:59 +01:00
91f1d68e29 feat(plugins): Keep Current Channel plugin (#248) 2022-11-23 02:51:45 +01:00
7e4f4f1794 feat(plugins): Volume Booster plugin (#249) 2022-11-22 23:22:54 +01:00
9f7ec0aa8d settings: better button text for plugin settings modal (#251) 2022-11-22 22:05:46 +00:00
0239bb0aac Commands: Show plugin name instead of 'Built-In' 2022-11-22 22:42:22 +01:00
ec20556d5c PlatformIndicators: Fix icon colours 2022-11-22 17:06:24 +01:00
Ven
11191b5943 Update 1_INSTALLING.md 2022-11-21 22:26:12 +01:00
1f72a0fc27 fix(arRPC): fix error on null activity (#244) 2022-11-21 20:40:40 +01:00
31ec1ec1b4 better platformindicators settings (#243) 2022-11-21 20:12:46 +01:00
0f7c80fd4d Fix no gap (#242) 2022-11-21 19:54:48 +01:00
Ven
b5bc88c7d4 Settings export/import (#235) 2022-11-21 19:25:40 +01:00
b42b8d755f Platform indicators: ignore unnecessary element (#240) 2022-11-21 19:25:21 +01:00
bfe1fd9912 fix: add keys to plugins grid (#237) 2022-11-21 18:45:22 +01:00
c45d89697a make userscript autoincrement version :trollface: (#233) 2022-11-21 16:32:56 +01:00
0a92bd6521 PlatformIndicators: Fix server list 2022-11-21 15:59:19 +01:00
33c33eb0fd feat(plugin): PlatformIndicators (#227)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-21 15:44:30 +01:00
dcf1148bb4 feat(plugin): TimeBarAllActivities (#228) 2022-11-21 11:53:28 +01:00
58e28b4281 feat(fakeNitro): add an option to change emote sizes (#225)
closes https://github.com/Vendicated/Vencord/issues/204
2022-11-21 03:43:16 +01:00
bb14d4989d feat(plugin): NoUnblockToJump (#229) 2022-11-21 03:40:15 +01:00
9bcdc8451f feat(arRPC): update for server 2.2 (#230) 2022-11-21 00:57:30 +01:00
46b14cb2e0 feat(plugins):WhoReacted keep reaction count (#231) 2022-11-21 00:56:17 +01:00
9240865f65 feat(arRPC): update for server 2.0 (#224) 2022-11-20 16:21:42 +01:00
e85d763f22 feat(plugin): WebRichPresence (arRPC) (#223) 2022-11-20 14:31:00 +01:00
82911386db oop 2022-11-19 22:17:55 +01:00
e63ed9cac4 onekocord 2022-11-19 22:13:16 +01:00
ba45ecda56 feat(plugin): Last.fm rich presence (#220)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-19 18:40:52 +01:00
7ff2d2ba8a fix startup timings page (#219) 2022-11-19 15:52:17 +00:00
a5154d6283 feat(plugin): Quick mention button (#218)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-19 16:11:11 +01:00
5ce2dc1bb4 feat(plugin): Read all notifications button (#217)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-19 14:54:48 +01:00
8f2c247f27 Fix commands showing up multiple times Part 2 2022-11-18 23:31:53 +01:00
43f41d20fa Fix commands showing up multiple times 2022-11-18 23:29:34 +01:00
Ven
50c356e397 fix brain fart 2022-11-18 13:39:43 +01:00
503a2ec517 Add option to ignore incoming blocked messages (#179) 2022-11-18 05:12:45 +01:00
83b3b1f16b fix settings debug info on web 2022-11-17 13:49:51 +01:00
2628bdce42 WebContextMenus: Port copy/open link items to Discord Web 2022-11-17 01:30:23 +01:00
8b0911b86a Updater: Ignore non release commits 2022-11-17 00:45:00 +01:00
Ven
47d127a895 Update FUNDING.yml 2022-11-17 00:41:20 +01:00
410613726b Donor Badges && Add donate info to settings 2022-11-17 00:21:20 +01:00
Ven
8b3f290e3c Create FUNDING.yml 2022-11-16 22:40:26 +01:00
a788813383 VencordWeb: Migrate to manifest v3 2022-11-16 16:23:52 +01:00
e1de6f88fe Unexplode Modals on canary 2022-11-16 14:52:05 +01:00
ae86848cf6 Fix ReviewDB 2022-11-16 01:02:23 +01:00
84ec839b04 Add ReviewDB Plugin (#187)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-16 00:40:46 +01:00
b30508aef8 better handling for settings ui errors 2022-11-15 17:29:31 +01:00
eabbf7d9bd fix(fakeNitro): add missing predicate for sticker bypass (#215) 2022-11-15 09:34:53 +01:00
be088f9072 Don't unnecessarily create functions many times 2022-11-15 09:30:33 +01:00
2ca98a87d2 Fix Settings UI on canary 2022-11-15 09:28:06 +01:00
b49ac6b541 ClickableRoleDot -> BetterRoleDot; now allows using both role colour styles at once 2022-11-14 21:42:02 +01:00
Ven
82e444e196 Less confusing plugin names (bulk plugin rename) (#214)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2022-11-14 18:05:41 +01:00
Ven
a96f8a89f3 MessageLogger: fixes + ignoreSelf & ignoreBots option (#213) 2022-11-14 16:22:50 +01:00
afn
4642b54260 feat(plugin): FriendInvites (#208)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-13 23:34:10 +01:00
Ven
15b257a7b0 Friendlier Readme 2022-11-13 23:24:25 +01:00
0dbec8d0cd feat: message logger plugin (#49)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-13 23:13:32 +01:00
e5b23ff556 EmoteYoink: Don't depend on ReverseImageSearch whoopsie 2022-11-13 04:12:37 +01:00
9110d1f9bd Emote Clone plugin 2022-11-13 03:46:46 +01:00
81edc14070 fix PronounDB crash with new profile in dms, force start dependencies 2022-11-12 17:20:19 +01:00
b48c8d8a4a NitroSpoof: Fix inbuilt sticker logic; cleanup 2022-11-12 16:25:28 +01:00
8380328465 InteractionKeybinds: Scroll to message if offscreen
Closes https://github.com/Vendicated/Vencord/issues/200
2022-11-12 00:29:36 +01:00
30ca4f1cf9 feat: Badge API (#206) 2022-11-11 23:50:09 +01:00
62e0787cf2 fix(plugins): Fix IgnoreActivities (#205) 2022-11-11 19:02:03 +01:00
cc7c14ec88 Reporting: do not error for patches with all:true 2022-11-11 19:01:01 +01:00
a86452e774 fix react hook error 2022-11-11 18:58:30 +01:00
dddb28192c even more plugin fixes 2022-11-11 18:49:47 +01:00
2133823bd3 more plugin fixes 2022-11-11 16:43:40 +01:00
1176896a1b fix(plugins): PronounDB, ViewIcons, WebhookTags, NoBlockedMessages, BetterGifAltText, MessageAccessories 2022-11-11 16:14:09 +01:00
Ven
f3aba3edb0 ci: Add webhook secret to reporter env 2022-11-11 13:30:51 +01:00
409e54a9d8 ci(reporter): Post results to discord webhook 2022-11-11 13:27:44 +01:00
31fb19b8c9 ci: Hopefully fix reporting 2022-11-11 13:06:04 +01:00
a26f636c9b ci: Automated plugin test with puppeteer 2022-11-11 12:37:37 +01:00
8ba9c96f20 Fix most plugins 2022-11-11 00:11:44 +01:00
57f3feba68 spotifyControls: make album of local tracks unclickable (#203) 2022-11-10 19:33:00 +01:00
010523eeac feat(plugins): add vc effect event to moyai plugin (#199) 2022-11-10 14:04:06 +01:00
15f12073cf spotifyControls: make title/artists of local tracks unclickable (#201)
Co-authored-by: Vendicated <vendicated@riseup.net>
2022-11-10 14:02:34 +01:00
58636a9a82 CorruptMp4s: Depend on CommandsAPI ~ PronounDB: Add pronoundb link 2022-11-09 23:01:59 +01:00
0bc894d065 CorruptMp4s: Better default 2022-11-09 21:17:21 +01:00
6f38c4b7fe new plugin(CorruptMp4s): Mp4s with infinite/negative duration 2022-11-09 21:15:52 +01:00
c1d2f0078f StickerSpoof: Fix not correctly cleaning previous frame 2022-11-09 20:29:35 +01:00
3c8084ec36 Add VSCode debug config 2022-11-09 19:26:46 +01:00
3b65384b94 fix(spotifyControls): add album/cover null checks (local files) (#198)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-09 17:36:20 +01:00
e0450531ef StickerSpoof: fix small resolutions; AnonymiseFiles: fix extension logic 2022-11-09 17:30:48 +01:00
Ven
b032e9b6e3 oop 2022-11-09 16:08:11 +01:00
1e6b967d24 Fix moyai and fart 2022-11-09 12:47:16 +01:00
460f329e4f fix double click actions using outdated content 2022-11-08 18:09:11 +01:00
3a3a52c493 fix(NitroBypass): Fix using stickers bypass with Nitro Classic (#196) 2022-11-08 17:51:09 +01:00
afn
4e57ae66f1 feat(SpotifyControls): prettier design (#194)
Co-authored-by: afn <hey@afn.lol>
Co-authored-by: KraXen72 <DPELECH1@GMAil.com>
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-08 17:31:36 +01:00
f7d9be9140 lint: Disallow utils index imports
This keeps leading to issues due to circular imports.
Import from specific files instead, index just reexports
2022-11-07 23:34:14 +01:00
955573d31b me when i dont depend on MenuItemDeobfuscatorApi 2022-11-07 22:36:06 +01:00
Ven
6a8564089b SpotifyControls plugin (#190) 2022-11-07 22:28:29 +01:00
7d5ade21fc feat(nitroBypass): add sticker bypass (#184)
Co-authored-by: Vendicated <vendicated@riseup.net>
2022-11-07 22:23:34 +01:00
Ven
d69dfd6205 ahem 2022-11-07 21:39:49 +01:00
Ven
177d353f50 Rename DevBuild releases to include git hash 2022-11-07 21:38:14 +01:00
a13c0df1cd build: Add metadata header to all bundles 2022-11-07 21:29:31 +01:00
0af4579204 Add tracer, fix MessageActions slow startup 2022-11-07 21:05:33 +01:00
851d07f31a fix(ReverseImageSearch): Don't apply to non image files 2022-11-07 18:52:34 +01:00
963a7332b4 Migrate proxied components to and fix LazyComponent 2022-11-06 18:37:01 +01:00
440baf6028 Improve proxyLazy 2022-11-06 18:00:59 +01:00
9663e229a6 feat(plugins): add Startup Timings (#189) 2022-11-05 11:09:05 +01:00
0cb24cad7e feat: make text selectable in PatchHelper (#188)
* feat: make text selectable in PatchHelper

* real div moment
2022-11-05 10:02:29 +01:00
65620f4976 Webpack: Do not emit errors if devtools open 2022-11-03 20:36:17 +01:00
cb7469afad Simplemarkdown pleeeeease 2022-11-03 19:15:51 +01:00
2c3dee4120 qol improvements 2022-11-03 19:12:50 +01:00
c20dc269d2 Modify CSP instead of deleting it 2022-11-02 22:15:55 +01:00
a7795533df Remove clipboardImageFix - Discord fixed the bug woooooo 2022-11-02 20:54:39 +01:00
5e1b42120c Fix plugins on new update 2022-11-02 20:13:55 +01:00
676f5c7e30 ViewIcons: size 2048 -> 512, to fit on screen 2022-11-02 17:30:15 +01:00
13c73699e9 Fix Webpack modules that are not arrow funcs, Part II 2022-11-01 15:06:15 +01:00
64aed87de4 Fix Webpack modules that are not arrow funcs 2022-11-01 14:28:25 +01:00
1944f3957f fix forceOwnerCrown Plugin Spamming Errors in Console (#180)
Co-authored-by: Nico <nico@d3sox.me>
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-01 02:19:07 +01:00
Ven
04d6f341ee PatchHelper, a tool to help you write patches (#182) 2022-11-01 01:49:41 +01:00
0c25278c59 fix renamed app.asar detection on Windows (#185) 2022-11-01 01:47:07 +01:00
0fda900ccc Fix: settings.appearance may be undefined 2022-10-31 17:17:54 +01:00
Ven
8adf7ca155 Webpack Warnings & Errors (#178)
* dev: Useful strict Warnings & Errors

* Always log error

* Ignore pending patches with all or whose predicate = false

* Error -> Warn
2022-10-30 20:45:18 +01:00
b905743077 removed channel type (#170)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-30 19:17:46 +01:00
a43a41f61f vcDoubleClick: don't require dbl click on active vc, fix stage channels (#172) 2022-10-30 18:47:12 +01:00
Ven
3af9a14a0e Patcher: More useful errors with code diffs (#177)
* Patcher: More useful errors with code diffs

* Nicer log formatting

* PluginCards: ellipsises
2022-10-30 02:58:11 +01:00
739b1e47d4 New plugin: LoadingQuotes 2022-10-29 22:53:23 +02:00
Ven
d72542405a Implement Subcommands; fix errors due to Settings <-> Plugins circular imports (#174) 2022-10-29 20:45:31 +02:00
95aa2d9d8d ClipboardImageFix is not actually required 2022-10-29 20:36:43 +02:00
Ven
93859883c1 build: inject createElement alias (#176) 2022-10-29 20:27:48 +02:00
37105ac416 feat(plugin): ClipboardImageFix (#173) 2022-10-29 20:25:40 +02:00
f6e0efe20a Reverse image search plugin 2022-10-29 15:25:34 +02:00
1764206e19 Add MenuItemDeobfuscator 2022-10-29 15:25:10 +02:00
6b0caaae37 fix(ShowHiddenChannels): Fix New unreads box for hidden channels #168 2022-10-27 20:26:54 +02:00
c76e9f5e3d better patch & visual bug fix (#167) 2022-10-27 18:37:54 +02:00
Ven
ce73a5f172 use gh cli to update release (#166) 2022-10-27 10:55:56 +02:00
9548978d80 fix(IgnoreActivities): id -> exePath (#164)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-27 10:27:52 +02:00
13882b5732 feat: custom components in settings (#165) 2022-10-26 23:42:26 +02:00
49e72bab32 moarKaomojis plugin (#137) 2022-10-26 15:38:41 +02:00
bbd3633038 fix(Ignore Activities): Fixes games manually added not being able to be ignored (#162)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-26 15:31:55 +02:00
c65f757bc4 Fix betterUploadButton 2022-10-26 15:10:11 +02:00
b87f0bf3f9 Settings: Cache default value 2022-10-26 14:28:27 +02:00
670b7d7d01 Make tsc happy 2022-10-26 13:54:23 +02:00
f492d26379 Make jsFactory shorter -> bundle size -10% 2022-10-26 13:49:28 +02:00
Ven
56b00f715a ci: Only rebuild on actual code changes 2022-10-26 13:03:53 +02:00
fe5a78ddc9 Update README 2022-10-25 21:40:23 +02:00
5e7c155f6e feat(settings): add beforeSave check (#161) 2022-10-25 18:49:50 +01:00
e06ba68c40 fix(vcDoubleClick): add required parentheses (#160) 2022-10-25 18:15:21 +02:00
d6fe937a70 fix(vcDoubleClick): exclude text channel mentions (#159) 2022-10-25 18:09:21 +02:00
2f46b934c9 feat: add new plugin ForceOwnerCrown (#157) 2022-10-25 17:32:05 +02:00
2af324c302 Improve genPluginList 2022-10-25 12:29:55 +02:00
4bddcee40b Add autogenerated plugin list, closes #151 2022-10-25 12:20:29 +02:00
559edbfffe Fix vcDoubleClick, add support for stage channels (#158) 2022-10-25 10:53:06 +02:00
6c38362401 Ignore Activities: Fix button not working (#156)
explode
2022-10-25 00:18:30 +02:00
00402c69d6 feat(plugin): Ignore Activities (#142) 2022-10-25 00:05:40 +02:00
30dd4b9e01 [ShowHiddenChannels] Add last message info, fix collapsing (#146)
Co-authored-by: Snek <107999380+sneksnake@users.noreply.github.com>
2022-10-24 23:22:39 +02:00
Ven
5fac8be0ae Vencord Standalone without git/node (#148) 2022-10-23 23:23:52 +02:00
ffbb52512c fix: plugin dependencies not enabling (#150) 2022-10-23 19:09:02 +01:00
ff9d904fcb Update license headers in files created by Samu (#149) 2022-10-23 16:33:28 +02:00
50c0d472d7 feat(NoCanaryMessageLinks): Allow custom subdomains (#147) 2022-10-23 14:19:53 +02:00
abbc08fb06 feat(plugin): Show Hidden Channels (#101) 2022-10-23 11:44:21 +02:00
409fb6ff4e encoding binary doesn't return a buffer?? 2022-10-22 23:47:26 +02:00
934a89add0 feat: switch in plugin settings (#140) 2022-10-22 23:38:48 +02:00
a3b0556a9a buildWeb: use fflate instead of yazl 2022-10-22 23:35:30 +02:00
35d2b8d1cf patcher: Add back support for discord_arch_electron 2022-10-22 19:05:50 +02:00
0328966e0f Unbrick OpenAsar installs 2022-10-22 18:34:29 +02:00
2eb8f3ae19 oops, add plugin to deps 2022-10-22 18:23:38 +02:00
61fd38d6d9 style: Sort imports 2022-10-22 18:18:41 +02:00
Ven
a7dbd73547 Windows: Patch host updater to reinject (#138) 2022-10-22 18:17:02 +02:00
c116d00d03 Implement Chrome extension loading myself because electron-devtools-installer is ultra bloated 2022-10-22 06:31:47 +02:00
44f6f71c3e Monaco for Discord Desktop 2022-10-22 04:42:54 +02:00
Ven
23d4cae123 Installer: add custom path option (#77) 2022-10-22 02:36:49 +02:00
0da02e009c Remove now obsolete licensing info from README 2022-10-22 01:29:17 +02:00
7d555a96ea fix userscript, lint new files 2022-10-22 01:22:44 +02:00
f92f3f1a5e Add license headers 2022-10-22 01:17:14 +02:00
6769de29cd feat(api): add Clipboard entry in webpack common (#127) 2022-10-22 00:18:32 +02:00
e2b622c76b feat(api): Message Accessories API (#131) 2022-10-22 00:17:18 +02:00
4b1e96b76e Vencord Web: QuickCss Monaco Editor 2022-10-21 23:58:41 +02:00
e93111fb67 Update nitroBypass.ts (#132) 2022-10-21 13:37:53 +02:00
ccf7f66a79 Update PronounDB Plugin (#115)
* Add X-PronounDB-Source header, add options to pronoundb

* Adapt to defaults fix, better lowercase logic

* User popouts :)
2022-10-21 12:46:38 +02:00
d8afde2b4d feat(plugins): Moyai ignore bots setting (#130) 2022-10-21 00:05:08 +02:00
a15d5de493 New plugin: vcDoubleClick 2022-10-20 23:06:28 +02:00
05051399b6 Fix error when enabling a plugin with commands if CommandsAPI hasn't loaded 2022-10-20 20:23:14 +02:00
e4068ef9a6 Document apis 2022-10-20 20:23:14 +02:00
c80ed1b824 legal: Make esbuild generate & link licensing information 2022-10-20 20:23:14 +02:00
50047dd3c2 Add DataStore (IndexedDB wrapper)
An alternative to the synchronous Settings API to work around storage
limitations of localStorage and allow  storing of arbitrarily large
data in form of most Javascript DataTypes,
see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types
2022-10-20 20:23:14 +02:00
36f4478a4f more eslint 2022-10-20 11:58:20 +02:00
350e7b0a6a feat(moyai): Add option to toggle triggering moyai when the window is not focused (#123) 2022-10-20 07:48:46 +02:00
7eba5b99b0 feat: add sendify plugin (#122)
* feat: add sendify plugin

* fix: tweak plugin description

* fix: respect message replies

This implements code provided by Ven, while taking into account possible
errors from Discord as well as minor changes for readability.

Co-authored-by: Ven <vendicated@riseup.net>
2022-10-20 00:53:09 +02:00
f81ab5ef93 Add volume settings to fart plugin (#113)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-19 23:28:02 +02:00
1f50f78912 feat: settings sliders (#120)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-19 21:57:27 +02:00
efab399309 feat(BANger): Customisable gif or video (#121)
Signed-off-by: glitchy <thepatheticweebgamer@gmail.com>
2022-10-19 21:50:58 +02:00
dd1537a5d6 feat(plugin): Ify improvements (#119) 2022-10-19 21:28:01 +02:00
d97c3e2e02 Fix ViewIcons, make banner have right cursor
Discord loves pushing 2 almost identical Avatar components for A/B
testing, they have never heard of an if statement 🚎
2022-10-19 19:45:22 +02:00
7cdc4e4c03 feat: randomiseFileNames -> anonymiseFileNames (#116)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-19 15:23:22 +02:00
d3bf5cec9a Fix nitroBypass 2022-10-19 12:27:20 +02:00
Ven
a4303e3810 README: fix link 2022-10-19 12:05:35 +02:00
Ven
1ea8a0b69b Add License details 2022-10-19 11:57:42 +02:00
139dd7a92e fix(interactionKeybinds): disable ping when replying to own message (#118) 2022-10-19 09:36:40 +02:00
Ven
b66903cf52 Settings: Implement plugin options defaults (#117) 2022-10-18 22:53:37 +02:00
Ven
287173458f Updater: Fix error when update check fails
fixes #114
2022-10-18 09:57:32 +02:00
beb9aae26b show only the dependants that are enabled (#111) 2022-10-18 09:53:01 +02:00
9d6021f0b9 feat: gitignored src/userplugins directory (#112) 2022-10-17 23:46:52 +02:00
5a18292d92 fix(Experiments): Broken isStaff match 2022-10-17 21:22:08 +01:00
5625d63e46 Settings 2.0 (#107)
Co-authored-by: Vendicated <vendicated@riseup.net>
2022-10-17 20:18:25 +01:00
ae730e8398 Add pronoundb plugin (#104) 2022-10-17 18:05:22 +02:00
ad054d5c65 Fix messageActions 2022-10-17 16:29:26 +02:00
82d53b1928 added mock command (#103) 2022-10-16 23:25:27 +02:00
c7c5ffdd44 Allow customising SettingsDir via VENCORD_USER_DATA_DIR environment variable (#106) 2022-10-16 23:20:43 +02:00
0ccea16453 Unbrick updater 2022-10-16 17:23:18 +02:00
20237f5664 watch: enable sourcemaps and disable minify 2022-10-16 17:21:23 +02:00
01ae0983b3 Optimise Web via treeshaking, cleanup build scripts 2022-10-16 17:15:15 +02:00
845088ec02 buildWeb: suppress experimental api warnings 2022-10-15 20:56:48 +02:00
9c7b548a9e BrowserExtension: Disable CSP 2022-10-15 20:56:21 +02:00
c8d87da62d fart: lazy Audio, moyai: fix cors issues 2022-10-15 19:07:44 +02:00
0d996633f2 ViewIcons: Use ImageModal again 2022-10-14 22:40:20 +02:00
a4e98f9252 proxyLazy: Fix constructors 2022-10-14 22:40:16 +02:00
53794ec180 Fix jsdoc 2022-10-14 21:40:10 +02:00
296336535f Fix modals, add wp.mapMangledModule 2022-10-14 21:34:35 +02:00
563f2fb1dc Add workspace settings for vscode 2022-10-14 19:05:01 +02:00
d73a6e2c89 Fix fxTwitter regex (#100)
* Fixes so that wont affect other things like likes
* Fixes #97 

Co-authored-by: Ven <vendicated@riseup.net>
2022-10-14 18:13:13 +02:00
Mai
2cb6c23347 add choices to CommandsAPI Options (#94) 2022-10-14 13:21:44 +02:00
87b6d6ab12 Patch#all option to patch duplicate modules (#99) 2022-10-14 01:47:10 +02:00
bf49acd535 Fix Settings errors when retrieving a null value; add PlainSettings 2022-10-14 00:36:44 +02:00
ea0ded0f11 Populate package.json fields 2022-10-14 00:12:43 +02:00
d26196d6c5 Add a UserScript build 2022-10-14 00:12:30 +02:00
5fe04c5882 turns out electron13 has no array.at() 2022-10-13 18:51:25 +02:00
Ven
a73e10fc77 quickReply => InteractionKeybinds: now supports edits (#95) 2022-10-13 18:42:35 +02:00
8817e2dff7 Reorganise command plugins 2022-10-12 22:22:37 +02:00
267b2b1a07 Commands: basic error handling 2022-10-12 22:22:21 +02:00
83d480a68c Fix bots showing as webhook 2022-10-12 19:42:36 +02:00
ebe62a1790 FakeClyde (plugin) and sendBotMessage (API addition) (#66)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-12 18:19:42 +02:00
Ven
09b3f6d19b ci: rename lint -> test 2022-10-12 16:06:53 +02:00
8dff79d3f7 petpet: fix guild pfps 2022-10-12 05:55:44 +02:00
9b7ebe4680 petpet 2022-10-12 05:35:34 +02:00
8e93c5cb43 CommandsApi: fix accidently overwriting inputType BUILT_IN 2022-10-12 05:34:39 +02:00
66f8fde353 Improve ErrorBoundary layout
Now the error cause will be wrapped to prevent it from being cut off,
only wrap the stacktrace in pre
2022-10-12 01:54:38 +02:00
071508c61a fix(Webhook Tags): Don't mark clyde messages as Webhook 2022-10-12 01:40:10 +02:00
bfb4114e18 feat(plugin): UnminifyErrors (#90)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-12 01:25:50 +02:00
6afd959530 feat(plugin): No F1 (#88) 2022-10-11 21:54:52 +02:00
86eacea74d Make ReactDevTools Opt-in 2022-10-11 21:48:28 +02:00
516f8c488a Switch to standalone-electron-types, -200MB npms 2022-10-11 17:48:14 +02:00
c32426882e Add CI (#87)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-11 15:19:04 +02:00
39a7b2f5a9 feat(plugin): No RPC (#84) 2022-10-11 04:50:42 +02:00
7a0560b9d4 Fart plugin (#71)
* the silly

* fart conflicts

* Update fart.ts

Co-authored-by: Ven <vendicated@riseup.net>
2022-10-11 00:20:27 +02:00
e685e399f9 fix(plugin): fix isStaff returning true for all users (#83)
Co-authored-by: nmsturcke <30734036+nmsturcke@users.noreply.github.com>
2022-10-10 23:45:11 +02:00
54198b1a4a fxTwitter (plugin) (#80)
Co-authored-by: splatterxl <splatterxl@outlook.ie>
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-10 22:45:54 +02:00
124d1ad9c7 Plugin that removes canary subdomain from message (#60) 2022-10-10 01:35:50 +02:00
abfade4f38 Update lockfile 2022-10-09 23:02:06 +02:00
a89e17a390 Make typescript happy, tsc test run in CI 2022-10-09 22:58:08 +02:00
5610df8b37 Holy shidd Aliucod Desktop real????
This is what happens if you dont pay attention while kanging :trollface:
2022-10-09 22:36:26 +02:00
9e6ee4df52 Fail on build error 2022-10-09 22:35:59 +02:00
304bf4fe29 Unignore lock file 2022-10-09 22:31:51 +02:00
Ven
25a64ab6be Update lint.yml 2022-10-09 22:24:52 +02:00
Kot
a3c2da31c3 Specify pnpm for corepack (#27) 2022-10-09 22:22:35 +02:00
f875d63c6d Fix webpack search 2022-10-09 22:15:23 +02:00
e7fb4ebd4e Protocol whitelist (#70)
* allowed protocols

* i forgot javascript actually has includes lol
2022-10-09 19:55:13 +02:00
2105de8ca5 Add more Webpack Commons & utils 2022-10-09 19:48:42 +02:00
bb7332cefd Moyai: Support Reactions, ignore bots 2022-10-09 19:48:22 +02:00
43951456d3 🗿🗿🗿 2022-10-09 17:27:50 +01:00
d3c581eb4e 🗿🗿🗿 (#75)
🗿🗿🗿
2022-10-09 17:18:18 +01:00
151f2eef8a Improve plugin typings 2022-10-09 01:59:21 +02:00
e0bbdd89bd fix lazyWebpack.construct, lint uwuify 2022-10-08 21:11:14 +02:00
b101e643d5 added uwuify, but correct (#56)
* added uwuify command

* second try, uwuify: the return

* used the correct api

* added stuttering, repeating words ending with "y" and word replaces

* forgot to add one line lmao

* deleted a test statement

* added more isowo checks

* fixed replace-checking edgecase

* moved around statements - thanks arjix

* fixed another edgecase - gosh my code sucks

* did eslint bs, and added comments

* removed multiple toLowerCase calls
2022-10-08 20:56:38 +02:00
dea34503ef Add more eslint rules 2022-10-08 20:36:57 +02:00
0109381a4f feat(plugin): add quickreply (#61) 2022-10-08 19:27:20 +02:00
8842ad7652 Description change for Spotify Plugin (#63) 2022-10-08 19:26:09 +02:00
73a1bc94d1 fix(lazyWebpack): implement more proxy traps (#65) 2022-10-08 19:23:05 +02:00
ea14bad85d Use GitHub-specific markdown (#67)
See https://github.com/community/community/discussions/16925
2022-10-08 19:21:27 +02:00
f9a682f1c3 docs docs docs docs (#59)
:trollface:
2022-10-08 00:44:12 +01:00
175c1a78f8 fix(plugin): fix stream quality bypass in nitroBypass (#57) 2022-10-06 16:33:30 +02:00
Ven
74c3930e0a add eslint config (#53)
* eslint

* workflow

* lint main
2022-10-06 00:42:58 +02:00
e563521416 Add commands API (#38) 2022-10-06 00:11:32 +02:00
a9e67aa340 pnpm inject/uninject instead of patch/unpatch 2022-10-05 21:58:17 +01:00
25fcc528ea Updater: Inform about more recent commits 2022-10-05 17:09:37 +02:00
443978929b Updater: Exclude more recent local commits 2022-10-05 16:47:28 +02:00
45644dec43 feat(plugin): add clearURLs (#47) 2022-10-05 16:37:49 +02:00
3e0355cb53 Fix consoleShortcuts for web 2022-10-05 14:30:21 +02:00
7e526e4172 Kill unsafe require 2022-10-05 14:30:21 +02:00
98cfa090d4 feat(plugin): add webhookTags (#46)
* feat(plugin): add webhookTags

* fix(plugin): merge same find patches in webhookTags
2022-10-05 04:45:35 +02:00
77aa0c78a0 fix(plugin): add MessageEventsAPI to dependencies of unindent (#48) 2022-10-05 03:33:37 +02:00
e010b2d63e fix(installer): Fix patch conflict with newer pnpm versions (#44) 2022-10-04 22:36:46 +02:00
dafbd39113 Fix ify not preventing auto-pausing without premium (#28) 2022-10-04 22:26:56 +02:00
88542b9ede feat(installer): Implement cross-platform patcher. (#39)
* megu cute
2022-10-04 22:07:34 +02:00
c5e0c7a6e7 fix(randomiseFileNames): work correctly on web (#40)
* fix(randomiseFileNames): work correctly on web

* fix the fix
2022-10-04 21:46:08 +02:00
e1027e06c1 fix(plugins): Fixed BANger (#41)
* fix(plugins): Fixed BANger

That was easy.

* add(Devs): Added Myself to Devs

* Update banger.ts

Co-authored-by: Ven <vendicated@riseup.net>
2022-10-04 21:38:38 +02:00
f1a31a6184 feat: no blocked messages plugin (#34) 2022-10-04 16:46:21 +02:00
8186fe290e gitignore lockfile 2022-10-04 13:23:15 +02:00
a6551957e7 fix NoDevtoolsWarning for web 2022-10-04 13:17:00 +02:00
3a9f692644 Fix Desktop thinking it's web
I forgor to test host after adding web support
2022-10-04 12:57:39 +02:00
e35393b40c feat: no system badges plugin (#33) 2022-10-04 02:36:02 +02:00
Ven
0a2c637c61 Update README.md 2022-10-04 00:57:49 +02:00
cc25753314 feat: Experimental browser support 2022-10-04 00:52:50 +02:00
a9eae106c7 Cleanup deps 2022-10-04 00:52:50 +02:00
f2d913c672 feat(plugin): add betterUploadButton (#32)
plugin to upload with a single click and open the context menu with right click
2022-10-03 20:43:25 +02:00
8fe60971f5 security: remove openPath, restrict openExternal
Now only allows opening http urls.
2022-10-03 19:17:54 +02:00
71a59f4020 Remove electron level telemetry blocking
This is obsolete because the noTrack plugin already does this in
renderer.
2022-10-03 18:56:22 +02:00
Ven
07ed4fa01f Update README.md 2022-10-03 17:03:49 +02:00
Ven
e454ffbfed Update README 2022-10-03 16:43:11 +02:00
d102d5d976 Make NitroBypass only add spaces when there are none (#26) 2022-10-02 22:12:48 +02:00
250 changed files with 21042 additions and 2078 deletions

114
.eslintrc.json Normal file
View File

@ -0,0 +1,114 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser"],
"plugins": [
"@typescript-eslint",
"header",
"simple-import-sort",
"unused-imports",
"path-alias"
],
"settings": {
"import/resolver": {
"alias": {
"map": [
["@webpack", "./src/webpack"],
["@webpack/common", "./src/webpack/common"],
["@utils", "./src/utils"],
["@api", "./src/api"],
["@components", "./src/components"]
]
}
}
},
"rules": {
// Since it's only been a month and Vencord has already been stolen
// by random skids who rebranded it to "AlphaCord" and erased all license
// information
"header/header": [
2,
"block",
[
{
"pattern": "!?",
"template": " "
},
" * Vencord, a modification for Discord's desktop app",
{
"pattern": " \\* Copyright \\(c\\) \\d{4}",
"template": " * Copyright (c) 2022 Vendicated and contributors"
},
" *",
" * This program is free software: you can redistribute it and/or modify",
" * it under the terms of the GNU General Public License as published by",
" * the Free Software Foundation, either version 3 of the License, or",
" * (at your option) any later version.",
" *",
" * This program is distributed in the hope that it will be useful,",
" * but WITHOUT ANY WARRANTY; without even the implied warranty of",
" * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
" * GNU General Public License for more details.",
" *",
" * You should have received a copy of the GNU General Public License",
" * along with this program. If not, see <https://www.gnu.org/licenses/>.",
""
],
2
],
"quotes": ["error", "double", { "avoidEscape": true }],
"jsx-quotes": ["error", "prefer-double"],
"no-mixed-spaces-and-tabs": "error",
"indent": ["error", 4, { "SwitchCase": 1 }],
"arrow-parens": ["error", "as-needed"],
"eol-last": ["error", "always"],
"func-call-spacing": ["error", "never"],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"space-in-parens": ["error", "never"],
"block-spacing": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", { "object": true, "array": false }],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }],
"dot-notation": "error",
"no-useless-escape": "error",
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error"
}
}

13
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# These are supported funding model platforms
github: Vendicated
patreon: Aliucord
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

64
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Build DevBuild
on:
push:
branches:
- main
paths:
- .github/workflows/build.yml
- src/**
- browser/**
- scripts/build/**
- package.json
- pnpm-lock.yaml
env:
FORCE_COLOR: true
jobs:
Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19
uses: actions/setup-node@v3
with:
node-version: 19
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build web
run: pnpm buildWeb --standalone
- name: Sign firefox extension
run: |
pnpx web-ext sign --api-key $WEBEXT_USER --api-secret $WEBEXT_SECRET --channel=unlisted
env:
WEBEXT_USER: ${{ secrets.WEBEXT_USER }}
WEBEXT_SECRET: ${{ secrets.WEBEXT_SECRET }}
- name: Build
run: pnpm build --standalone
- name: Rename extensions for more user friendliness
run: |
mv dist/*.xpi dist/Vencord-for-Firefox.xpi
mv dist/extension-v3.zip dist/Vencord-for-Chrome-and-Edge.zip
rm -rf dist/extension-v2-unpacked
- name: Get some values needed for the release
id: release_values
run: |
echo "release_tag=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Upload Devbuild
run: |
gh release upload devbuild --clobber dist/*
gh release edit devbuild --title "DevBuild $RELEASE_TAG"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ env.release_tag }}

View File

@ -0,0 +1,43 @@
name: Test Patches
on:
workflow_dispatch:
schedule:
# Every day at midnight
- cron: 0 0 * * *
jobs:
TestPlugins:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 19
uses: actions/setup-node@v3
with:
node-version: 19
cache: "pnpm"
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
pnpm add puppeteer
sudo apt-get install -y chromium-browser
- name: Build web
run: pnpm buildWeb --standalone
- name: Create Report
timeout-minutes: 10
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
export CHROMIUM_BIN=$(which chromium-browser)
esbuild test/generateReport.ts > dist/report.mjs
node dist/report.mjs >> $GITHUB_STEP_SUMMARY
env:
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

30
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v2
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint & Test if desktop version compiles
run: pnpm test
- name: Lint & Test if web version compiles
run: pnpm testWeb

21
.gitignore vendored
View File

@ -1,2 +1,23 @@
dist
node_modules
*.exe
vencord_installer
.idea
yarn.lock
package-lock.json
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
*.tsbuildinfo
src/userplugins
ExtensionCache/
settings/

1
.npmrc Normal file
View File

@ -0,0 +1 @@
strict-peer-dependencies=false

View File

@ -1,3 +1,11 @@
{
"recommendations": [ "EditorConfig.EditorConfig" ]
"recommendations": [
"EditorConfig.EditorConfig",
"pmneo.tsimporter",
"dbaeumer.vscode-eslint",
"formulahendry.auto-rename-tag",
"GregorBiswanger.json2ts",
"eamodio.gitlens",
"kamikillerto.vscode-colorize"
]
}

37
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,37 @@
{
// this allows you to debug Vencord from VSCode.
// How to use:
// You need to run Discord via the command line to pass some flags to it.
// If you want to debug the main (node.js) process (preload.ts, ipcMain/*, patcher.ts),
// add the --inspect flag
// To debug the renderer (99% of Vencord), add the --remote-debugging-port=9223 flag
//
// Now launch the desired configuration in VSCode and start Discord with the flags.
// For example, to debug both process, run Electron: All then launch Discord with
// discord --remote-debugging-port=9223 --inspect
"version": "0.2.0",
"configurations": [
{
"name": "Electron: Main",
"type": "node",
"request": "attach",
"port": 9229,
"timeout": 30000
},
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"timeout": 30000,
"webRoot": "${workspaceFolder}/src"
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
}
]
}

16
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double"
}

View File

@ -4,15 +4,16 @@ First of all, thank you for contributing! :3
To ensure your contribution is robust, please follow the below guide!
For a friendly introduction to plugins, see [Megu's Plugin Guide!](docs/2_PLUGINS.md)
## Style Guide
- This project has a very minimal .editorconfig. Make sure your editor supports this!
If you are using VSCode, it should automatically recommend you the extension; If not,
please install the Editorconfig extension
- Try to follow the formatting in the rest of the project and stay consistent
- Follow the file naming convention. File names should usually be camelCase, unless they export a Class
or React Component, in which case they should be PascalCase
- This project has a very minimal .editorconfig. Make sure your editor supports this!
If you are using VSCode, it should automatically recommend you the extension; If not,
please install the Editorconfig extension
- Try to follow the formatting in the rest of the project and stay consistent
- Follow the file naming convention. File names should usually be camelCase, unless they export a Class
or React Component, in which case they should be PascalCase
## Contributing a Plugin
@ -23,7 +24,6 @@ This way we can ensure compatibility and high quality patches.
Follow the below guide to make your first plugin!
### Finding the right module to patch
If the thing you want to patch is an action performed when interacting with a part of the UI, use React DevTools.
@ -50,20 +50,22 @@ This is the regex that will operate on the module found with "find". Just like i
this only matches exactly the part you want to patch and no other parts in the file.
The easiest way to write and test your regex is the following:
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you
see something like `447887: (e,t,n)=>{` (Obviously the number will differ).
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
- Now either test regexes on this string in the console or use a tool like https://regex101.com
- Get the ID of the module you want to patch. To do this, go to it in the sources tab and scroll up until you
see something like `447887: (e,t,n)=>{` (Obviously the number will differ).
- Now paste the following into the console: `Vencord.Webpack.wreq.m[447887].toString()` (Changing the number to your ID)
- Now either test regexes on this string in the console or use a tool like https://regex101.com
Also pay attention to the following:
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
Instead, use one of the following approaches where applicable:
- Match 1 or 2 of any character: `.{1,2}`, for example to match the variable name in `var a=b`, `var (.{1,2})=`
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
`var .{1,2}=([^;]+);`
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
- Never hardcode variable or parameter names or any other minified names. They will change in the future. The only Exception to this rule
are the react props parameter which seems to always be `e`, but even then only rely on this if it is necessary.
Instead, use one of the following approaches where applicable:
- Match 1 or 2 of any character: `.{1,2}`, for example to match the variable name in `var a=b`, `var (.{1,2})=`
- Match any but a guaranteed terminating character: `[^;]+`, for example to match the entire assigned value in `var a=b||c||func();`,
`var .{1,2}=([^;]+);`
- If you don't care about that part, just match a bunch of chars: `.{0,50}`, for example to extract the variable "b" in `createElement("div",{a:"foo",c:"bar"},b)`, `createElement\("div".{0,30},(.{1,2})\),`. Note the `.{0,30}`, this is essentially the same as `.+`, but safer as you can't end up accidently eating thousands of characters
- Additionally, as you might have noticed, all of the appove approaches use regex groups (`(...)`) to capture the variable name. You can then use those groups in your replacement to access those variables dynamically
#### "replace"
@ -75,6 +77,6 @@ and use those in your replacement
Make sure your replacement does not introduce any whitespace. While this might seem weird, random whitespace may mess up other patches.
This includes spaces, tabs and especially newlines
___
---
And that's it! Now open a Pull Request with your Plugin

View File

@ -1,34 +1,58 @@
# Vencord
My own Discord Desktop mod :)
A Discord client mod that does things differently
## Features
- Works on Discord's latest swc update that breaks all other mods
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-21)
- Inline patches: Patch Discord's code with regex replacements! See [the experiments plugin](src/plugins/experiments.ts) for an example. While being more complex, this is more powerful than monkey patching since you can patch only small parts of functions instead of fully replacing them, access non exported/local variables and even replace constants (like in the aforementioned experiments patch!)
- Experiments
- Custom Css: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
- Super easy to install, no git or node or anything else required
- Many plugins built in: [See a list](https://gist.github.com/Vendicated/8696cde7b92548064a3ae92ead84d033)
- Some highlights: SpotifyControls, Experiments, NoTrack, MessageLogger, QuickReply, Free Emotes/Stickers, custom slash commands, ShowHiddenChannels
- Browser Support: Run Vencord in your Browser via extension or UserScript
- Custom CSS and Themes: Inbuilt css editor with support to import any css files (including BetterDiscord themes)
- Works in all Electron versions (Confirmed working on versions 13-23)
## Installing
## Installing / Uninstalling
If you're just a normal user, use [our simple gui installer!](https://github.com/Vendicated/VencordInstaller#usage)
If you're a power user who wants to contribute and make plugins or just want to build from source and install manually, read [Megu's Installation Guide!](docs/1_INSTALLING.md)
## Installing on Browser
Install the browser extension for [![Chrome](https://img.shields.io/badge/chrome-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Chrome-and-Edge.zip), [![Firefox](https://img.shields.io/badge/firefox-ext-brightgreen)](https://github.com/Vendicated/Vencord/releases/latest/download/Vencord-for-Firefox.xpi) or [UserScript](https://github.com/Vendicated/Vencord/releases/download/devbuild/Vencord.user.js). Please note that they aren't automatically updated for now, so you will regularely have to reinstall it.
You may also build them from source, to do that do the same steps as in the manual regular install method,
except run `pnpm buildWeb` instead of `pnpm build`, and your outputs will be in the dist folder
Make sure you have NodeJs and git installed. I will be using pnpm, you can use npm instead
```sh
git clone https://github.com/Vendicated/Vencord
cd Vencord
pnpm i
pnpm build
pnpm buildWeb
```
The builds are now in the dist/ folder (Vencord/dist).
Now install with either the powershell/bash script or use [X1nto's installer](https://github.com/X1nto/VencordInstaller/releases/latest)
You will find the built extension at dist/extension.zip. Now just install this extension in your Browser
## Installing Plugins
> **Note**
> You can only use 3rd party plugins in the manual Vencord install for now.
Vencord comes with a bunch of plugins out of the box!
However, if you want to install your own ones, create a `userplugins` folder in the `src` directory and create or clone your plugins in there.
Don't forget to rebuild!
Want to learn how to create your own plugin, and maybe PR it into Vencord? See the [Contributing](#contributing) section below!
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) and [Megu's Plugin Guide!](docs/2_PLUGINS.md)
[contribute]: CONTRIBUTING.md
[contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute] [contribute]
## Join
[join]: https://discord.gg/D9uwnFnqmd
[join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join] [join]

21
browser/Vencord.ts Normal file
View File

@ -0,0 +1,21 @@
/*!
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./VencordNativeStub";
export * from "../src/Vencord";

View File

@ -0,0 +1,66 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as DataStore from "../src/api/DataStore";
import IpcEvents from "../src/utils/IpcEvents";
// Discord deletes this so need to store in variable
const { localStorage } = window;
// listeners for ipc.on
const listeners = {} as Record<string, Set<Function>>;
const handlers = {
[IpcEvents.GET_REPO]: () => "https://github.com/Vendicated/Vencord", // shrug
[IpcEvents.GET_SETTINGS_DIR]: () => "LocalStorage",
[IpcEvents.GET_QUICK_CSS]: () => DataStore.get("VencordQuickCss").then(s => s ?? ""),
[IpcEvents.SET_QUICK_CSS]: (css: string) => {
DataStore.set("VencordQuickCss", css);
listeners[IpcEvents.QUICK_CSS_UPDATE]?.forEach(l => l(null, css));
},
[IpcEvents.GET_SETTINGS]: () => localStorage.getItem("VencordSettings") || "{}",
[IpcEvents.SET_SETTINGS]: (s: string) => localStorage.setItem("VencordSettings", s),
[IpcEvents.GET_UPDATES]: () => ({ ok: true, value: [] }),
[IpcEvents.OPEN_EXTERNAL]: (url: string) => open(url, "_blank"),
};
function onEvent(event: string, ...args: any[]) {
const handler = handlers[event];
if (!handler) throw new Error(`Event ${event} not implemented.`);
return handler(...args);
}
// probably should make this less cursed at some point
window.VencordNative = {
getVersions: () => ({}),
ipc: {
send: (event: string, ...args: any[]) => void onEvent(event, ...args),
sendSync: onEvent,
on(event: string, listener: () => {}) {
(listeners[event] ??= new Set()).add(listener);
},
off(event: string, listener: () => {}) {
return listeners[event]?.delete(listener);
},
invoke: (event: string, ...args: any[]) => Promise.resolve(onEvent(event, ...args))
},
};

48
browser/background.js Normal file
View File

@ -0,0 +1,48 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Linnea Gräf
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function setContentTypeOnStylesheets(details) {
if (details.type === "stylesheet") {
details.responseHeaders = details.responseHeaders.filter(it => it.name.toLowerCase() !== 'content-type');
details.responseHeaders.push({ name: "Content-Type", value: "text/css" });
}
return { responseHeaders: details.responseHeaders };
}
var cspHeaders = [
"content-security-policy",
"content-security-policy-report-only",
];
function removeCSPHeaders(details) {
return {
responseHeaders: details.responseHeaders.filter(header =>
!cspHeaders.includes(header.name.toLowerCase()))
};
}
browser.webRequest.onHeadersReceived.addListener(
setContentTypeOnStylesheets, { urls: ["https://raw.githubusercontent.com/*"] }, ["blocking", "responseHeaders"]
);
browser.webRequest.onHeadersReceived.addListener(
removeCSPHeaders, { urls: ["https://raw.githubusercontent.com/*", "*://*.discord.com/*"] }, ["blocking", "responseHeaders"]
);

8
browser/content.js Normal file
View File

@ -0,0 +1,8 @@
if (typeof browser === "undefined") {
var browser = chrome;
}
var script = document.createElement("script");
script.src = browser.runtime.getURL("dist/Vencord.js");
// documentElement because we load before body/head are ready
document.documentElement.appendChild(script);

25
browser/manifestv2.json Normal file
View File

@ -0,0 +1,25 @@
{
"manifest_version": 2,
"name": "Vencord Web",
"description": "The Vencord Client Mod for Discord Web.",
"version": "1.0.0",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"permissions": [
"webRequest",
"webRequestBlocking",
"*://*.discord.com/*",
"https://raw.githubusercontent.com/*"
],
"content_scripts": [
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"]
}
],
"web_accessible_resources": ["dist/Vencord.js"],
"background": {
"scripts": ["background.js"]
}
}

40
browser/manifestv3.json Normal file
View File

@ -0,0 +1,40 @@
{
"manifest_version": 3,
"name": "Vencord Web",
"description": "Yeee",
"version": "1.0.0",
"author": "Vendicated",
"homepage_url": "https://github.com/Vendicated/Vencord",
"host_permissions": [
"*://*.discord.com/*",
"https://raw.githubusercontent.com/*"
],
"permissions": ["declarativeNetRequest"],
"content_scripts": [
{
"run_at": "document_start",
"matches": ["*://*.discord.com/*"],
"js": ["content.js"]
}
],
"web_accessible_resources": [
{
"resources": ["dist/Vencord.js"],
"matches": ["*://*.discord.com/*"]
}
],
"declarative_net_request": {
"rule_resources": [
{
"id": "modifyResponseHeaders",
"enabled": true,
"path": "modifyResponseHeaders.json"
}
]
}
}

View File

@ -0,0 +1,38 @@
[
{
"id": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{
"header": "content-security-policy",
"operation": "remove"
},
{
"header": "content-security-policy-report-only",
"operation": "remove"
}
]
},
"condition": {
"resourceTypes": ["main_frame"]
}
},
{
"id": 2,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{
"header": "content-type",
"operation": "set",
"value": "text/css"
}
]
},
"condition": {
"resourceTypes": ["stylesheet"],
"urlFilter": "https://raw.githubusercontent.com/*"
}
}
]

View File

@ -0,0 +1,24 @@
// ==UserScript==
// @name Vencord
// @description A Discord client mod - Web version
// @version %version%
// @author Vendicated (https://github.com/Vendicated)
// @namespace https://github.com/Vendicated/Vencord
// @supportURL https://github.com/Vendicated/Vencord
// @license GPL-3.0
// @match *://*.discord.com/*
// @grant none
// @run-at document-start
// @compatible chrome Chrome + Tampermonkey or Violentmonkey
// @compatible firefox Firefox Tampermonkey
// @compatible opera Opera + Tampermonkey or Violentmonkey
// @compatible edge Edge + Tampermonkey or Violentmonkey
// @compatible safari Safari + Tampermonkey or Violentmonkey
// ==/UserScript==
// this UserScript DOES NOT work on Firefox with Violentmonkey or Greasemonkey due to a bug that makes it impossible
// to overwrite stuff on the window on sites that use CSP. Use Tampermonkey or use a chromium based browser
// https://github.com/violentmonkey/violentmonkey/issues/997
// this is a compiled and minified version of Vencord. For the source code, visit the GitHub repo

123
build.mjs Executable file → Normal file
View File

@ -1,122 +1,3 @@
#!/usr/bin/node
import { execSync } from "child_process";
import esbuild from "esbuild";
import { readdirSync } from "fs";
import { performance } from "perf_hooks";
// FIXME: Delete this soon, for now it is needed so people can update
/**
* @type {esbuild.WatchMode|false}
*/
const watch = process.argv.includes("--watch");
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {esbuild.Plugin}
*/
const makeAllPackagesExternalPlugin = {
name: 'make-all-packages-external',
setup(build) {
let filter = /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
},
};
/**
* @type {esbuild.Plugin}
*/
const globPlugins = {
name: "glob-plugins",
setup: build => {
build.onResolve({ filter: /^plugins$/ }, args => {
return {
namespace: "import-plugins",
path: args.path
};
});
build.onLoad({ filter: /^plugins$/, namespace: "import-plugins" }, () => {
const files = readdirSync("./src/plugins");
let code = "";
let obj = "";
for (let i = 0; i < files.length; i++) {
if (files[i] === "index.ts") {
continue;
}
const mod = `__pluginMod${i}`;
code += `import ${mod} from "./${files[i].replace(/.tsx?$/, "")}";\n`;
obj += `[${mod}.name]: ${mod},`;
}
code += `export default {${obj}}`;
return {
contents: code,
resolveDir: "./src/plugins"
};
});
}
};
const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
/**
* @type {esbuild.Plugin}
*/
const gitHashPlugin = {
name: "git-hash-plugin",
setup: build => {
const filter = /^git-hash$/;
build.onResolve({ filter }, args => ({
namespace: "git-hash", path: args.path
}));
build.onLoad({ filter, namespace: "git-hash" }, () => ({
contents: `export default "${gitHash}"`
}));
}
};
await Promise.all([
esbuild.build({
logLevel: "info",
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
format: "cjs",
bundle: true,
platform: "node",
target: ["esnext"],
sourcemap: "linked",
plugins: [makeAllPackagesExternalPlugin],
watch
}),
esbuild.build({
logLevel: "info",
entryPoints: ["src/patcher.ts"],
outfile: "dist/patcher.js",
bundle: true,
format: "cjs",
target: ["esnext"],
external: ["electron"],
platform: "node",
sourcemap: "linked",
plugins: [makeAllPackagesExternalPlugin],
watch
}),
esbuild.build({
logLevel: "info",
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
format: "iife",
bundle: true,
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer" },
globalName: "Vencord",
external: ["plugins", "git-hash"],
plugins: [
globPlugins,
gitHashPlugin
],
sourcemap: false,
watch,
minify: true,
})
]).catch(err => {
console.error("Build failed");
console.error(err.message);
});
import("./scripts/build/build.mjs");

203
docs/1_INSTALLING.md Normal file
View File

@ -0,0 +1,203 @@
> **Warning**
> These instructions are only for advanced users. If you're not a Developer, you should use our [graphical installer](https://github.com/Vendicated/VencordInstaller#usage) instead.
# Installation Guide
Welcome to Megu's Installation Guide! In this file, you will learn about how to download, install, and uninstall Vencord!
## Sections
- [Installation Guide](#installation-guide)
- [Sections](#sections)
- [Dependencies](#dependencies)
- [Installing Vencord](#installing-vencord)
- [Updating Vencord](#updating-vencord)
- [Uninstalling Vencord](#uninstalling-vencord)
- [Manually Installing Vencord](#manually-installing-vencord)
- [On Windows](#on-windows)
- [On Linux](#on-linux)
- [On MacOS](#on-macos)
- [Manual Patching](#manual-patching)
- [Manually Uninstalling Vencord](#manually-uninstalling-vencord)
## Dependencies
- Install Git from https://git-scm.com/download
- Install Node.JS LTS from here: https://nodejs.dev/en/
## Installing Vencord
> :exclamation: If this doesn't work, see [Manually Installing Vencord](#manually-installing-vencord)
Install `pnpm`:
> :exclamation: this may need to be run as admin depending on your system, and you may need to close and reopen your terminal.
```shell
npm i -g pnpm
```
Clone Vencord:
```shell
git clone https://github.com/Vendicated/Vencord
cd Vencord
```
Install dependencies:
```shell
pnpm install --frozen-lockfile
```
Build Vencord:
```shell
pnpm build
```
Inject vencord into your client:
```shell
pnpm inject
```
Then fully close Discord from your taskbar or task manager, and restart it. Vencord should be injected - you can check this by looking for the Vencord section in Discord settings.
## Updating Vencord
If you're using Discord already, go into the `Updater` tab in settings.
Sometimes it may be neccessary to manually update if the GUI updater fails.
To pull latest changes:
```shell
git pull
```
If this fails, you likely need to reset your local changes to vencord to resolve merge errors:
> :exclamation: This command will remove any local changes you've made to vencord. Make sure you back up if you made any code changes you don't want to lose!
```shell
git reset --hard
git pull
```
and then to build the changes:
```shell
pnpm build
```
Then just refresh your client
## Uninstalling Vencord
Simply run:
```shell
pnpm uninject
```
The above command may ask you to also run:
```shell
pnpm install --frozen-lockfile
pnpm uninject
```
## Manually Installing Vencord
- [Windows](#on-windows)
- [Linux](#on-linux)
- [MacOS](#on-macos)
### On Windows
Press Win+R and enter: `%LocalAppData%` and hit enter. In this page, find the page (Discord, DiscordPTB, DiscordCanary, etc) that you want to patch.
Now follow the instructions at [Manual Patching](#manual-patching)
### On Linux
The Discord folder is usually in one of the following paths:
- /usr/share
- /usr/lib64
- /opt
- /home/$USER/.local/share
If you use flatpak, it will usually be in one of the following paths:
- /var/lib/flatpak/app/com.discordapp.Discord/current/active/files
- /home/$USER/.local/share/flatpak/app/com.discordapp.Discord/current/active/files
You will need to give flatpak access to vencord with one of the following commands:
> :exclamation: If not on stable, replace `com.discordapp.Discord` with your branch name, e.g., `com.discordapp.DiscordCanary`
> :exclamation: Replace `/path/to/vencord/` with the path to your vencord folder (NOT the dist folder)
If Discord flatpak install is in /home/:
```shell
flatpak override --user com.discordapp.Discord --filesystem="/path/to/vencord/"
```
If Discord flatpak install not in /home/:
```shell
sudo flatpak override com.discordapp.Discord --filesystem="/path/to/vencord"
```
Now follow the instructions at [Manual Patching](#manual-patching)
### On MacOS
Open finder and go to your Applications folder. Right-Click on the Discord application you want to patch, and view contents.
Go to the `Contents/Resources` folder.
Now follow the instructions at [Manual Patching](#manual-patching)
### Manual Patching
> :exclamation: If using Flatpak on linux, go to the folder that contains the `app.asar` file, and skip to where we create the `app` folder below.
> :exclamation: On Linux/MacOS, there's a chance there won't be an `app-<number>` folder, but there probably is a `resources` folder, so keep reading :)
Inside there, look for the `app-<number>` folders. If you have multiple, use the highest number. If that doesn't work, do it for the rest of the `app-<number>` folders.
Inside there, go to the `resources` folder. There should be a file called `app.asar`. If there isn't, look at a different `app-<number>` folder instead.
Make a new folder in `resources` called `app`. In here, we will make two files:
`package.json` and `index.js`
In `index.js`:
> :exclamation: Replace the path in the first line with the path to `patcher.js` in your vencord dist folder.
> On Windows, you can get this by shift-rightclicking the patcher.js file and selecting "copy as path"
```js
require("C:/Users/<your user>/path/to/vencord/dist/patcher.js");
require("../app.asar");
```
And in `package.json`:
```json
{ "name": "discord", "main": "index.js" }
```
Finally, fully close & reopen your Discord client and check to see that `Vencord` appears in settings!
### Manually Uninstalling Vencord
> :exclamation: Do not delete `app.asar` - Only delete the `app` folder we created.
Use the instructions above to find the `app` folder, and delete it. Then Close & Reopen Discord.
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

111
docs/2_PLUGINS.md Normal file
View File

@ -0,0 +1,111 @@
# Plugins Guide
Welcome to Megu's Plugin Guide! In this file, you will learn about how to write your own plugin!
You don't need to run `pnpm build` every time you make a change. Instead, use `pnpm watch` - this will auto-compile Vencord whenever you make a change. If using code patches (recommended), you will need to CTRL+R to load the changes.
## Plugin Entrypoint
> If it doesn't already exist, create a folder called `userplugins` in the `src` directory of this repo.
1. Create a folder in `src/userplugins/` with the name of your plugin. For example, `src/userplugins/epicPlugin/` - All of your plugin files will go here.
2. Create a file in that folder called `index.ts`
3. In `index.ts`, copy-paste the following template code:
```ts
import definePlugin from "@utils/types";
export default definePlugin({
name: "Epic Plugin",
description: "This plugin is absolutely epic",
authors: [
{
id: 12345n,
name: "Your Name",
},
],
patches: [],
// Delete these two below if you are only using code patches
start() {},
stop() {},
});
```
Change the name, description, and authors to your own information.
Replace `12345n` with your user ID ending in `n` (e.g., `545581357812678656n`). If you don't want to share your Discord account, use `0n` instead!
## How Plugins Work In Vencord
Vencord uses a different way of making mods than you're used to.
Instead of monkeypatching webpack, we directly modify the code before Discord loads it.
This is _significantly_ more efficient than monkeypatching webpack, and is surprisingly easy, but it may be confusing at first.
## Making your patch
For an in-depth guide into patching code, see [CONTRIBUTING.md](../CONTRIBUTING.md)
in the `index.ts` file we made earlier, you'll see a `patches` array.
> You'll see examples of how patches are used in all the existing plugins, and it'll be easier to understand by looking at those examples, so do that first, and then return here!
> For a good example of a plugin using code patches AND runtime patching, check `src/plugins/unindent.ts`, which uses code patches to run custom runtime code.
One of the patches in the `isStaff` plugin, looks like this:
```ts
{
match: /(\w+)\.isStaff=function\(\){return\s*!1};/,
replace: "$1.isStaff=function(){return true};",
},
```
The above regex matches the string in discord that will look something like:
```js
abc.isStaff = function () {
return !1;
};
```
Remember that Discord code is minified, so there won't be any newlines, and there will only be spaces where necessary. So the source code looks something like:
```
abc.isStaff=function(){return!1;}
```
You can find these snippets by opening the devtools (`ctrl+shift+i`) and pressing `ctrl+shift+f`, searching for what you're looking to modify in there, and beautifying the file to make it more readable.
In the `match` regex in the example shown above, you'll notice at the start there is a `(\w+)`.
Anything in the brackets will be accessible in the `replace` string using `$<number>`. e.g., the first pair of brackets will be `$1`, the second will be `$2`, etc.
The replacement string we used is:
```
"$1.isStaff=function(){return true;};"
```
Which, using the above example, would replace the code with:
> **Note**
> In this example, `$1` becomes `abc`
```js
abc.isStaff = function () {
return true;
};
```
The match value _can_ be a string, rather than regex, however usually regex will be better suited, as it can work with unknown values, whereas strings must be exact matches.
Once you've made your plugin, make sure you run `pnpm test` and make sure your code is nice and clean!
If you want to publish your plugin into the Vencord repo, move your plugin from `src/userplugins` into the `src/plugins` folder and open a PR!
> **Warning**
> Make sure you've read [CONTRIBUTING.md](../CONTRIBUTING.md) before opening a PR
If you need more help, ask in the support channel in our [Discord Server](https://discord.gg/D9uwnFnqmd).

View File

@ -1,90 +0,0 @@
# Vencord Windows Installer
$patcher = "$PWD\dist\patcher.js"
$patcher_safe = $patcher -replace '\\', '\\'
$APP_PATCH = @"
require("$patcher_safe");
require("../app.asar");
"@
$PACKAGE_JSON = @"
{
"main": "index.js",
"name": "discord"
}
"@
$branch_paths = Get-ChildItem -Directory -Path $env:LOCALAPPDATA |
Select-String -Pattern "Discord\w*" -AllMatches |
Select-String -Pattern "DiscordGames" -NotMatch # Ignore DiscordGames folder
$branches = @()
foreach ($branch in $branch_paths) {
$branch = $branch.Line.Split("\")[-1]
if ($branch -eq "Discord") {
$branch = "Discord Stable"
} else {
$branch = $branch.Replace("Discord", "Discord ")
}
$branches = $branches + $branch
}
$branch_count = $branches.Count
Write-Output "Found $branch_count Branches"
Write-Output "====================================="
Write-Output "===== Select a Branch to patch ======"
$i = 0
foreach ($branch in $branches) {
Write-Output "=== $i. $branch"
$i++
}
Write-Output "====================================="
$pos = Read-Host "Enter a number"
if ($null -eq $branches[$pos]) {
Write-Output "Invalid branch selection"
exit
}
$branch = $branches.Get($pos)
$discord_root = $branch_paths.Get($pos)
Write-Output "`nPatching $branch"
$app_folders = Get-ChildItem -Directory -Path $discord_root |
Select-String -Pattern "app-"
foreach ($folder in $app_folders)
{
$version = [regex]::match($folder, 'app-([\d\.]+)').Groups[1].Value
Write-Output "Patching Version $version"
$resources = "$folder\resources"
if (-not(Test-Path -Path "$resources")) {
Write-Error "Resources folder does not exist. Outdated version?`n"
continue
}
if (-not(Test-Path -Path "$resources\app.asar")) {
Write-Error "Failed to find app.asar in $folder`n"
continue
}
$app = "$resources\app"
if (Test-Path -Path $app) {
Write-Error "Are you already patched? App folder already exists at $resources`n"
continue
}
$null = New-Item -Path $app -ItemType Directory
$null = Tee-Object -InputObject $APP_PATCH -FilePath "$app\index.js"
$null = Tee-Object -InputObject $PACKAGE_JSON -FilePath "$app\package.json"
Write-Output "Patched $branch (version $version) successfully"
}

View File

@ -1,75 +0,0 @@
#!/bin/sh
#
# Super simple installer. You should probably run this as root.
# If you are getting permission issues, this is probably why.
#
# If this doesn't work for you, or you're not on Linux, just
# - locate your Discord folder
# - inside the resources folder, create a new folder "app"
# - inside app create the files index.js and package.json.
# See the two tee commands at the end of the file for their contents
patcher="$PWD/dist/patcher.js"
discord_bin="$(which discord)"
discord_actual="$(readlink "$discord_bin")"
if [ -z "$discord_actual" ]; then
case "$(head -n1 "$discord_bin")" in
# has shebang?
\#!/*)
# Wrapper script, assume 2nd line has exec electron call and try to match asar path
path="$(tail -1 "$discord_bin" | grep -Eo "\S+/app.asar" | sed 's/${name}/discord/')"
if [ -z "$path" ]; then
echo "Unsupported Install. $discord_bin is wrapper script but last line isn't exec call?"
exit
elif [ -e "$path" ]; then
discord="$(dirname "$path")"
else
echo "Unsupported Install. $path not found"
exit 1
fi
;;
*)
echo "Unsupported Install. $discord_bin is neither symlink nor a wrapper script.";
exit 1
;;
esac
else
discord="$(dirname "$discord_actual")"
fi
resources="$discord/resources"
app="$resources/app"
app_asar="app.asar"
if [ ! -e "$resources" ]; then
if [ -e "$discord/app.asar.unpacked" ]; then
# System Electron Install
mv "$discord/app.asar" "$discord/_app.asar"
mv "$discord/app.asar.unpacked" "$discord/_app.asar.unpacked"
app="$discord/app.asar"
app_asar="_app.asar"
else
echo "Unsupported Install. $discord has no resources folder but also isn't system electron install"
exit
fi
fi
if [ -e "$app" ]; then
echo "app folder exists. Looks like your Discord is already modified."
exit
fi
mkdir "$app"
tee > "$app/index.js" << EOF
require("$patcher");
require("../$app_asar");
EOF
tee > "$app/package.json" << EOF
{
"main": "index.js",
"name": "discord"
}
EOF

View File

@ -1,19 +1,75 @@
{
"devDependencies": {
"@types/flux": "^3.1.11",
"@types/node": "^18.7.13",
"@types/react": "^18.0.17",
"electron": "^20.1.0",
"esbuild": "^0.15.5"
},
"dependencies": {
"discord-types": "^1.3.26",
"electron-devtools-installer": "^3.2.0",
"jsposed": "^1.0.2",
"prettier": "^2.7.1"
},
"scripts": {
"build": "node build.mjs",
"watch": "node build.mjs --watch"
}
"name": "vencord",
"private": "true",
"version": "1.0.1",
"description": "A Discord client mod that does things differently",
"keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
"url": "https://github.com/Vendicated/Vencord/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Vendicated/Vencord.git"
},
"license": "GPL-3.0",
"author": "Vendicated",
"directories": {
"doc": "docs"
},
"scripts": {
"build": "node scripts/build/build.mjs",
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"inject": "node scripts/patcher/install.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "pnpm lint --fix",
"test": "pnpm lint && pnpm build && pnpm testTsc",
"testWeb": "pnpm lint && pnpm buildWeb && pnpm testTsc",
"testTsc": "tsc --noEmit",
"uninject": "node scripts/patcher/uninstall.js",
"watch": "node scripts/build/build.mjs --watch"
},
"dependencies": {
"fflate": "^0.7.4"
},
"devDependencies": {
"@types/diff": "^5.0.2",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/yazl": "^2.4.2",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"@vap/core": "0.0.12",
"@vap/shiki": "0.10.3",
"console-menu": "^0.1.0",
"diff": "^5.1.0",
"discord-types": "^1.3.26",
"esbuild": "^0.15.16",
"eslint": "^8.28.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"highlight.js": "10.6.0",
"moment": "^2.29.4",
"puppeteer-core": "^19.3.0",
"standalone-electron-types": "^1.0.0",
"type-fest": "^3.3.0",
"typescript": "^4.9.3"
},
"packageManager": "pnpm@7.13.4",
"pnpm": {
"patchedDependencies": {
"eslint-plugin-path-alias@1.0.0": "patches/eslint-plugin-path-alias@1.0.0.patch"
}
},
"webExt": {
"artifactsDir": "./dist",
"build": {
"overwriteDest": true
},
"sourceDir": "./dist/extension-v2-unpacked"
}
}

View File

@ -0,0 +1,13 @@
diff --git a/lib/rules/no-relative.js b/lib/rules/no-relative.js
index 71594c83f1f4f733ffcc6047d7f7084348335dbe..d8623d87c89499c442171db3272cba07c9efabbe 100644
--- a/lib/rules/no-relative.js
+++ b/lib/rules/no-relative.js
@@ -41,7 +41,7 @@ module.exports = {
ImportDeclaration(node) {
const importPath = node.source.value;
- if (!/^(\.?\.\/)/.test(importPath)) {
+ if (!/^(\.\.\/)/.test(importPath)) {
return;
}

2192
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

89
scripts/build/build.mjs Executable file
View File

@ -0,0 +1,89 @@
#!/usr/bin/node
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import esbuild from "esbuild";
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone,
IS_DEV: JSON.stringify(watch)
};
if (defines.IS_STANDALONE === "false")
// If this is a local build (not standalone), optimise
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
/**
* @type {esbuild.BuildOptions}
*/
const nodeCommonOpts = {
...commonOpts,
format: "cjs",
platform: "node",
target: ["esnext"],
minify: true,
bundle: true,
external: ["electron", ...commonOpts.external],
define: defines,
};
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
const sourcemap = watch ? "inline" : "external";
await Promise.all([
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/preload.ts"],
outfile: "dist/preload.js",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
}),
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/patcher.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
}),
esbuild.build({
...commonOpts,
entryPoints: ["src/Vencord.ts"],
outfile: "dist/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap,
plugins: [
globPlugins,
...commonOpts.plugins
],
define: {
...defines,
IS_WEB: false
}
}),
]).catch(err => {
console.error("Build failed");
console.error(err.message);
// make ci fail
if (!commonOpts.watch)
process.exitCode = 1;
});

110
scripts/build/buildWeb.mjs Executable file
View File

@ -0,0 +1,110 @@
#!/usr/bin/node
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import esbuild from "esbuild";
import { zip } from "fflate";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { readFile } from "fs/promises";
import { join, resolve } from "path";
// wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins, watch } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
*/
const commonOptions = {
...commonOpts,
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
external: ["plugins", "git-hash"],
plugins: [
globPlugins,
gitHashPlugin,
gitRemotePlugin,
fileIncludePlugin
],
target: ["esnext"],
define: {
IS_WEB: "true",
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch)
}
};
await Promise.all(
[
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
footer: { js: "//# sourceURL=VencordWeb" },
}),
esbuild.build({
...commonOptions,
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", `${PackageJSON.version}.${new Date().getTime()}`)
},
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(window,'Vencord',{get:()=>Vencord});"
},
})
]
);
async function buildPluginZip(target, files, shouldZip) {
const entries = {
"dist/Vencord.js": readFileSync("dist/browser.js"),
...Object.fromEntries(await Promise.all(files.map(async f => [
(f.startsWith("manifest") ? "manifest.json" : f),
await readFile(join("browser", f))
]))),
};
if (shouldZip) {
zip(entries, {}, (err, data) => {
if (err) {
console.error(err);
process.exitCode = 1;
} else {
writeFileSync("dist/" + target, data);
console.info("Extension written to dist/" + target);
}
});
} else {
if (existsSync(target))
rmSync(target, { recursive: true });
for (const entry in entries) {
const destination = "dist/" + target + "/" + entry;
const parentDirectory = resolve(destination, "..");
mkdirSync(parentDirectory, { recursive: true });
writeFileSync(destination, entries[entry]);
}
console.info("Unpacked Extension written to dist/" + target);
}
}
await buildPluginZip("extension-v3.zip", ["modifyResponseHeaders.json", "content.js", "manifestv3.json"], true);
await buildPluginZip("extension-v2.zip", ["background.js", "content.js", "manifestv2.json"], true);
await buildPluginZip("extension-v2-unpacked", ["background.js", "content.js", "manifestv2.json"], false);

168
scripts/build/common.mjs Normal file
View File

@ -0,0 +1,168 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { exec, execSync } from "child_process";
import { existsSync } from "fs";
import { readdir, readFile } from "fs/promises";
import { join } from "path";
import { promisify } from "util";
export const watch = process.argv.includes("--watch");
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
export const banner = {
js: `
// Vencord ${gitHash}
// Standalone: ${isStandalone}
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
`.trim()
};
// https://github.com/evanw/esbuild/issues/619#issuecomment-751995294
/**
* @type {esbuild.Plugin}
*/
export const makeAllPackagesExternalPlugin = {
name: "make-all-packages-external",
setup(build) {
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
},
};
/**
* @type {esbuild.Plugin}
*/
export const globPlugins = {
name: "glob-plugins",
setup: build => {
const filter = /^~plugins$/;
build.onResolve({ filter }, args => {
return {
namespace: "import-plugins",
path: args.path
};
});
build.onLoad({ filter, namespace: "import-plugins" }, async () => {
const pluginDirs = ["plugins", "userplugins"];
let code = "";
let plugins = "\n";
let i = 0;
for (const dir of pluginDirs) {
if (!existsSync(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file === "index.ts") {
continue;
}
const mod = `p${i}`;
code += `import ${mod} from "./${dir}/${file.replace(/.tsx?$/, "")}";\n`;
plugins += `[${mod}.name]:${mod},\n`;
i++;
}
}
code += `export default {${plugins}};`;
return {
contents: code,
resolveDir: "./src"
};
});
}
};
/**
* @type {esbuild.Plugin}
*/
export const gitHashPlugin = {
name: "git-hash-plugin",
setup: build => {
const filter = /^~git-hash$/;
build.onResolve({ filter }, args => ({
namespace: "git-hash", path: args.path
}));
build.onLoad({ filter, namespace: "git-hash" }, () => ({
contents: `export default "${gitHash}"`
}));
}
};
/**
* @type {esbuild.Plugin}
*/
export const gitRemotePlugin = {
name: "git-remote-plugin",
setup: build => {
const filter = /^~git-remote$/;
build.onResolve({ filter }, args => ({
namespace: "git-remote", path: args.path
}));
build.onLoad({ filter, namespace: "git-remote" }, async () => {
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
const remote = res.stdout.trim()
.replace("https://github.com/", "")
.replace("git@github.com:", "")
.replace(/.git$/, "");
return { contents: `export default "${remote}"` };
});
}
};
/**
* @type {esbuild.Plugin}
*/
export const fileIncludePlugin = {
name: "file-include-plugin",
setup: build => {
const filter = /^~fileContent\/.+$/;
build.onResolve({ filter }, args => ({
namespace: "include-file",
path: args.path,
pluginData: {
path: join(args.resolveDir, args.path.slice("include-file/".length))
}
}));
build.onLoad({ filter, namespace: "include-file" }, async ({ pluginData: { path } }) => {
const [name, format] = path.split(";");
return {
contents: `export default ${JSON.stringify(await readFile(name, format ?? "utf-8"))}`
};
});
}
};
/**
* @type {import("esbuild").BuildOptions}
*/
export const commonOpts = {
logLevel: "info",
bundle: true,
watch,
minify: !watch,
sourcemap: watch ? "inline" : "",
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
external: ["~plugins", "~git-hash", "~git-remote"],
inject: ["./scripts/build/inject/react.mjs"],
jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment",
// Work around https://github.com/evanw/esbuild/issues/2460
tsconfig: "./scripts/build/tsconfig.esbuild.json"
};

View File

@ -0,0 +1,21 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const VencordFragment = Symbol.for("react.fragment");
export let VencordCreateElement =
(...args) => (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View File

@ -0,0 +1,7 @@
// Work around https://github.com/evanw/esbuild/issues/2460
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react"
}
}

62
scripts/genPluginList.js Normal file
View File

@ -0,0 +1,62 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// A script to automatically generate a list of all plugins.
// Just copy paste the entire file into a running Vencord install and it will prompt you
// to save the file
// eslint-disable-next-line spaced-comment
/// <reference types="../src/modules"/>
(() => {
/**
* @type {typeof import("~plugins").default}
*/
const Plugins = Vencord.Plugins.plugins;
const header = `
<!-- This file is auto generated, do not edit -->
# Vencord Plugins
`;
let tableOfContents = "\n\n";
let list = "\n\n";
for (const p of Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name))) {
tableOfContents += `- [${p.name}](#${p.name.replaceAll(" ", "-")})\n`;
list += `## ${p.name}
${p.description}
**Authors**: ${p.authors.map(a => a.name).join(", ")}
`;
if (p.commands?.length) {
list += "\n\n#### Commands\n";
for (const cmd of p.commands) {
list += `${cmd.name} - ${cmd.description}\n\n`;
}
}
list += "\n\n";
}
copy(header + tableOfContents + list);
})();

342
scripts/patcher/common.js Normal file
View File

@ -0,0 +1,342 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const readline = require("readline");
const fs = require("fs");
const menu = require("console-menu");
const BRANCH_NAMES = [
"Discord",
"DiscordPTB",
"DiscordCanary",
"DiscordDevelopment",
"discord",
"discordptb",
"discordcanary",
"discorddevelopment",
"discord-ptb",
"discord-canary",
"discord-development",
// Flatpak
"com.discordapp.Discord",
"com.discordapp.DiscordPTB",
"com.discordapp.DiscordCanary",
"com.discordapp.DiscordDevelopment",
];
const MACOS_DISCORD_DIRS = [
"Discord.app",
"Discord PTB.app",
"Discord Canary.app",
"Discord Development.app",
];
if (process.platform === "linux" && process.env.SUDO_USER) {
process.env.HOME = fs
.readFileSync("/etc/passwd", "utf-8")
.match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0]
.split(":")[5];
}
const LINUX_DISCORD_DIRS = [
"/usr/share",
"/usr/lib64",
"/opt",
`${process.env.HOME}/.local/share`,
`${process.env.HOME}/.dvm`,
"/var/lib/flatpak/app",
`${process.env.HOME}/.local/share/flatpak/app`,
];
const FLATPAK_NAME_MAPPING = {
DiscordCanary: "discord-canary",
DiscordPTB: "discord-ptb",
DiscordDevelopment: "discord-development",
Discord: "discord",
};
const ENTRYPOINT = path
.join(process.cwd(), "dist", "patcher.js")
.replace(/\\/g, "/");
function question(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer);
});
});
}
async function getMenuItem(installations) {
const menuItems = installations.map(info => ({
title: info.patched ? "[MODIFIED] " + info.location : info.location,
info,
}));
const result = await menu(
[
...menuItems,
{ title: "Specify custom path", info: "custom" },
{ title: "Exit without patching", exit: true }
],
{
header: "Select a Discord installation to patch:",
border: true,
helpMessage:
"Use the up/down arrow keys to select an option. " +
"Press ENTER to confirm.",
}
);
if (!result || !result.info || result.exit) {
console.log("No installation selected.");
process.exit(0);
}
if (result.info === "custom") {
const customPath = await question("Please enter the path: ");
if (!customPath || !fs.existsSync(customPath)) {
console.log("No such Path or not specifed.");
process.exit();
}
const resourceDir = path.join(customPath, "resources");
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
console.log("Unsupported Install. resources/app.asar not found");
process.exit();
}
const appDir = path.join(resourceDir, "app");
result.info = {
branch: "unknown",
patched: fs.existsSync(appDir),
location: customPath,
versions: [{
path: appDir,
name: null
}],
arch: process.platform === "linux" ? "linux" : "win32",
isFlatpak: false,
};
}
if (result.info.patched) {
const answer = await question(
"This installation has already been modified. Overwrite? [Y/n]: "
);
if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) {
console.log("Not patching.");
process.exit(0);
}
}
return result.info;
}
function getWindowsDirs() {
const dirs = [];
for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) {
if (!BRANCH_NAMES.includes(dir)) continue;
const location = path.join(process.env.LOCALAPPDATA, dir);
if (!fs.statSync(location).isDirectory()) continue;
const appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(file => file.name.startsWith("app-"))
.map(file => path.join(location, file.name));
const versions = [];
let patched = false;
for (const fqAppDir of appDirs) {
const resourceDir = path.join(fqAppDir, "resources");
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
versions.push({
path: appDir,
name: /app-([0-9.]+)/.exec(fqAppDir)[1],
});
}
if (appDirs.length) {
dirs.push({
branch: dir,
patched,
location,
versions,
arch: "win32",
flatpak: false,
});
}
}
return dirs;
}
function getDarwinDirs() {
const dirs = [];
for (const dir of fs.readdirSync("/Applications")) {
if (!MACOS_DISCORD_DIRS.includes(dir)) continue;
const location = path.join("/Applications", dir, "Contents");
if (!fs.existsSync(location)) continue;
if (!fs.statSync(location).isDirectory()) continue;
const appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(file => file.name.startsWith("Resources"))
.map(file => path.join(location, file.name));
const versions = [];
let patched = false;
for (const resourceDir of appDirs) {
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
versions.push({
path: appDir,
name: null, // MacOS installs have no version number
});
}
if (appDirs.length) {
dirs.push({
branch: dir,
patched,
location,
versions,
arch: "win32",
});
}
}
return dirs;
}
function getLinuxDirs() {
const dirs = [];
for (const dir of LINUX_DISCORD_DIRS) {
if (!fs.existsSync(dir)) continue;
for (const branch of fs.readdirSync(dir)) {
if (!BRANCH_NAMES.includes(branch)) continue;
const location = path.join(dir, branch);
if (!fs.statSync(location).isDirectory()) continue;
const isFlatpak = location.includes("/flatpak/");
let appDirs = [];
if (isFlatpak) {
const fqDir = path.join(location, "current", "active", "files");
if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue;
const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1];
if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) {
continue;
}
const appDir = path.join(
fqDir,
FLATPAK_NAME_MAPPING[branchName]
);
if (!fs.existsSync(appDir)) continue;
if (!fs.statSync(appDir).isDirectory()) continue;
const resourceDir = path.join(appDir, "resources");
appDirs.push(resourceDir);
} else {
appDirs = fs
.readdirSync(location, { withFileTypes: true })
.filter(file => file.isDirectory())
.filter(
file =>
file.name.startsWith("app-") ||
file.name === "resources"
)
.map(file => path.join(location, file.name));
}
const versions = [];
let patched = false;
for (const resourceDir of appDirs) {
if (!fs.existsSync(path.join(resourceDir, "app.asar"))) {
continue;
}
const appDir = path.join(resourceDir, "app");
if (fs.existsSync(appDir)) {
patched = true;
}
const version = /app-([0-9.]+)/.exec(resourceDir);
versions.push({
path: appDir,
name: version && version.length > 1 ? version[1] : null,
});
}
if (appDirs.length) {
dirs.push({
branch,
patched,
location,
versions,
arch: "linux",
isFlatpak,
});
}
}
}
return dirs;
}
module.exports = {
BRANCH_NAMES,
MACOS_DISCORD_DIRS,
LINUX_DISCORD_DIRS,
FLATPAK_NAME_MAPPING,
ENTRYPOINT,
question,
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
};

153
scripts/patcher/install.js Executable file
View File

@ -0,0 +1,153 @@
#!/usr/bin/node
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");
console.log("\nVencord Installer\n");
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
process.exit(1);
}
if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) {
console.log("You need to build the project first. Run:", "pnpm build");
process.exit(1);
}
const {
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
ENTRYPOINT,
question
} = require("./common");
switch (process.platform) {
case "win32":
install(getWindowsDirs());
break;
case "darwin":
install(getDarwinDirs());
break;
case "linux":
install(getLinuxDirs());
break;
default:
console.log("Unknown OS");
break;
}
async function install(installations) {
const selected = await getMenuItem(installations);
// Attempt to give flatpak perms
if (selected.isFlatpak) {
try {
const cwd = process.cwd();
const globalCmd = `flatpak override ${selected.branch} --filesystem=${cwd}`;
const userCmd = `flatpak override --user ${selected.branch} --filesystem=${cwd}`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
console.log("Gave write perms to Discord Flatpak.");
} catch (e) {
console.log("Failed to give write perms to Discord Flatpak.");
console.log(
"Try running this script as an administrator:",
"sudo pnpm inject"
);
process.exit(1);
}
const answer = await question(
`Would you like to allow ${selected.branch} to talk to org.freedesktop.Flatpak?\n` +
"This is essentially full host access but necessary to spawn git. Without it, the updater will not work\n" +
"Consider using the http based updater (using the gui installer) instead if you want to maintain the sandbox.\n" +
"[y/N]: "
);
if (["y", "yes", "yeah"].includes(answer.toLowerCase())) {
try {
const globalCmd = `flatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
const userCmd = `flatpak override --user ${selected.branch} --talk-name=org.freedesktop.Flatpak`;
const cmd = selected.location.startsWith("/home")
? userCmd
: globalCmd;
execSync(cmd);
console.log("Sucessfully gave talk permission");
} catch (err) {
console.error("Failed to give talk permission\n", err);
}
} else {
console.log(`Not giving full host access. If you change your mind later, you can run:\nflatpak override ${selected.branch} --talk-name=org.freedesktop.Flatpak`);
}
}
for (const version of selected.versions) {
const dir = version.path;
// Check if we have write perms to the install directory...
try {
fs.accessSync(selected.location, fs.constants.W_OK);
} catch (e) {
console.error("No write access to", selected.location);
console.error(
"Try running this script as an administrator:",
"sudo pnpm inject"
);
process.exit(1);
}
if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) {
fs.rmSync(dir, { recursive: true });
}
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
path.join(dir, "index.js"),
`require("${ENTRYPOINT}");`
);
fs.writeFileSync(
path.join(dir, "package.json"),
JSON.stringify({
name: "discord",
main: "index.js",
})
);
const requiredFiles = ["index.js", "package.json"];
if (requiredFiles.every(f => fs.existsSync(path.join(dir, f)))) {
console.log(
"Successfully patched",
version.name
? `${selected.branch} ${version.name}`
: selected.branch
);
} else {
console.log("Failed to patch", dir);
console.log("Files in directory:", fs.readdirSync(dir));
}
}
}

78
scripts/patcher/uninstall.js Executable file
View File

@ -0,0 +1,78 @@
#!/usr/bin/node
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const path = require("path");
const fs = require("fs");
console.log("\nVencord Uninstaller\n");
if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) {
console.log("You need to install dependencies first. Run:", "pnpm install --frozen-lockfile");
process.exit(1);
}
const {
getMenuItem,
getWindowsDirs,
getDarwinDirs,
getLinuxDirs,
} = require("./common");
switch (process.platform) {
case "win32":
uninstall(getWindowsDirs());
break;
case "darwin":
uninstall(getDarwinDirs());
break;
case "linux":
uninstall(getLinuxDirs());
break;
default:
console.log("Unknown OS");
break;
}
async function uninstall(installations) {
const selected = await getMenuItem(installations);
for (const version of selected.versions) {
const dir = version.path;
// Check if we have write perms to the install directory...
try {
fs.accessSync(selected.location, fs.constants.W_OK);
} catch (e) {
console.error("No write access to", selected.location);
console.error(
"Try running this script as an administrator:",
"sudo pnpm uninject"
);
process.exit(1);
}
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true });
}
console.log(
"Successfully unpatched",
version.name
? `${selected.branch} ${version.name}`
: selected.branch
);
}
}

View File

@ -0,0 +1,24 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
process.emit = (originalEmit => function (name, data) {
if (name === "warning" && data?.name === "ExperimentalWarning")
return false;
return originalEmit.apply(process, arguments);
})(process.emit);

View File

@ -1,43 +1,77 @@
export * as Plugins from "./plugins";
export * as Webpack from "./webpack";
/*!
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export * as Api from "./api";
export * as Updater from "./utils/updater";
export * as Plugins from "./plugins";
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater";
export * as Webpack from "./webpack";
export { PlainSettings, Settings };
import "./utils/quickCss";
import "./webpack/patchWebpack";
import { popNotice, showNotice } from "./api/Notices";
import { Settings } from "./api/settings";
import { startAllPlugins } from "./plugins";
export { Settings };
import "./webpack/patchWebpack";
import "./utils/quickCss";
import { checkForUpdates, UpdateLogger } from './utils/updater';
import { PlainSettings, Settings } from "./api/settings";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { Router } from "./webpack/common";
export let Components;
export let Components: any;
async function init() {
await onceReady;
startAllPlugins();
Components = await import("./components");
try {
const isOutdated = await checkForUpdates();
if (isOutdated && Settings.notifyAboutUpdates)
setTimeout(() => {
showNotice(
"A Vencord update is available!",
"View Update",
() => {
popNotice();
Router.open("VencordUpdater");
}
);
}, 10000);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
if (!IS_WEB) {
try {
const isOutdated = await checkForUpdates();
if (isOutdated && Settings.notifyAboutUpdates)
setTimeout(() => {
showNotice(
"A Vencord update is available!",
"View Update",
() => {
popNotice();
Router.open("VencordUpdater");
}
);
}, 10000);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
}
if (IS_DEV) {
const pendingPatches = patches.filter(p => !p.all && p.predicate?.() !== false);
if (pendingPatches.length)
PMLogger.warn(
"Webpack has finished initialising, but some patches haven't been applied yet.",
"This might be expected since some Modules are lazy loaded, but please verify",
"that all plugins are working as intended.",
"You are seeing this warning because this is a Development build of Vencord.",
"\nThe following patches have not been applied:",
"\n\n" + pendingPatches.map(p => `${p.plugin}: ${p.find}`).join("\n")
);
}
}

View File

@ -1,5 +1,23 @@
import IPC_EVENTS from './utils/IpcEvents';
import { IpcRenderer, ipcRenderer } from 'electron';
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IPC_EVENTS from "@utils/IpcEvents";
import { IpcRenderer, ipcRenderer } from "electron";
function assertEventAllowed(event: string) {
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
@ -27,14 +45,5 @@ export default {
assertEventAllowed(event);
return ipcRenderer.invoke(event, ...args);
}
},
require(mod: string) {
const settings = ipcRenderer.sendSync(IPC_EVENTS.GET_SETTINGS);
try {
if (!JSON.parse(settings).unsafeRequire) throw "no";
} catch {
throw new Error("Unsafe require is not allowed. Enable it in settings and try again.");
}
return require(mod);
}
};

104
src/api/Badges.ts Normal file
View File

@ -0,0 +1,104 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { User } from "discord-types/general";
import { HTMLProps } from "react";
import Plugins from "~plugins";
export enum BadgePosition {
START,
END
}
export interface ProfileBadge {
/** The tooltip to show on hover */
tooltip: string;
/** The custom image to use */
image?: string;
/** Action to perform when you click the badge */
onClick?(): void;
/** Should the user display this badge? */
shouldShow?(userInfo: BadgeUserArgs): boolean;
/** Optional props (e.g. style) for the badge */
props?: HTMLProps<HTMLImageElement>;
/** Insert at start or end? */
position?: BadgePosition;
/** The badge name to display. Discord uses this, but we don't. */
key?: string;
}
const Badges = new Set<ProfileBadge>();
/**
* Register a new badge with the Badges API
* @param badge The badge to register
*/
export function addBadge(badge: ProfileBadge) {
Badges.add(badge);
}
/**
* Unregister a badge from the Badges API
* @param badge The badge to remove
*/
export function removeBadge(badge: ProfileBadge) {
return Badges.delete(badge);
}
/**
* Inject badges into the profile badges array.
* You probably don't need to use this.
*/
export function inject(badgeArray: ProfileBadge[], args: BadgeUserArgs) {
for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) {
badge.position === BadgePosition.START
? badgeArray.unshift(badge)
: badgeArray.push(badge);
}
}
(Plugins.BadgeAPI as any).addDonorBadge(badgeArray, args.user.id);
return badgeArray;
}
export interface BadgeUserArgs {
user: User;
profile: Profile;
premiumSince: Date;
premiumGuildSince?: Date;
}
interface ConnectedAccount {
type: string;
id: string;
name: string;
verified: boolean;
}
interface Profile {
connectedAccounts: ConnectedAccount[];
premiumType: number;
premiumSince: string;
premiumGuildSince?: any;
lastFetched: number;
profileFetchFailed: boolean;
application?: any;
}

View File

@ -0,0 +1,61 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { mergeDefaults } from "@utils/misc";
import { findByCodeLazy, findByPropsLazy, waitFor } from "@webpack";
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const createBotMessage = findByCodeLazy('username:"Clyde"');
const MessageSender = findByPropsLazy("receiveMessage");
let SnowflakeUtils: any;
waitFor("fromTimestamp", m => SnowflakeUtils = m);
export function generateId() {
return `-${SnowflakeUtils.fromTimestamp(Date.now())}`;
}
/**
* Send a message as Clyde
* @param {string} channelId ID of channel to send message to
* @param {Message} message Message to send
* @returns {Message}
*/
export function sendBotMessage(channelId: string, message: PartialDeep<Message>): Message {
const botMessage = createBotMessage({ channelId, content: "", embeds: [] });
MessageSender.receiveMessage(channelId, mergeDefaults(message, botMessage));
return message as Message;
}
/**
* Get the value of an option by name
* @param args Arguments array (first argument passed to execute)
* @param name Name of the argument
* @param fallbackValue Fallback value in case this option wasn't passed
* @returns Value
*/
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value || fallbackValue) as any;
}

166
src/api/Commands/index.ts Normal file
View File

@ -0,0 +1,166 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { makeCodeblock } from "@utils/misc";
import { sendBotMessage } from "./commandHelpers";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
export * from "./commandHelpers";
export * from "./types";
export let BUILT_IN: Command[];
export const commands = {} as Record<string, Command>;
// hack for plugins being evaluated before we can grab these from webpack
const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
/**
* Optional message option named "message" you can use in commands.
* Used in "tableflip" or "shrug"
* @see {@link RequiredMessageOption}
*/
export let OptionalMessageOption: Option = OptPlaceholder;
/**
* Required message option named "message" you can use in commands.
* Used in "me"
* @see {@link OptionalMessageOption}
*/
export let RequiredMessageOption: Option = ReqPlaceholder;
export const _init = function (cmds: Command[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => c.name === "shrug")!.options![0];
RequiredMessageOption = cmds.find(c => c.name === "me")!.options![0];
} catch (e) {
console.error("Failed to load CommandsApi");
}
return cmds;
} as never;
export const _handleCommand = function (cmd: Command, args: Argument[], ctx: CommandContext) {
if (!cmd.isVencordCommand)
return cmd.execute(args, ctx);
const handleError = (err: any) => {
// TODO: cancel send if cmd.inputType === BUILT_IN_TEXT
const msg = `An Error occurred while executing command "${cmd.name}"`;
const reason = err instanceof Error ? err.stack || err.message : String(err);
console.error(msg, err);
sendBotMessage(ctx.channel.id, {
content: `${msg}:\n${makeCodeblock(reason)}`,
author: {
username: "Vencord"
}
});
};
try {
const res = cmd.execute(args, ctx);
return res instanceof Promise ? res.catch(handleError) : res;
} catch (err) {
return handleError(err);
}
} as never;
/**
* Prepare a Command Option for Discord by filling missing fields
* @param opt
*/
export function prepareOption<O extends Option | Command>(opt: O): O {
opt.displayName ||= opt.name;
opt.displayDescription ||= opt.description;
opt.options?.forEach((opt, i, opts) => {
// See comment above Placeholders
if (opt === OptPlaceholder) opts[i] = OptionalMessageOption;
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
opt.choices?.forEach(x => x.displayName ||= x.name);
prepareOption(opts[i]);
});
return opt;
}
// Yes, Discord registers individual commands for each subcommand
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
// investigate
function registerSubCommands(cmd: Command, plugin: string) {
cmd.options?.forEach(o => {
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
throw new Error("When specifying sub-command options, all options must be sub-commands.");
const subCmd = {
...cmd,
...o,
type: ApplicationCommandType.CHAT_INPUT,
name: `${cmd.name} ${o.name}`,
displayName: `${cmd.name} ${o.name}`,
subCommandPath: [{
name: o.name,
type: o.type,
displayName: o.name
}],
rootCommand: cmd
};
registerCommand(subCmd as any, plugin);
});
}
export function registerCommand<C extends Command>(command: C, plugin: string) {
if (!BUILT_IN) {
console.warn(
"[CommandsAPI]",
`Not registering ${command.name} as the CommandsAPI hasn't been initialised.`,
"Please restart to use commands"
);
return;
}
if (BUILT_IN.some(c => c.name === command.name))
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
command.id ??= `-${BUILT_IN.length + 1}`;
command.applicationId ??= "-1"; // BUILT_IN;
command.type ??= ApplicationCommandType.CHAT_INPUT;
command.inputType ??= ApplicationCommandInputType.BUILT_IN_TEXT;
command.plugin ||= plugin;
prepareOption(command);
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
registerSubCommands(command, plugin);
return;
}
commands[command.name] = command;
BUILT_IN.push(command);
}
export function unregisterCommand(name: string) {
const idx = BUILT_IN.findIndex(c => c.name === name);
if (idx === -1)
return false;
BUILT_IN.splice(idx, 1);
delete commands[name];
return true;
}

104
src/api/Commands/types.ts Normal file
View File

@ -0,0 +1,104 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Channel, Guild } from "discord-types/general";
import { Promisable } from "type-fest";
export interface CommandContext {
channel: Channel;
guild?: Guild;
}
export enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
MENTIONABLE = 9,
NUMBER = 10,
ATTACHMENT = 11,
}
export enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
BOT = 3,
PLACEHOLDER = 4,
}
export interface Option {
name: string;
displayName?: string;
type: ApplicationCommandOptionType;
description: string;
displayDescription?: string;
required?: boolean;
options?: Option[];
choices?: Array<ChoicesOption>;
}
export interface ChoicesOption {
label: string;
value: string;
name: string;
displayName?: string;
}
export enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,
}
export interface CommandReturnValue {
content: string;
/** TODO: implement */
cancel?: boolean;
}
export interface Argument {
type: ApplicationCommandOptionType;
name: string;
value: string;
focused: undefined;
options: Argument[];
}
export interface Command {
id?: string;
applicationId?: string;
type?: ApplicationCommandType;
inputType?: ApplicationCommandInputType;
plugin?: string;
isVencordCommand?: boolean;
name: string;
displayName?: string;
description: string;
displayDescription?: string;
options?: Option[];
predicate?(ctx: CommandContext): boolean;
execute(args: Argument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
}

202
src/api/DataStore/LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

279
src/api/DataStore/index.ts Normal file
View File

@ -0,0 +1,279 @@
/* eslint-disable header/header */
/*!
* idb-keyval v6.2.0
* Copyright 2016, Jake Archibald
* Copyright 2022, Vendicated
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
// @ts-ignore - file size hacks
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-ignore - file size hacks
request.onabort = request.onerror = () => reject(request.error);
});
}
export function createStore(dbName: string, storeName: string): UseStore {
const request = indexedDB.open(dbName);
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
const dbp = promisifyRequest(request);
return (txMode, callback) =>
dbp.then(db =>
callback(db.transaction(storeName, txMode).objectStore(storeName)),
);
}
export type UseStore = <T>(
txMode: IDBTransactionMode,
callback: (store: IDBObjectStore) => T | PromiseLike<T>,
) => Promise<T>;
let defaultGetStoreFunc: UseStore | undefined;
function defaultGetStore() {
if (!defaultGetStoreFunc) {
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
}
return defaultGetStoreFunc;
}
/**
* Get a value by its key.
*
* @param key
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function get<T = any>(
key: IDBValidKey,
customStore = defaultGetStore(),
): Promise<T | undefined> {
return customStore("readonly", store => promisifyRequest(store.get(key)));
}
/**
* Set a value with a key.
*
* @param key
* @param value
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function set(
key: IDBValidKey,
value: any,
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", store => {
store.put(value, key);
return promisifyRequest(store.transaction);
});
}
/**
* Set multiple values at once. This is faster than calling set() multiple times.
* It's also atomic if one of the pairs can't be added, none will be added.
*
* @param entries Array of entries, where each entry is an array of `[key, value]`.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function setMany(
entries: [IDBValidKey, any][],
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", store => {
entries.forEach(entry => store.put(entry[1], entry[0]));
return promisifyRequest(store.transaction);
});
}
/**
* Get multiple values by their keys
*
* @param keys
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function getMany<T = any>(
keys: IDBValidKey[],
customStore = defaultGetStore(),
): Promise<T[]> {
return customStore("readonly", store =>
Promise.all(keys.map(key => promisifyRequest(store.get(key)))),
);
}
/**
* Update a value. This lets you see the old value and update it as an atomic operation.
*
* @param key
* @param updater A callback that takes the old value and returns a new value.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function update<T = any>(
key: IDBValidKey,
updater: (oldValue: T | undefined) => T,
customStore = defaultGetStore(),
): Promise<void> {
return customStore(
"readwrite",
store =>
// Need to create the promise manually.
// If I try to chain promises, the transaction closes in browsers
// that use a promise polyfill (IE10/11).
new Promise((resolve, reject) => {
store.get(key).onsuccess = function () {
try {
store.put(updater(this.result), key);
resolve(promisifyRequest(store.transaction));
} catch (err) {
reject(err);
}
};
}),
);
}
/**
* Delete a particular key from the store.
*
* @param key
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function del(
key: IDBValidKey,
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", store => {
store.delete(key);
return promisifyRequest(store.transaction);
});
}
/**
* Delete multiple keys at once.
*
* @param keys List of keys to delete.
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function delMany(
keys: IDBValidKey[],
customStore = defaultGetStore(),
): Promise<void> {
return customStore("readwrite", (store: IDBObjectStore) => {
keys.forEach((key: IDBValidKey) => store.delete(key));
return promisifyRequest(store.transaction);
});
}
/**
* Clear all values in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function clear(customStore = defaultGetStore()): Promise<void> {
return customStore("readwrite", store => {
store.clear();
return promisifyRequest(store.transaction);
});
}
function eachCursor(
store: IDBObjectStore,
callback: (cursor: IDBCursorWithValue) => void,
): Promise<void> {
store.openCursor().onsuccess = function () {
if (!this.result) return;
callback(this.result);
this.result.continue();
};
return promisifyRequest(store.transaction);
}
/**
* Get all keys in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function keys<KeyType extends IDBValidKey>(
customStore = defaultGetStore(),
): Promise<KeyType[]> {
return customStore("readonly", store => {
// Fast path for modern browsers
if (store.getAllKeys) {
return promisifyRequest(
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
);
}
const items: KeyType[] = [];
return eachCursor(store, cursor =>
items.push(cursor.key as KeyType),
).then(() => items);
});
}
/**
* Get all values in the store.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function values<T = any>(customStore = defaultGetStore()): Promise<T[]> {
return customStore("readonly", store => {
// Fast path for modern browsers
if (store.getAll) {
return promisifyRequest(store.getAll() as IDBRequest<T[]>);
}
const items: T[] = [];
return eachCursor(store, cursor => items.push(cursor.value as T)).then(
() => items,
);
});
}
/**
* Get all entries in the store. Each entry is an array of `[key, value]`.
*
* @param customStore Method to get a custom store. Use with caution (see the docs).
*/
export function entries<KeyType extends IDBValidKey, ValueType = any>(
customStore = defaultGetStore(),
): Promise<[KeyType, ValueType][]> {
return customStore("readonly", store => {
// Fast path for modern browsers
// (although, hopefully we'll get a simpler path some day)
if (store.getAll && store.getAllKeys) {
return Promise.all([
promisifyRequest(
store.getAllKeys() as unknown as IDBRequest<KeyType[]>,
),
promisifyRequest(store.getAll() as IDBRequest<ValueType[]>),
]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));
}
const items: [KeyType, ValueType][] = [];
return customStore("readonly", store =>
eachCursor(store, cursor =>
items.push([cursor.key as KeyType, cursor.value]),
).then(() => items),
);
});
}

View File

@ -0,0 +1,59 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export type AccessoryCallback = (props: Record<string, any>) => JSX.Element;
export type Accessory = {
callback: AccessoryCallback;
position?: number;
};
export const accessories = new Map<String, Accessory>();
export function addAccessory(
identifier: string,
callback: AccessoryCallback,
position?: number
) {
accessories.set(identifier, {
callback,
position,
});
}
export function removeAccessory(identifier: string) {
accessories.delete(identifier);
}
export function _modifyAccessories(
elements: JSX.Element[],
props: Record<string, any>
) {
for (const accessory of accessories.values()) {
elements.splice(
accessory.position != null
? accessory.position < 0
? elements.length + accessory.position
: accessory.position
: elements.length,
0,
accessory.callback(props)
);
}
return elements;
}

View File

@ -1,5 +1,24 @@
import type { Message, Channel } from 'discord-types/general';
import Logger from '../utils/logger';
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
import { MessageStore } from "@webpack/common";
import type { Channel, Message } from "discord-types/general";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
@ -18,25 +37,37 @@ export interface MessageObject {
validNonShortcutEmojis: Emoji[];
}
export type SendListener = (channelId: string, messageObj: MessageObject, extra: any) => void;
export interface MessageExtra {
stickerIds?: string[];
}
export type SendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => void | { cancel: boolean; };
export type EditListener = (channelId: string, messageId: string, messageObj: MessageObject) => void;
const sendListeners = new Set<SendListener>();
const editListeners = new Set<EditListener>();
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: any) {
export function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra) {
for (const listener of sendListeners) {
try {
listener(channelId, messageObj, extra);
} catch (e) { MessageEventsLogger.error(`MessageSendHandler: Listener encoutered an unknown error. (${e})`); }
const result = listener(channelId, messageObj, extra);
if (result && result.cancel === true) {
return true;
}
} catch (e) {
MessageEventsLogger.error("MessageSendHandler: Listener encountered an unknown error\n", e);
}
}
return false;
}
export function _handlePreEdit(channeld: string, messageId: string, messageObj: MessageObject) {
export function _handlePreEdit(channelId: string, messageId: string, messageObj: MessageObject) {
for (const listener of editListeners) {
try {
listener(channeld, messageId, messageObj);
} catch (e) { MessageEventsLogger.error(`MessageEditHandler: Listener encoutered an unknown error. (${e})`); }
listener(channelId, messageId, messageObj);
} catch (e) {
MessageEventsLogger.error("MessageEditHandler: Listener encountered an unknown error\n", e);
}
}
}
@ -67,11 +98,15 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
const listeners = new Set<ClickListener>();
export function _handleClick(message, channel, event) {
export function _handleClick(message: Message, channel: Channel, event: MouseEvent) {
// message object may be outdated, so (try to) fetch latest one
message = MessageStore.getMessage(channel.id, message.id) ?? message;
for (const listener of listeners) {
try {
listener(message, channel, event);
} catch (e) { MessageEventsLogger.error(`MessageClickHandler: Listener encoutered an unknown error. (${e})`); }
} catch (e) {
MessageEventsLogger.error("MessageClickHandler: Listener encountered an unknown error\n", e);
}
}
}

69
src/api/MessagePopover.ts Normal file
View File

@ -0,0 +1,69 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
import { Channel, Message } from "discord-types/general";
import type { MouseEventHandler } from "react";
const logger = new Logger("MessagePopover");
export interface ButtonItem {
key?: string,
label: string,
icon: React.ComponentType<any>,
message: Message,
channel: Channel,
onClick?: MouseEventHandler<HTMLButtonElement>,
onContextMenu?: MouseEventHandler<HTMLButtonElement>;
}
export type getButtonItem = (message: Message) => ButtonItem | null;
export const buttons = new Map<string, getButtonItem>();
export function addButton(
identifier: string,
item: getButtonItem,
) {
buttons.set(identifier, item);
}
export function removeButton(identifier: string) {
buttons.delete(identifier);
}
export function _buildPopoverElements(
msg: Message,
makeButton: (item: ButtonItem) => React.ComponentType
) {
const items = [] as React.ComponentType[];
for (const [identifier, getItem] of buttons.entries()) {
try {
const item = getItem(msg);
if (item) {
item.key ??= identifier;
items.push(makeButton(item));
}
} catch (err) {
logger.error(`[${identifier}]`, err);
}
}
return items;
}

View File

@ -1,4 +1,22 @@
import { waitFor } from "../webpack";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { waitFor } from "@webpack";
let NoticesModule: any;
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);

55
src/api/ServerList.ts Normal file
View File

@ -0,0 +1,55 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
const logger = new Logger("ServerListAPI");
export enum ServerListRenderPosition {
Above,
In,
}
const renderFunctionsAbove = new Set<Function>();
const renderFunctionsIn = new Set<Function>();
function getRenderFunctions(position: ServerListRenderPosition) {
return position === ServerListRenderPosition.Above ? renderFunctionsAbove : renderFunctionsIn;
}
export function addServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
getRenderFunctions(position).add(renderFunction);
}
export function removeServerListElement(position: ServerListRenderPosition, renderFunction: Function) {
getRenderFunctions(position).delete(renderFunction);
}
export const renderAll = (position: ServerListRenderPosition) => {
const ret: Array<JSX.Element> = [];
for (const renderFunction of getRenderFunctions(position)) {
try {
ret.unshift(renderFunction());
} catch (e) {
logger.error("Failed to render server list element:", e);
}
}
return ret;
};

View File

@ -1,2 +1,76 @@
export * as MessageEvents from "./MessageEvents";
export * as Notices from "./Notices";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as $Badges from "./Badges";
import * as $Commands from "./Commands";
import * as $DataStore from "./DataStore";
import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
import * as $Notices from "./Notices";
import * as $ServerList from "./ServerList";
/**
* An API allowing you to listen to Message Clicks or run your own logic
* before a message is sent
*
* If your plugin uses this, you must add MessageEventsAPI to its dependencies
*/
const MessageEvents = $MessageEventsAPI;
/**
* An API allowing you to create custom notices
* (snackbars on the top, like the Update prompt)
*/
const Notices = $Notices;
/**
* An API allowing you to register custom commands
*/
const Commands = $Commands;
/**
* A wrapper around IndexedDB. This can store arbitrarily
* large data and supports a lot of datatypes (Blob, Map, ...).
* For a full list, see the mdn link below
*
* This should always be preferred over the Settings API if possible, as
* localstorage has very strict size restrictions and blocks the event loop
*
* Make sure your keys are unique (tip: prefix them with ur plugin name)
* and please clean up no longer needed entries.
*
* This is actually just idb-keyval, so if you're familiar with that, you're golden!
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types}
*/
const DataStore = $DataStore;
/**
* An API allowing you to add custom components as message accessories
*/
const MessageAccessories = $MessageAccessories;
/**
* An API allowing you to add custom buttons in the message popover
*/
const MessagePopover = $MessagePopover;
/**
* An API allowing you to add badges to user profiles
*/
const Badges = $Badges;
/**
* An API allowing you to add custom elements to the server list
*/
const ServerList = $ServerList;
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };

View File

@ -1,12 +1,35 @@
import plugins from "plugins";
import IpcEvents from "../utils/IpcEvents";
import { React } from "../webpack/common";
import { mergeDefaults } from '../utils/misc';
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
interface Settings {
import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger";
import { mergeDefaults } from "@utils/misc";
import { OptionType } from "@utils/types";
import { React } from "@webpack/common";
import plugins from "~plugins";
const logger = new Logger("Settings");
export interface Settings {
notifyAboutUpdates: boolean;
unsafeRequire: boolean;
useQuickCss: boolean;
enableReactDevtools: boolean;
themeLinks: string[];
plugins: {
[plugin: string]: {
enabled: boolean;
@ -17,58 +40,100 @@ interface Settings {
const DefaultSettings: Settings = {
notifyAboutUpdates: true,
unsafeRequire: false,
useQuickCss: true,
themeLinks: [],
enableReactDevtools: false,
plugins: {}
};
for (const plugin in plugins) {
DefaultSettings.plugins[plugin] = {
enabled: plugins[plugin].required ?? false
};
}
try {
var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings;
for (const key in DefaultSettings) {
settings[key] ??= DefaultSettings[key];
}
mergeDefaults(settings, DefaultSettings);
} catch (err) {
console.error("Corrupt settings file. ", err);
var settings = mergeDefaults({} as Settings, DefaultSettings);
logger.error("An error occurred while loading the settings. Corrupt settings file?\n", err);
}
type SubscriptionCallback = ((newValue: any, path: string) => void) & { _path?: string; };
const subscriptions = new Set<SubscriptionCallback>();
function makeProxy(settings: Settings, root = settings, path = ""): Settings {
return new Proxy(settings, {
const proxyCache = {} as Record<string, any>;
// Wraps the passed settings object in a Proxy to nicely handle change listeners and default values
function makeProxy(settings: any, root = settings, path = ""): Settings {
return proxyCache[path] ??= new Proxy(settings, {
get(target, p: string) {
const v = target[p];
if (typeof v === "object" && !Array.isArray(v))
// using "in" is important in the following cases to properly handle falsy or nullish values
if (!(p in target)) {
// Return empty for plugins with no settings
if (path === "plugins" && p in plugins)
return target[p] = makeProxy({
enabled: plugins[p].required ?? false
}, root, `plugins.${p}`);
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
// the default value.
if (path.startsWith("plugins.")) {
const plugin = path.slice("plugins.".length);
if (plugin in plugins) {
const setting = plugins[plugin].options?.[p];
if (!setting) return v;
if ("default" in setting)
// normal setting with a default value
return (target[p] = setting.default);
if (setting.type === OptionType.SELECT) {
const def = setting.options.find(o => o.default);
if (def)
target[p] = def.value;
return def?.value;
}
}
}
return v;
}
// Recursively proxy Objects with the updated property path
if (typeof v === "object" && !Array.isArray(v) && v !== null)
return makeProxy(v, root, `${path}${path && "."}${p}`);
// primitive or similar, no need to proxy further
return v;
},
set(target, p: string, v) {
// avoid unnecessary updates to React Components and other listeners
if (target[p] === v) return true;
target[p] = v;
// Call any listeners that are listening to a setting of this path
const setPath = `${path}${path && "."}${p}`;
for (const subscription of subscriptions) {
if (!subscription._path || subscription._path === setPath) {
subscription(v, setPath);
}
}
// And don't forget to persist the settings!
VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root, null, 4));
return true;
}
});
}
/**
* Same as {@link Settings} but unproxied. You should treat this as readonly,
* as modifying properties on this will not save to disk or call settings
* listeners.
* WARNING: default values specified in plugin.options will not be ensured here. In other words,
* settings for which you specified a default value may be uninitialised. If you need proper
* handling for default values, use {@link Settings}
*/
export const PlainSettings = settings;
/**
* A smart settings object. Altering props automagically saves
* the updated settings to disk.
* This recursively proxies objects. If you need the object non proxied, use {@link PlainSettings}
*/
export const Settings = makeProxy(settings);
@ -76,14 +141,19 @@ export const Settings = makeProxy(settings);
* Settings hook for React components. Returns a smart settings
* object that automagically triggers a rerender if any properties
* are altered
* @param paths An optional list of paths to whitelist for rerenders
* @returns Settings
*/
export function useSettings() {
export function useSettings(paths?: string[]) {
const [, forceUpdate] = React.useReducer(() => ({}), {});
const onUpdate: SubscriptionCallback = paths
? (value, path) => paths.includes(path) && forceUpdate()
: forceUpdate;
React.useEffect(() => {
subscriptions.add(forceUpdate);
return () => void subscriptions.delete(forceUpdate);
subscriptions.add(onUpdate);
return () => void subscriptions.delete(onUpdate);
}, []);
return Settings;
@ -110,3 +180,21 @@ export function addSettingsListener(path: string, onUpdate: (newValue: any, path
(onUpdate as SubscriptionCallback)._path = path;
subscriptions.add(onUpdate);
}
export function migratePluginSettings(name: string, ...oldNames: string[]) {
const { plugins } = settings;
if (name in plugins) return;
for (const oldName of oldNames) {
if (oldName in plugins) {
logger.info(`Migrating settings from old name ${oldName} to ${name}`);
plugins[name] = plugins[oldName];
delete plugins[oldName];
VencordNative.ipc.invoke(
IpcEvents.SET_SETTINGS,
JSON.stringify(settings, null, 4)
);
break;
}
}
}

View File

@ -0,0 +1,68 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React, TextInput } from "@webpack/common";
// TODO: Refactor settings to use this as well
interface TextInputProps {
/**
* WARNING: Changing this between renders will have no effect!
*/
value: string;
/**
* This will only be called if the new value passed validate()
*/
onChange(newValue: string): void;
/**
* Optionally validate the user input
* Return true if the input is valid
* Otherwise, return a string containing the reason for this input being invalid
*/
validate(v: string): true | string;
}
/**
* A very simple wrapper around Discord's TextInput that validates input and shows
* the user an error message and only calls your onChange when the input is valid
*/
export function CheckedTextInput({ value: initialValue, onChange, validate }: TextInputProps) {
const [value, setValue] = React.useState(initialValue);
const [error, setError] = React.useState<string>();
function handleChange(v: string) {
setValue(v);
const res = validate(v);
if (res === true) {
setError(void 0);
onChange(v);
} else {
setError(res);
}
}
return (
<>
<TextInput
type="text"
value={value}
onChange={handleChange}
error={error}
/>
</>
);
}

View File

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IpcEvents from "@utils/IpcEvents";
import { Button } from "@webpack/common";
import { Heart } from "./Heart";
export default function DonateButton(props: any) {
return (
<Button
{...props}
look={Button.Looks.LINK}
color={Button.Colors.TRANSPARENT}
onClick={() =>
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated")
}
>
<Heart />
Donate
</Button>
);
}

View File

@ -1,10 +1,36 @@
import Logger from "../utils/logger";
import { Card, React } from "../webpack/common";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
interface Props {
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
/** Render nothing if an error occurs */
noop?: boolean;
/** Fallback component to render if an error occurs */
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; message: string; stack: string; }>>;
/** called when an error occurs */
onError?(error: Error, errorInfo: React.ErrorInfo): void;
/** Custom error message */
message?: string;
}
const color = "#e78284";
@ -13,57 +39,77 @@ const logger = new Logger("React ErrorBoundary", color);
const NO_ERROR = {};
export default class ErrorBoundary extends React.Component<React.PropsWithChildren<Props>> {
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
return (props) => (
<ErrorBoundary>
<Component {...props as any/* I hate react typings ??? */} />
</ErrorBoundary>
);
}
// We might want to import this in a place where React isn't ready yet.
// Thus, wrap in a LazyComponent
const ErrorBoundary = LazyComponent(() => {
return class ErrorBoundary extends React.PureComponent<React.PropsWithChildren<Props>> {
state = {
error: NO_ERROR as any,
stack: "",
message: ""
};
state = {
error: NO_ERROR as any,
message: ""
static getDerivedStateFromError(error: any) {
let stack = error?.stack ?? "";
let message = error?.message || String(error);
if (error instanceof Error && stack) {
const eolIdx = stack.indexOf("\n");
if (eolIdx !== -1) {
message = stack.slice(0, eolIdx);
stack = stack.slice(eolIdx + 1).replace(/https:\/\/\S+\/assets\//g, "");
}
}
return { error, stack, message };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.(error, errorInfo);
logger.error("A component threw an Error\n", error);
logger.error("Component Stack", errorInfo.componentStack);
}
render() {
if (this.state.error === NO_ERROR) return this.props.children;
if (this.props.noop) return null;
if (this.props.fallback)
return <this.props.fallback
children={this.props.children}
{...this.state}
/>;
const msg = this.props.message || "An error occurred while rendering this Component. More info can be found below and in your console.";
return (
<ErrorCard style={{
overflow: "hidden",
}}>
<h1>Oh no!</h1>
<p>{msg}</p>
<code>
{this.state.message}
{!!this.state.stack && (
<pre className={Margins.marginTop8}>
{this.state.stack}
</pre>
)}
</code>
</ErrorCard>
);
}
};
}) as
React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
};
static getDerivedStateFromError(error: any) {
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
);
return {
error: error?.stack?.replace(/https:\/\/\S+\/assets\//g, "") || error?.message || String(error)
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.props.onError?.(error, errorInfo);
logger.error("A component threw an Error\n", error);
logger.error("Component Stack", errorInfo.componentStack);
}
render() {
if (this.state.error === NO_ERROR) return this.props.children;
if (this.props.fallback)
return <this.props.fallback
children={this.props.children}
error={this.state.error}
/>;
return (
<ErrorCard style={{
overflow: "hidden",
}}>
<h1>Oh no!</h1>
<p>
An error occurred while rendering this Component. More info can be found below
and in your console.
</p>
<code>
<pre>
{this.state.error}
</pre>
</code>
</ErrorCard>
);
}
}
export default ErrorBoundary;

View File

@ -1,4 +1,22 @@
import { Card } from "../webpack/common";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Card } from "@webpack/common";
interface Props {
style?: React.CSSProperties;

View File

@ -1,15 +1,34 @@
import { PropsWithChildren } from "react";
import type { React } from '../webpack/common';
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { React } from "@webpack/common";
export function Flex(props: React.PropsWithChildren<{
flexDirection?: React.CSSProperties["flexDirection"];
style?: React.CSSProperties;
className?: string;
}>) {
} & React.HTMLProps<HTMLDivElement>>) {
props.style ??= {};
props.style.flexDirection ||= props.flexDirection;
props.style.gap ??= "1em";
props.style.display = "flex";
// TODO(ven): Remove me, what was I thinking??
props.style.gap ??= "1em";
props.style.flexDirection ||= props.flexDirection;
delete props.flexDirection;
return (
<div {...props}>
{props.children}

35
src/components/Heart.tsx Normal file
View File

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function Heart() {
return (
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
width="16"
style={{ marginRight: "0.5em", transform: "translateY(2px)" }}
>
<path
fill="#db61a2"
fill-rule="evenodd"
d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"
/>
</svg>
);
}

View File

@ -1,18 +1,35 @@
import { React } from "../webpack/common";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
interface Props {
href: string;
import { React } from "@webpack/common";
interface Props extends React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> {
disabled?: boolean;
style?: React.CSSProperties;
}
export function Link(props: React.PropsWithChildren<Props>) {
if (props.disabled) {
props.style ??= {};
props.style.pointerEvents = "none";
props["aria-disabled"] = true;
}
return (
<a href={props.href} target="_blank" style={props.style}>
<a role="link" target="_blank" {...props}>
{props.children}
</a>
);

44
src/components/Monaco.ts Normal file
View File

@ -0,0 +1,44 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { find } from "@webpack";
import monacoHtml from "~fileContent/monacoWin.html";
const queue = new Queue();
const setCss = debounce((css: string) => {
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
});
export async function launchMonacoEditor() {
const win = open("about:blank", void 0, "popup,width=1000,height=1000")!;
win.setCss = setCss;
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
win.getTheme = () =>
find(m =>
m.ProtoClass?.typeName.endsWith("PreloadedUserSettings")
)?.getCurrentValue()?.appearance?.theme === 2
? "vs-light"
: "vs-dark";
win.document.write(monacoHtml);
}

View File

@ -0,0 +1,297 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { debounce } from "@utils/debounce";
import { makeCodeblock } from "@utils/misc";
import { search } from "@webpack";
import { Button, Clipboard, Forms, Margins, Parser, React, Switch, Text, TextInput } from "@webpack/common";
import { CheckedTextInput } from "./CheckedTextInput";
import ErrorBoundary from "./ErrorBoundary";
// Do not include diff in non dev builds (side effects import)
if (IS_DEV) {
var differ = require("diff") as typeof import("diff");
}
const findCandidates = debounce(function ({ find, setModule, setError }) {
const candidates = search(find);
const keys = Object.keys(candidates);
const len = keys.length;
if (len === 0)
setError("No match. Perhaps that module is lazy loaded?");
else if (len !== 1)
setError("Multiple matches. Please refine your filter");
else
setModule([keys[0], candidates[keys[0]]]);
});
function ReplacementComponent({ module, match, replacement, setReplacementError }) {
const [id, fact] = module;
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", "");
try {
var patched = src.replace(match, replacement);
setReplacementError(void 0);
} catch (e) {
setReplacementError((e as Error).message);
return ["", [], []];
}
const m = src.match(match);
return [patched, m, makeDiff(src, patched, m)];
}, [id, match, replacement]);
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
if (!match || original === patched) return null;
const changeSize = patched.length - original.length;
// Use 200 surrounding characters of context
const start = Math.max(0, match.index! - 200);
const end = Math.min(original.length, match.index! + match[0].length + 200);
// (changeSize may be negative)
const endPatched = end + changeSize;
const context = original.slice(start, end);
const patchedContext = patched.slice(start, endPatched);
return differ.diffWordsWithSpace(context, patchedContext);
}
function renderMatch() {
if (!matchResult)
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
const groups = matchResult.length > 1
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
: "";
return (
<>
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
</>
);
}
function renderDiff() {
return diff?.map(p => {
const color = p.added ? "lime" : p.removed ? "red" : "grey";
return <div style={{ color, userSelect: "text" }}>{p.value}</div>;
});
}
return (
<>
<Forms.FormTitle>Module {id}</Forms.FormTitle>
{!!matchResult?.[0]?.length && (
<>
<Forms.FormTitle>Match</Forms.FormTitle>
{renderMatch()}
</>)
}
{!!diff?.length && (
<>
<Forms.FormTitle>Diff</Forms.FormTitle>
{renderDiff()}
</>
)}
{!!diff?.length && (
<Button className={Margins.marginTop20} onClick={() => {
try {
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
setCompileResult([true, "Compiled successfully"]);
} catch (err) {
setCompileResult([false, (err as Error).message]);
}
}}>Compile</Button>
)}
{compileResult &&
<Forms.FormText style={{ color: compileResult[0] ? "var(--text-positive)" : "var(--text-danger)" }}>
{compileResult[1]}
</Forms.FormText>
}
</>
);
}
function ReplacementInput({ replacement, setReplacement, replacementError }) {
const [isFunc, setIsFunc] = React.useState(false);
const [error, setError] = React.useState<string>();
function onChange(v: string) {
setError(void 0);
if (isFunc) {
try {
const func = (0, eval)(v);
if (typeof func === "function")
setReplacement(() => func);
else
setError("Replacement must be a function");
} catch (e) {
setReplacement(v);
setError((e as Error).message);
}
} else {
setReplacement(v);
}
}
React.useEffect(
() => void (isFunc ? onChange(replacement) : setError(void 0)),
[isFunc]
);
return (
<>
<Forms.FormTitle>replacement</Forms.FormTitle>
<TextInput
value={replacement?.toString()}
onChange={onChange}
error={error ?? replacementError}
/>
{!isFunc && (
<>
<Forms.FormTitle>Cheat Sheet</Forms.FormTitle>
{Object.entries({
"$$": "Insert a $",
"$&": "Insert the entire match",
"$`": "Insert the substring before the match",
"$'": "Insert the substring after the match",
"$n": "Insert the nth capturing group ($1, $2...)"
}).map(([placeholder, desc]) => (
<Forms.FormText key={placeholder}>
{Parser.parse("`" + placeholder + "`")}: {desc}
</Forms.FormText>
))}
</>
)}
<Switch
className={Margins.marginTop8}
value={isFunc}
onChange={setIsFunc}
note="'replacement' will be evaled if this is toggled"
hideBorder={true}
>
Treat as Function
</Switch>
</>
);
}
function PatchHelper() {
const [find, setFind] = React.useState<string>("");
const [match, setMatch] = React.useState<string>("");
const [replacement, setReplacement] = React.useState<string | Function>("");
const [replacementError, setReplacementError] = React.useState<string>();
const [module, setModule] = React.useState<[number, Function]>();
const [findError, setFindError] = React.useState<string>();
const code = React.useMemo(() => {
return `
{
find: ${JSON.stringify(find)},
replacement: {
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
}
}
`.trim();
}, [find, match, replacement]);
function onFindChange(v: string) {
setFindError(void 0);
setFind(v);
if (v.length) {
findCandidates({ find: v, setModule, setError: setFindError });
}
}
function onMatchChange(v: string) {
try {
new RegExp(v);
setFindError(void 0);
setMatch(v);
} catch (e: any) {
setFindError((e as Error).message);
}
}
return (
<Forms.FormSection>
<Text variant="heading-md/normal" tag="h2" className={Margins.marginBottom8}>Patch Helper</Text>
<Forms.FormTitle>find</Forms.FormTitle>
<TextInput
type="text"
value={find}
onChange={onFindChange}
error={findError}
/>
<Forms.FormTitle>match</Forms.FormTitle>
<CheckedTextInput
value={match}
onChange={onMatchChange}
validate={v => {
try {
return (new RegExp(v), true);
} catch (e) {
return (e as Error).message;
}
}}
/>
<ReplacementInput
replacement={replacement}
setReplacement={setReplacement}
replacementError={replacementError}
/>
<Forms.FormDivider />
{module && (
<ReplacementComponent
module={module}
match={new RegExp(match)}
replacement={replacement}
setReplacementError={setReplacementError}
/>
)}
{!!(find && match && replacement) && (
<>
<Forms.FormTitle className={Margins.marginTop20}>Code</Forms.FormTitle>
<div style={{ userSelect: "text" }}>{Parser.parse(makeCodeblock(code, "ts"))}</div>
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
</>
)}
</Forms.FormSection>
);
}
export default IS_DEV ? ErrorBoundary.wrap(PatchHelper) : null;

View File

@ -0,0 +1,239 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { generateId } from "@api/Commands";
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { LazyComponent } from "@utils/misc";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { proxyLazy } from "@utils/proxyLazy";
import { OptionType, Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "@webpack/common";
import { User } from "discord-types/general";
import { Constructor } from "type-fest";
import {
ISettingElementProps,
SettingBooleanComponent,
SettingCustomComponent,
SettingNumericComponent,
SettingSelectComponent,
SettingSliderComponent,
SettingTextComponent
} from "./components";
const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");
const UserRecord: Constructor<Partial<User>> = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
interface PluginModalProps extends ModalProps {
plugin: Plugin;
onRestartNeeded(): void;
}
/** To stop discord making unwanted requests... */
function makeDummyUser(user: { name: string, id: BigInt; }) {
const newUser = new UserRecord({
username: user.name,
id: generateId(),
bot: true,
});
FluxDispatcher.dispatch({
type: "USER_UPDATE",
user: newUser,
});
return newUser;
}
const Components: Record<OptionType, React.ComponentType<ISettingElementProps<any>>> = {
[OptionType.STRING]: SettingTextComponent,
[OptionType.NUMBER]: SettingNumericComponent,
[OptionType.BIGINT]: SettingNumericComponent,
[OptionType.BOOLEAN]: SettingBooleanComponent,
[OptionType.SELECT]: SettingSelectComponent,
[OptionType.SLIDER]: SettingSliderComponent,
[OptionType.COMPONENT]: SettingCustomComponent
};
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
const [authors, setAuthors] = React.useState<Partial<User>[]>([]);
const pluginSettings = useSettings().plugins[plugin.name];
const [tempSettings, setTempSettings] = React.useState<Record<string, any>>({});
const [errors, setErrors] = React.useState<Record<string, boolean>>({});
const [saveError, setSaveError] = React.useState<string | null>(null);
const canSubmit = () => Object.values(errors).every(e => !e);
React.useEffect(() => {
(async () => {
for (const user of plugin.authors.slice(0, 6)) {
const author = user.id
? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user))
: makeDummyUser(user);
setAuthors(a => [...a, author]);
}
})();
}, []);
async function saveAndClose() {
if (!plugin.options) {
onClose();
return;
}
if (plugin.beforeSave) {
const result = await Promise.resolve(plugin.beforeSave(tempSettings));
if (result !== true) {
setSaveError(result);
return;
}
}
let restartNeeded = false;
for (const [key, value] of Object.entries(tempSettings)) {
const option = plugin.options[key];
pluginSettings[key] = value;
option?.onChange?.(value);
if (option?.restartNeeded) restartNeeded = true;
}
if (restartNeeded) onRestartNeeded();
onClose();
}
function renderSettings() {
if (!pluginSettings || !plugin.options) {
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
}
const options = Object.entries(plugin.options).map(([key, setting]) => {
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}
function onError(hasError: boolean) {
setErrors(e => ({ ...e, [key]: hasError }));
}
const Component = Components[setting.type];
return (
<Component
id={key}
key={key}
option={setting}
onChange={onChange}
onError={onError}
pluginSettings={pluginSettings}
/>
);
});
return <Flex flexDirection="column" style={{ gap: 12 }}>{options}</Flex>;
}
function renderMoreUsers(_label: string, count: number) {
const sliceCount = plugin.authors.length - count;
const sliceStart = plugin.authors.length - sliceCount;
const sliceEnd = sliceStart + plugin.authors.length - count;
return (
<Tooltip text={plugin.authors.slice(sliceStart, sliceEnd).map(u => u.name).join(", ")}>
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
+{sliceCount}
</div>
)}
</Tooltip>
);
}
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader>
<Text variant="heading-md/bold">{plugin.name}</Text>
</ModalHeader>
<ModalContent style={{ marginBottom: 8, marginTop: 8 }}>
<Forms.FormSection>
<Forms.FormTitle tag="h3">About {plugin.name}</Forms.FormTitle>
<Forms.FormText>{plugin.description}</Forms.FormText>
<div style={{ marginTop: 8, marginBottom: 8, width: "fit-content" }}>
<UserSummaryItem
users={authors}
count={plugin.authors.length}
guildId={undefined}
renderIcon={false}
max={6}
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={renderMoreUsers}
/>
</div>
</Forms.FormSection>
{!!plugin.settingsAboutComponent && (
<div style={{ marginBottom: 8 }}>
<Forms.FormSection>
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
<plugin.settingsAboutComponent />
</ErrorBoundary>
</Forms.FormSection>
</div>
)}
<Forms.FormSection>
<Forms.FormTitle tag="h3">Settings</Forms.FormTitle>
{renderSettings()}
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Flex flexDirection="column" style={{ width: "100%" }}>
<Flex style={{ marginLeft: "auto" }}>
<Button
onClick={onClose}
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
>
Cancel
</Button>
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
{({ onMouseEnter, onMouseLeave }) => (
<Button
size={Button.Sizes.SMALL}
color={Button.Colors.BRAND}
onClick={saveAndClose}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
disabled={!canSubmit()}
>
Save & Close
</Button>
)}
</Tooltip>
</Flex>
{saveError && <Text variant="text-md/semibold" style={{ color: "var(--text-danger)" }}>Error while saving: {saveError}</Text>}
</Flex>
</ModalFooter>
</ModalRoot>
);
}

View File

@ -0,0 +1,68 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionBoolean } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
const def = pluginSettings[id] ?? option.default;
const [state, setState] = React.useState(def ?? false);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
const options = [
{ label: "Enabled", value: true, default: def === true },
{ label: "Disabled", value: false, default: typeof def === "undefined" || def === false },
];
function handleChange(newValue: boolean): void {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
isDisabled={option.disabled?.() ?? false}
options={options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -0,0 +1,25 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionComponent } from "@utils/types";
import { ISettingElementProps } from ".";
export function SettingCustomComponent({ option, onChange, onError }: ISettingElementProps<PluginOptionComponent>) {
return option.component({ setValue: onChange, setError: onError, option });
}

View File

@ -0,0 +1,67 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { OptionType, PluginOptionNumber } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
import { ISettingElementProps } from ".";
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
export function SettingNumericComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionNumber>) {
function serialize(value: any) {
if (option.type === OptionType.BIGINT) return BigInt(value);
return Number(value);
}
const [state, setState] = React.useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
setState(`${Number.MAX_SAFE_INTEGER}`);
onChange(serialize(newValue));
} else {
setState(newValue);
onChange(serialize(newValue));
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<TextInput
type="number"
pattern="-?[0-9]+"
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a number"}
disabled={option.disabled?.() ?? false}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -0,0 +1,61 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionSelect } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
const [state, setState] = React.useState<any>(def ?? null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setState(newValue);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Select
isDisabled={option.disabled?.() ?? false}
options={option.options}
placeholder={option.placeholder ?? "Select an option"}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -0,0 +1,68 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionSlider } from "@utils/types";
import { Forms, React, Slider } from "@webpack/common";
import { ISettingElementProps } from ".";
export function makeRange(start: number, end: number, step = 1) {
const ranges: number[] = [];
for (let value = start; value <= end; value += step) {
ranges.push(Math.round(value * 100) / 100);
}
return ranges;
}
export function SettingSliderComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionSlider>) {
const def = pluginSettings[id] ?? option.default;
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue: number): void {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<Slider
disabled={option.disabled?.() ?? false}
markers={option.markers}
minValue={option.markers[0]}
maxValue={option.markers[option.markers.length - 1]}
initialValue={def}
onValueChange={handleChange}
onValueRender={(v: number) => String(v.toFixed(2))}
stickToMarkers={option.stickToMarkers ?? true}
{...option.componentProps}
/>
</Forms.FormSection>
);
}

View File

@ -0,0 +1,56 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionString } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingTextComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
const [state, setState] = React.useState(pluginSettings[id] ?? option.default ?? null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = (option.isValid && option.isValid(newValue)) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setState(newValue);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{option.description}</Forms.FormTitle>
<TextInput
type="text"
value={state}
onChange={handleChange}
placeholder={option.placeholder ?? "Enter a value"}
disabled={option.disabled?.() ?? false}
{...option.componentProps}
/>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View File

@ -0,0 +1,38 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> {
option: T;
onChange(newValue: any): void;
pluginSettings: {
[setting: string]: any;
enabled: boolean;
};
id: string;
onError(hasError: boolean): void;
}
export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent";
export * from "./SettingNumericComponent";
export * from "./SettingSelectComponent";
export * from "./SettingSliderComponent";
export * from "./SettingTextComponent";

View File

@ -0,0 +1,336 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { showNotice } from "@api/Notices";
import { Settings, useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { ChangeList } from "@utils/ChangeList";
import Logger from "@utils/Logger";
import { classes, LazyComponent } from "@utils/misc";
import { openModalLazy } from "@utils/modal";
import { Plugin } from "@utils/types";
import { findByCode, findByPropsLazy } from "@webpack";
import { Alerts, Button, Forms, Margins, Parser, React, Select, Switch, Text, TextInput, Toasts, Tooltip } from "@webpack/common";
import Plugins from "~plugins";
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
import PluginModal from "./PluginModal";
import * as styles from "./styles";
const logger = new Logger("PluginSettings", "#a6d189");
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
const CogWheel = LazyComponent(() => findByCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
const InfoIcon = LazyComponent(() => findByCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
function showErrorToast(message: string) {
Toasts.show({
message,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
interface ReloadRequiredCardProps extends React.HTMLProps<HTMLDivElement> {
plugins: string[];
}
function ReloadRequiredCard({ plugins, ...props }: ReloadRequiredCardProps) {
if (plugins.length === 0) return null;
const pluginPrefix = plugins.length === 1 ? "The plugin" : "The following plugins require a reload to apply changes:";
const pluginSuffix = plugins.length === 1 ? " requires a reload to apply changes." : ".";
return (
<ErrorCard {...props} style={{ padding: "1em", display: "grid", gridTemplateColumns: "1fr auto", gap: 8, ...props.style }}>
<span style={{ margin: "auto 0" }}>
{pluginPrefix} <code>{plugins.join(", ")}</code>{pluginSuffix}
</span>
<Button look={Button.Looks.INVERTED} onClick={() => location.reload()}>Reload</Button>
</ErrorCard>
);
}
interface PluginCardProps extends React.HTMLProps<HTMLDivElement> {
plugin: Plugin;
disabled: boolean;
onRestartNeeded(name: string): void;
}
function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLeave }: PluginCardProps) {
const settings = useSettings();
const pluginSettings = settings.plugins[plugin.name];
const [iconHover, setIconHover] = React.useState(false);
function isEnabled() {
return pluginSettings?.enabled || plugin.started;
}
function openModal() {
openModalLazy(async () => {
return modalProps => {
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
};
});
}
function toggleEnabled() {
const wasEnabled = isEnabled();
// If we're enabling a plugin, make sure all deps are enabled recursively.
if (!wasEnabled) {
const { restartNeeded, failures } = startDependenciesRecursive(plugin);
if (failures.length) {
logger.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`);
showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null);
return;
} else if (restartNeeded) {
// If any dependencies have patches, don't start the plugin yet.
pluginSettings.enabled = true;
onRestartNeeded(plugin.name);
return;
}
}
// if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes.
if (plugin.patches) {
pluginSettings.enabled = !wasEnabled;
onRestartNeeded(plugin.name);
return;
}
// If the plugin is enabled, but hasn't been started, then we can just toggle it off.
if (wasEnabled && !plugin.started) {
pluginSettings.enabled = !wasEnabled;
return;
}
const result = wasEnabled ? stopPlugin(plugin) : startPlugin(plugin);
const action = wasEnabled ? "stop" : "start";
if (!result) {
logger.error(`Failed to ${action} plugin ${plugin.name}`);
showErrorToast(`Failed to ${action} plugin: ${plugin.name}`);
return;
}
pluginSettings.enabled = !wasEnabled;
}
return (
<Flex style={styles.PluginsGridItem} flexDirection="column" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Switch
onChange={toggleEnabled}
disabled={disabled}
value={isEnabled()}
note={<Text variant="text-md/normal" style={{
height: 40,
overflow: "hidden",
// mfw css is so bad you need whatever this is to get multi line overflow ellipsis to work
textOverflow: "ellipsis",
display: "-webkit-box", // firefox users will cope (it doesn't support it)
WebkitLineClamp: 2,
lineClamp: 2,
WebkitBoxOrient: "vertical",
boxOrient: "vertical"
}}>
{plugin.description}
</Text>}
hideBorder={true}
>
<Flex style={{ marginTop: "auto", width: "100%", height: "100%", alignItems: "center" }}>
<Text variant="text-md/bold" style={{ flexGrow: "1" }}>{plugin.name}</Text>
<button role="switch" onClick={() => openModal()} style={styles.SettingsIcon} className="button-12Fmur">
{plugin.options
? <CogWheel
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>
: <InfoIcon
width="24" height="24"
style={{ color: iconHover ? "" : "var(--text-muted)" }}
onMouseEnter={() => setIconHover(true)}
onMouseLeave={() => setIconHover(false)}
/>}
</button>
</Flex>
</Switch>
</Flex>
);
}
export default ErrorBoundary.wrap(function Settings() {
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>(), []);
React.useEffect(() => {
return () => void (changes.hasChanges && Alerts.show({
title: "Restart required",
body: (
<>
<p>The following plugins require a restart:</p>
<div>{changes.map((s, i) => (
<>
{i > 0 && ", "}
{Parser.parse("`" + s + "`")}
</>
))}</div>
</>
),
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
}));
}, []);
const depMap = React.useMemo(() => {
const o = {} as Record<string, string[]>;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
if (deps) {
for (const dep of deps) {
o[dep] ??= [];
o[dep].push(plugin);
}
}
}
return o;
}, []);
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
const [searchValue, setSearchValue] = React.useState({ value: "", status: "all" });
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: string) => setSearchValue(prev => ({ ...prev, status }));
const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => {
const showEnabled = searchValue.status === "enabled" || searchValue.status === "all";
const showDisabled = searchValue.status === "disabled" || searchValue.status === "all";
const enabled = settings.plugins[plugin.name]?.enabled || plugin.started;
return (
((showEnabled && enabled) || (showDisabled && !enabled)) &&
(
plugin.name.toLowerCase().includes(searchValue.value.toLowerCase()) ||
plugin.description.toLowerCase().includes(searchValue.value.toLowerCase())
)
);
};
return (
<Forms.FormSection>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Filters
</Forms.FormTitle>
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
<div style={styles.FiltersBar}>
<TextInput value={searchValue.value} placeholder={"Search for a plugin..."} onChange={onSearch} style={{ marginBottom: 24 }} />
<div className={InputStyles.inputWrapper}>
<Select
className={InputStyles.inputDefault}
options={[
{ label: "Show All", value: "all", default: true },
{ label: "Show Enabled", value: "enabled" },
{ label: "Show Disabled", value: "disabled" }
]}
serialize={String}
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
/>
</div>
</div>
<Forms.FormTitle className={Margins.marginTop20}>Plugins</Forms.FormTitle>
<div style={styles.PluginsGrid}>
{sortedPlugins?.length ? sortedPlugins
.filter(a => !a.required && !dependencyCheck(a.name, depMap).length && pluginFilter(a))
.map(plugin => {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
return <PluginCard
onRestartNeeded={name => changes.add(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
key={plugin.name}
/>;
})
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
}
</div>
<Forms.FormDivider />
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Required Plugins
</Forms.FormTitle>
<div style={styles.PluginsGrid}>
{sortedPlugins?.length ? sortedPlugins
.filter(a => a.required || dependencyCheck(a.name, depMap).length && pluginFilter(a))
.map(plugin => {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
const tooltipText = plugin.required
? "This plugin is required for Vencord to function."
: makeDependencyList(dependencyCheck(plugin.name, depMap));
return <Tooltip text={tooltipText} key={plugin.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.handleChange(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
/>
)}
</Tooltip>;
})
: <Text variant="text-md/normal">No plugins meet search criteria.</Text>
}
</div>
</Forms.FormSection >
);
}, {
message: "Failed to render the Plugin Settings. If this persists, try using the installer to reinstall!",
onError: handleComponentFailed,
});
function makeDependencyList(deps: string[]) {
return (
<React.Fragment>
<Forms.FormText>This plugin is required by:</Forms.FormText>
{deps.map((dep: string) => <Forms.FormText style={{ margin: "0 auto" }}>{dep}</Forms.FormText>)}
</React.Fragment>
);
}
function dependencyCheck(pluginName: string, depMap: Record<string, string[]>): string[] {
return depMap[pluginName]?.filter(d => Settings.plugins[d].enabled) || [];
}

View File

@ -0,0 +1,50 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const PluginsGrid: React.CSSProperties = {
marginTop: 16,
display: "grid",
gridGap: 16,
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
};
export const PluginsGridItem: React.CSSProperties = {
backgroundColor: "var(--background-modifier-selected)",
color: "var(--interactive-active)",
borderRadius: 3,
cursor: "pointer",
display: "block",
height: "min-content",
padding: 10,
width: "100%",
};
export const FiltersBar: React.CSSProperties = {
gap: 10,
height: 40,
gridTemplateColumns: "1fr 150px",
display: "grid"
};
export const SettingsIcon: React.CSSProperties = {
height: "24px",
width: "24px",
padding: "0",
background: "transparent",
marginRight: 8
};

View File

@ -1,173 +0,0 @@
import { classes, humanFriendlyJoin, useAwaiter } from "../utils/misc";
import Plugins from 'plugins';
import { useSettings } from "../api/settings";
import IpcEvents from "../utils/IpcEvents";
import { Button, Switch, Forms, React, Margins, Toasts, Alerts, Parser } from "../webpack/common";
import ErrorBoundary from "./ErrorBoundary";
import { startPlugin } from "../plugins";
import { stopPlugin } from '../plugins/index';
import { Flex } from './Flex';
import { ChangeList } from '../utils/ChangeList';
function showErrorToast(message: string) {
Toasts.show({
message,
type: Toasts.Type.FAILURE,
id: Toasts.genId(),
options: {
position: Toasts.Position.BOTTOM
}
});
}
export default ErrorBoundary.wrap(function Settings() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
const settings = useSettings();
const changes = React.useMemo(() => new ChangeList<string>, []);
React.useEffect(() => {
return () => void (changes.hasChanges && Alerts.show({
title: "Restart required",
body: (
<>
<p>The following plugins require a restart:</p>
<div>{changes.map((s, i) => (
<>
{i > 0 && ", "}
{Parser.parse('`' + s + '`')}
</>
))}</div>
</>
),
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: () => location.reload()
}));
}, []);
const depMap = React.useMemo(() => {
const o = {} as Record<string, string[]>;
for (const plugin in Plugins) {
const deps = Plugins[plugin].dependencies;
if (deps) {
for (const dep of deps) {
o[dep] ??= [];
o[dep].push(plugin);
}
}
}
return o;
}, []);
const sortedPlugins = React.useMemo(() => Object.values(Plugins).sort((a, b) => a.name.localeCompare(b.name)), []);
return (
<Forms.FormSection tag="h1" title="Vencord">
<Forms.FormTitle tag="h5">
Settings
</Forms.FormTitle>
<Forms.FormText>
SettingsDir: <code style={{ userSelect: 'text', cursor: 'text' }}>{settingsDir}</code>
</Forms.FormText>
<Flex className={classes(Margins.marginBottom20)}>
<Button
onClick={() => window.DiscordNative.app.relaunch()}
size={Button.Sizes.SMALL}
color={Button.Colors.GREEN}
>
Reload
</Button>
<Button
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}
>
Launch Directory
</Button>
<Button
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir, "quickCss.css")}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}
>
Open QuickCSS File
</Button>
</Flex>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Settings</Forms.FormTitle>
<Switch
value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v}
note="Enable QuickCSS"
>
Use QuickCss
</Switch>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp"
>
Get notified about new Updates
</Switch>
<Switch
value={settings.unsafeRequire}
onChange={(v: boolean) => settings.unsafeRequire = v}
note="Enables VencordNative.require. Useful for testing, very bad for security. Leave this off unless you need it."
>
Enable Unsafe Require
</Switch>
<Forms.FormDivider />
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Plugins
</Forms.FormTitle>
{sortedPlugins.map(p => {
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
return (
<Switch
disabled={p.required || dependency}
key={p.name}
value={settings.plugins[p.name].enabled || p.required || dependency}
onChange={v => {
settings.plugins[p.name].enabled = v;
if (v) {
p.dependencies?.forEach(d => {
settings.plugins[d].enabled = true;
if (!Plugins[d].started && !stopPlugin) {
settings.plugins[p.name].enabled = false;
showErrorToast(`Failed to start dependency ${d}. Check the console for more info.`);
}
});
if (!p.started && !startPlugin(p)) {
showErrorToast(`Failed to start plugin ${p.name}. Check the console for more info.`);
}
} else {
if (p.started && !stopPlugin(p)) {
showErrorToast(`Failed to stop plugin ${p.name}. Check the console for more info.`);
}
}
if (p.patches) changes.handleChange(p.name);
}}
note={p.description}
tooltipNote={
p.required ?
"This plugin is required. Thus you cannot disable it."
: dependency ?
`${humanFriendlyJoin(enabledDependants)} ${enabledDependants.length === 1 ? "depends" : "depend"} on this plugin. Thus you cannot disable it.`
: null
}
>
{p.name}
</Switch>
);
})
}
</Forms.FormSection >
);
});

View File

@ -0,0 +1,69 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { downloadSettingsBackup, uploadSettingsBackup } from "@utils/settingsSync";
import { Button, Card, Forms, Margins, Text } from "@webpack/common";
function BackupRestoreTab() {
return (
<Forms.FormSection title="Settings Sync">
<Card style={{
backgroundColor: "var(--info-warning-background)",
borderColor: "var(--info-warning-foreground)",
color: "var(--info-warning-text)",
padding: "1em",
marginBottom: "0.5em",
}}>
<Flex flexDirection="column">
<strong>Warning</strong>
<span>Importing a settings file will overwrite your current settings.</span>
</Flex>
</Card>
<Text variant="text-md/normal" className={Margins.marginBottom8}>
You can import and export your Vencord settings as a JSON file.
This allows you to easily transfer your settings to another device,
or recover your settings after reinstalling Vencord or Discord.
</Text>
<Text variant="text-md/normal" className={Margins.marginBottom8}>
Settings Export contains:
<ul>
<li>&mdash; Custom QuickCSS</li>
<li>&mdash; Plugin Settings</li>
</ul>
</Text>
<Flex>
<Button
onClick={uploadSettingsBackup}
size={Button.Sizes.SMALL}
>
Import Settings
</Button>
<Button
onClick={downloadSettingsBackup}
size={Button.Sizes.SMALL}
>
Export Settings
</Button>
</Flex>
</Forms.FormSection>
);
}
export default ErrorBoundary.wrap(BackupRestoreTab);

View File

@ -0,0 +1,22 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import PluginSettings from "@components/PluginSettings";
export default ErrorBoundary.wrap(PluginSettings);

View File

@ -0,0 +1,135 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Link } from "@components/Link";
import { useAwaiter } from "@utils/misc";
import { findLazy } from "@webpack";
import { Card, Forms, Margins, React, TextArea } from "@webpack/common";
const TextAreaProps = findLazy(m => typeof m.textarea === "string");
function Validator({ link }: { link: string; }) {
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
if (res.status > 300) throw `${res.status} ${res.statusText}`;
const contentType = res.headers.get("Content-Type");
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
throw "Not a CSS file. Remember to use the raw link!";
return "Okay!";
}));
const text = pending
? "Checking..."
: err
? `Error: ${err instanceof Error ? err.message : String(err)}`
: "Valid!";
return <Forms.FormText style={{
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--text-positive)"
}}>{text}</Forms.FormText>;
}
function Validators({ themeLinks }: { themeLinks: string[]; }) {
if (!themeLinks.length) return null;
return (
<>
<Forms.FormTitle className={Margins.marginTop20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(link => (
<Card style={{
padding: ".5em",
marginBottom: ".5em"
}} key={link}>
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{link}
</Forms.FormTitle>
<Validator link={link} />
</Card>
))}
</div>
</>
);
}
export default ErrorBoundary.wrap(function () {
const settings = useSettings();
const ref = React.useRef<HTMLTextAreaElement>();
function onBlur() {
settings.themeLinks = [...new Set(
ref.current!.value
.trim()
.split(/\n+/)
.map(s => s.trim())
.filter(Boolean)
)];
}
return (
<>
<Card style={{
padding: "1em",
marginBottom: "1em",
marginTop: "1em"
}}>
<Forms.FormTitle tag="h5">Paste links to .css / .theme.css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>Be careful to use the raw links or github.io links!</Forms.FormText>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes
</Link>
<Link href="https://github.com/search?q=discord+theme">Github</Link>
</div>
<Forms.FormText>If using the BD site, click on "Source" somewhere below the Download button</Forms.FormText>
<Forms.FormText>In the GitHub repository of your theme, find X.theme.css / X.css, click on it, then click the "Raw" button</Forms.FormText>
<Forms.FormText>
If the theme has configuration that requires you to edit the file:
<ul>
<li> Make a github account</li>
<li> Click the fork button on the top right</li>
<li> Edit the file</li>
<li> Use the link to your own repository instead</li>
</ul>
</Forms.FormText>
</Card>
<Forms.FormTitle tag="h5">Themes</Forms.FormTitle>
<TextArea
style={{
padding: ".5em",
border: "1px solid var(--background-modifier-accent)"
}}
ref={ref}
defaultValue={settings.themeLinks.join("\n")}
className={TextAreaProps.textarea}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}
/>
<Validators themeLinks={settings.themeLinks} />
</>
);
});

View File

@ -1,12 +1,31 @@
import gitHash from "git-hash";
import { changes, checkForUpdates, getRepo, rebuild, update, UpdateLogger, updateError } from '../utils/updater';
import { React, Forms, Button, Margins, Alerts, Card, Parser, Toasts } from '../webpack/common';
import { Flex } from "./Flex";
import { useAwaiter } from '../utils/misc';
import { Link } from "./Link";
import ErrorBoundary from "./ErrorBoundary";
import { ErrorCard } from "./ErrorCard";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { handleComponentFailed } from "@components/handleComponentFailed";
import { Link } from "@components/Link";
import { classes, useAwaiter } from "@utils/misc";
import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Margins, Parser, React, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
@ -43,35 +62,40 @@ function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>
dispatcher(false);
}
};
};
}
export default ErrorBoundary.wrap(function Updater() {
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
interface CommonProps {
repo: string;
repoPending: boolean;
}
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return (
<Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => (
<div>
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
<code>{hash}</code>
</Link>
<span style={{
marginLeft: "0.5em",
color: "var(--text-normal)"
}}>{message} - {author}</span>
</div>
))}
</Card>
);
}
function Updatable(props: CommonProps) {
const [updates, setUpdates] = React.useState(changes);
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const [updates, setUpdates] = React.useState(changes);
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
const isOutdated = updates?.length > 0;
const isOutdated = (updates?.length ?? 0) > 0;
return (
<Forms.FormSection tag="h1" title="Vencord Updater">
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)} ({gitHash})</Forms.FormText>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
<>
{!updates && updateError ? (
<>
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
@ -79,31 +103,15 @@ export default ErrorBoundary.wrap(function Updater() {
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) :
(
<Forms.FormText className={Margins.marginBottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)
}
{isOutdated && (
<Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => (
<div>
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
<code>{hash}</code>
</Link>
<span style={{
marginLeft: "0.5em",
color: "var(--text-normal)"
}}>{message} - {author}</span>
</div>
))}
</Card>
) : (
<Forms.FormText className={Margins.marginBottom8}>
{isOutdated ? `There are ${updates.length} Updates` : "Up to Date!"}
</Forms.FormText>
)}
<Flex className={`${Margins.marginBottom8} ${Margins.marginTop8}`}>
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.marginBottom8, Margins.marginTop8)}>
{isOutdated && <Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
@ -155,6 +163,54 @@ export default ErrorBoundary.wrap(function Updater() {
Check for Updates
</Button>
</Flex>
</>
);
}
function Newer(props: CommonProps) {
return (
<>
<Forms.FormText className={Margins.marginBottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText>
<Changes {...props} updates={changes} />
</>
);
}
function Updater() {
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
const commonProps: CommonProps = {
repo,
repoPending
};
return (
<Forms.FormSection>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)} ({gitHash})</Forms.FormText>
<Forms.FormDivider />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</Forms.FormSection >
);
}
export default IS_WEB ? null : ErrorBoundary.wrap(Updater, {
message: "Failed to render the Updater. If this persists, try using the installer to reinstall!",
onError: handleComponentFailed,
});

View File

@ -0,0 +1,149 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/settings";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import IpcEvents from "@utils/IpcEvents";
import { useAwaiter } from "@utils/misc";
import { Button, Card, Forms, Margins, React, Switch } from "@webpack/common";
const st = (style: string) => `vcSettings${style}`;
function VencordSettings() {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), {
fallbackValue: "Loading..."
});
const settings = useSettings();
const [donateImage] = React.useState(
Math.random() > 0.5
? "https://cdn.discordapp.com/emojis/1026533090627174460.png"
: "https://media.discordapp.net/stickers/1039992459209490513.png"
);
return (
<React.Fragment>
<DonateCard image={donateImage} />
<Forms.FormSection title="Quick Actions">
<Card className={st("QuickActionCard")}>
{IS_WEB ? (
<Button
onClick={() => require("../Monaco").launchMonacoEditor()}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
) : (
<React.Fragment>
<Button
onClick={() => window.DiscordNative.app.relaunch()}
size={Button.Sizes.SMALL}>
Restart Client
</Button>
<Button
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_MONACO_EDITOR)}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}>
Open QuickCSS File
</Button>
<Button
onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
Open Settings Folder
</Button>
<Button
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")}
size={Button.Sizes.SMALL}
disabled={settingsDirPending}>
Open in GitHub
</Button>
</React.Fragment>
)}
</Card>
</Forms.FormSection>
<Forms.FormDivider />
<Forms.FormSection title="Settings">
<Forms.FormText className={Margins.marginBottom20}>
Hint: You can change the position of this settings section in the settings of the "Settings" plugin!
</Forms.FormText>
<Switch
value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v}
note="Loads styles from your QuickCss file">
Use QuickCss
</Switch>
{!IS_WEB && (
<React.Fragment>
<Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart">
Enable React Developer Tools
</Switch>
<Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp">
Get notified about new Updates
</Switch>
</React.Fragment>
)}
</Forms.FormSection>
</React.Fragment>
);
}
interface DonateCardProps {
image: string;
}
function DonateCard({ image }: DonateCardProps) {
return (
<Card style={{
padding: "1em",
display: "flex",
flexDirection: "row",
marginBottom: "1em",
marginTop: "1em"
}}>
<div>
<Forms.FormTitle tag="h5">Support the Project</Forms.FormTitle>
<Forms.FormText>
Please consider supporting the Development of Vencord by donating!
</Forms.FormText>
<DonateButton style={{ transform: "translateX(-1em)" }} />
</div>
<img
role="presentation"
src={image}
alt=""
height={128}
style={{ marginLeft: "auto", transform: "rotate(10deg)" }}
/>
</Card>
);
}
export default ErrorBoundary.wrap(VencordSettings);

View File

@ -0,0 +1,92 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack";
import { Forms, Router, Text } from "@webpack/common";
import cssText from "~fileContent/settingsStyles.css";
import BackupRestoreTab from "./BackupRestoreTab";
import PluginsTab from "./PluginsTab";
import ThemesTab from "./ThemesTab";
import Updater from "./Updater";
import VencordSettings from "./VencordTab";
const style = document.createElement("style");
style.textContent = cssText;
document.head.appendChild(style);
const st = (style: string) => `vcSettings${style}`;
const TabBar = findByCodeLazy('[role="tab"][aria-disabled="false"]');
interface SettingsProps {
tab: string;
}
interface SettingsTab {
name: string;
component?: React.ComponentType;
}
const SettingsTabs: Record<string, SettingsTab> = {
VencordSettings: { name: "Vencord", component: () => <VencordSettings /> },
VencordPlugins: { name: "Plugins", component: () => <PluginsTab /> },
VencordThemes: { name: "Themes", component: () => <ThemesTab /> },
VencordUpdater: { name: "Updater" }, // Only show updater if IS_WEB is false
VencordSettingsSync: { name: "Backup & Restore", component: () => <BackupRestoreTab /> },
};
if (!IS_WEB) SettingsTabs.VencordUpdater.component = () => Updater && <Updater />;
function Settings(props: SettingsProps) {
const { tab = "VencordSettings" } = props;
const CurrentTab = SettingsTabs[tab]?.component;
return <Forms.FormSection>
<Text variant="heading-md/normal" tag="h2">Vencord Settings</Text>
<TabBar
type={TabBar.Types.TOP}
look={TabBar.Looks.BRAND}
className={st("TabBar")}
selectedItem={tab}
onItemSelect={Router.open}
>
{Object.entries(SettingsTabs).map(([key, { name, component }]) => {
if (!component) return null;
return <TabBar.Item
id={key}
className={st("TabBarItem")}
key={key}>
{name}
</TabBar.Item>;
})}
</TabBar>
<Forms.FormDivider />
{CurrentTab && <CurrentTab />}
</Forms.FormSection >;
}
export default function (props: SettingsProps) {
return <ErrorBoundary>
<Settings tab={props.tab} />
</ErrorBoundary>;
}

View File

@ -0,0 +1,23 @@
.vcSettingsTabBar {
margin-top: 20px;
margin-bottom: -2px;
border-bottom: 2px solid var(--background-modifier-accent);
}
.vcSettingsTabBarItem {
margin-right: 32px;
padding-bottom: 16px;
margin-bottom: -2px;
}
.vcSettingsQuickActionCard {
padding: 1em;
display: flex;
gap: 1em;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
flex-grow: 1;
flex-direction: row;
margin-bottom: 1em;
}

View File

@ -0,0 +1,44 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { isOutdated, rebuild, update } from "@utils/updater";
export async function handleComponentFailed() {
if (isOutdated) {
setImmediate(async () => {
const wantsUpdate = confirm(
"Uh Oh! Failed to render this Page." +
" However, there is an update available that might fix it." +
" Would you like to update and restart now?"
);
if (wantsUpdate) {
try {
await update();
await rebuild();
if (IS_WEB)
location.reload();
else
DiscordNative.app.relaunch();
} catch (e) {
console.error(e);
alert("That also failed :( Try updating or reinstalling with the installer!");
}
}
});
}
}

View File

@ -1,2 +1,21 @@
export { default as Settings } from "./Settings";
export { default as Updater } from "./Updater";
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export { default as PatchHelper } from "./PatchHelper";
export { default as PluginSettings } from "./PluginSettings";
export { default as VencordSettings } from "./VencordSettings";

View File

@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>QuickCss Editor</title>
<link rel="stylesheet" data-name="vs/editor/editor.main"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css">
<style>
html,
body,
#container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js"></script>
<script>
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } });
require(["vs/editor/editor.main"], () => {
getCurrentCss().then(css => {
var editor = monaco.editor.create(document.getElementById('container'), {
value: css,
language: 'css',
theme: getTheme(),
});
editor.onDidChangeModelContent(() =>
setCss(editor.getValue())
);
window.addEventListener("resize", () => {
// make monaco re-layout
editor.layout();
});
});
});
</script>
</body>
</html>

64
src/debug/Tracer.ts Normal file
View File

@ -0,0 +1,64 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "@utils/Logger";
if (IS_DEV) {
var traces = {} as Record<string, [number, any[]]>;
var logger = new Logger("Tracer", "#FFD166");
}
const noop = function () { };
export const beginTrace = !IS_DEV ? noop :
function beginTrace(name: string, ...args: any[]) {
if (name in traces)
throw new Error(`Trace ${name} already exists!`);
traces[name] = [performance.now(), args];
};
export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
const end = performance.now();
const [start, args] = traces[name];
delete traces[name];
logger.debug(`${name} took ${end - start}ms`, args);
};
type Func = (...args: any[]) => any;
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
const noopTracer =
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
export const traceFunction = !IS_DEV
? noopTracer
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
return function (this: any, ...args: Parameters<F>) {
const traceName = mapper?.(...args) ?? name;
beginTrace(traceName, ...arguments);
try {
return f.apply(this, args);
} finally {
finishTrace(traceName);
}
} as F;
};

46
src/globals.d.ts vendored
View File

@ -1,16 +1,60 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare global {
/**
* This exists only at build time, so references to it in patches should insert it
* via String interpolation OR use different replacement code based on this
* but NEVER reference it inside the patched code
*
* @example
* // BAD
* replace: "IS_WEB?foo:bar"
* // GOOD
* replace: IS_WEB ? "foo" : "bar"
* // also good
* replace: `${IS_WEB}?foo:bar`
*/
export var IS_WEB: boolean;
export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var VencordNative: typeof import("./VencordNative").default;
export var Vencord: typeof import("./Vencord");
export var appSettings: {
set(setting: string, v: any): void;
};
/**
* Only available when running in Electron, undefined on web.
* Thus, avoid using this or only use it inside an {@link IS_WEB} guard.
*
* If you really must use it, mark your plugin as Desktop App only via
* `target: "DESKTOP"`
*/
export var DiscordNative: any;
interface Window {
webpackChunkdiscord_app: {
push(chunk: any): any;
pop(): any;
};
[k: PropertyKey]: any;
[k: string]: any;
}
}

35
src/ipcMain/constants.ts Normal file
View File

@ -0,0 +1,35 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app } from "electron";
import { join } from "path";
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? (
process.env.DISCORD_USER_DATA_DIR
? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData")
: join(app.getPath("userData"), "..", "Vencord")
);
export const SETTINGS_DIR = join(DATA_DIR, "settings");
export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
export const ALLOWED_PROTOCOLS = [
"https:",
"http:",
"steam:",
"spotify:"
];

57
src/ipcMain/crxToZip.ts Normal file
View File

@ -0,0 +1,57 @@
/* eslint-disable header/header */
/*!
* crxToZip
* Copyright (c) 2013 Rob Wu <rob@robwu.nl>
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
export function crxToZip(buf: Buffer) {
function calcLength(a: number, b: number, c: number, d: number) {
let length = 0;
length += a << 0;
length += b << 8;
length += c << 16;
length += d << 24 >>> 0;
return length;
}
// 50 4b 03 04
// This is actually a zip file
if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) {
return buf;
}
// 43 72 32 34 (Cr24)
if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) {
throw new Error("Invalid header: Does not start with Cr24");
}
// 02 00 00 00
// or
// 03 00 00 00
const isV3 = buf[4] === 3;
const isV2 = buf[4] === 2;
if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) {
throw new Error("Unexpected crx format version number.");
}
if (isV2) {
const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]);
const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]);
// 16 = Magic number (4), CRX format version (4), lengths (2x4)
const zipStartOffset = 16 + publicKeyLength + signatureLength;
return buf.subarray(zipStartOffset, buf.length);
}
// v3 format has header size and then header
const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]);
const zipStartOffset = 12 + headerSize;
return buf.subarray(zipStartOffset, buf.length);
}

76
src/ipcMain/extensions.ts Normal file
View File

@ -0,0 +1,76 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { session } from "electron";
import { unzip } from "fflate";
import { constants as fsConstants } from "fs";
import { access, mkdir, rm, writeFile } from "fs/promises";
import { join } from "path";
import { DATA_DIR } from "./constants";
import { crxToZip } from "./crxToZip";
import { get } from "./simpleGet";
const extensionCacheDir = join(DATA_DIR, "ExtensionCache");
async function extract(data: Buffer, outDir: string) {
await mkdir(outDir, { recursive: true });
return new Promise<void>((resolve, reject) => {
unzip(data, (err, files) => {
if (err) return void reject(err);
Promise.all(Object.keys(files).map(async f => {
// Signature stuff
// 'Cannot load extension with file or directory name
// _metadata. Filenames starting with "_" are reserved for use by the system.';
if (f.startsWith("_metadata/")) return;
if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true });
const pathElements = f.split("/");
const name = pathElements.pop()!;
const directories = pathElements.join("/");
const dir = join(outDir, directories);
if (directories) {
await mkdir(dir, { recursive: true });
}
await writeFile(join(dir, name), files[f]);
}))
.then(() => resolve())
.catch(err => {
rm(outDir, { recursive: true, force: true });
reject(err);
});
});
});
}
export async function installExt(id: string) {
const extDir = join(extensionCacheDir, `${id}`);
try {
await access(extDir, fsConstants.F_OK);
} catch (err) {
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`;
const buf = await get(url);
await extract(crxToZip(buf), extDir);
}
session.defaultSession.loadExtension(extDir);
}

View File

@ -1,16 +1,34 @@
import { app, BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises";
import { join } from 'path';
import { debounce } from "../utils/debounce";
import IpcEvents from '../utils/IpcEvents';
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./updater";
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
const SETTINGS_DIR = join(DATA_DIR, "settings");
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { BrowserWindow, desktopCapturer, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises";
import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -18,7 +36,7 @@ function readCss() {
return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
}
function readSettings() {
export function readSettings() {
try {
return readFileSync(SETTINGS_FILE, "utf-8");
} catch {
@ -29,18 +47,33 @@ function readSettings() {
// Fix for screensharing in Electron >= 17
ipcMain.handle(IpcEvents.GET_DESKTOP_CAPTURE_SOURCES, (_, opts) => desktopCapturer.getSources(opts));
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url));
ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => {
try {
var { protocol } = new URL(url);
} catch {
throw "Malformed URL";
}
if (!ALLOWED_PROTOCOLS.includes(protocol))
throw "Disallowed protocol.";
shell.openExternal(url);
});
const cssWriteQueue = new Queue();
const settingsWriteQueue = new Queue();
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css))
);
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());
ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings());
let settingsWriteQueue = Promise.resolve();
ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => {
settingsWriteQueue = settingsWriteQueue.then(() => writeFile(SETTINGS_FILE, s));
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
});
@ -52,3 +85,17 @@ export function initIpc(mainWindow: BrowserWindow) {
}, 50));
});
}
ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
const win = new BrowserWindow({
title: "QuickCss Editor",
autoHideMenuBar: true,
darkTheme: true,
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false
}
});
await win.loadURL(`data:text/html;base64,${monacoHtml}`);
});

37
src/ipcMain/simpleGet.ts Normal file
View File

@ -0,0 +1,37 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import https from "https";
export function get(url: string, options: https.RequestOptions = {}) {
return new Promise<Buffer>((resolve, reject) => {
https.get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400)
return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300)
return void resolve(get(headers.location!, options));
const chunks = [] as Buffer[];
res.on("error", reject);
res.on("data", chunk => chunks.push(chunk));
res.once("end", () => resolve(Buffer.concat(chunks)));
});
});
}

View File

@ -1,94 +0,0 @@
import { ipcMain } from 'electron';
import { promisify } from "util";
import IpcEvents from "../utils/IpcEvents";
import { execFile as cpExecFile } from 'child_process';
import { join } from 'path';
import { createReadStream } from 'fs';
import { createHash } from 'crypto';
const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile);
function git(...args: string[]) {
return execFile("git", args, {
cwd: VENCORD_SRC_DIR
});
}
async function calculateHashes() {
const hashes = {} as Record<string, string>;
await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => {
hash.end();
hashes[file] = hash.read();
r();
});
fis.pipe(hash);
}))
);
return hashes;
}
function serializeErrors(func: (...args: any[]) => any) {
return async function () {
try {
return {
ok: true,
value: await func(...arguments)
};
} catch (e: any) {
return {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
...e
} : e
};
}
};
}
async function getRepo() {
const res = await git("remote", "get-url", "origin");
return res.stdout.trim()
.replace(/git@(.+):/, "https://$1/")
.replace(/\.git$/, "");
}
async function calculateGitChanges() {
await git("fetch");
const res = await git("log", `HEAD...origin/main`, "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {
const [author, hash, ...rest] = line.split("/");
return {
hash, author, message: rest.join("/")
};
}) : [];
}
async function pull() {
const res = await git("pull");
return res.stdout.includes("Fast-forward");
}
async function build() {
const res = await execFile("node", ["build.mjs"], {
cwd: VENCORD_SRC_DIR
});
return !res.stderr.includes("Build failed");
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));

View File

@ -0,0 +1,59 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createHash } from "crypto";
import { createReadStream } from "fs";
import { join } from "path";
export async function calculateHashes() {
const hashes = {} as Record<string, string>;
await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => {
hash.end();
hashes[file] = hash.read();
r();
});
fis.pipe(hash);
}))
);
return hashes;
}
export function serializeErrors(func: (...args: any[]) => any) {
return async function () {
try {
return {
ok: true,
value: await func(...arguments)
};
} catch (e: any) {
return {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
...e
} : e
};
}
};
}

View File

@ -0,0 +1,81 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IpcEvents from "@utils/IpcEvents";
import { execFile as cpExecFile } from "child_process";
import { ipcMain } from "electron";
import { join } from "path";
import { promisify } from "util";
import { calculateHashes, serializeErrors } from "./common";
const VENCORD_SRC_DIR = join(__dirname, "..");
const execFile = promisify(cpExecFile);
const isFlatpak = Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord"));
function git(...args: string[]) {
const opts = { cwd: VENCORD_SRC_DIR };
if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts);
else return execFile("git", args, opts);
}
async function getRepo() {
const res = await git("remote", "get-url", "origin");
return res.stdout.trim()
.replace(/git@(.+):/, "https://$1/")
.replace(/\.git$/, "");
}
async function calculateGitChanges() {
await git("fetch");
const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {
const [author, hash, ...rest] = line.split("/");
return {
hash, author, message: rest.join("/")
};
}) : [];
}
async function pull() {
const res = await git("pull");
return res.stdout.includes("Fast-forward");
}
async function build() {
const opts = { cwd: VENCORD_SRC_DIR };
let res;
if (isFlatpak) res = await execFile("flatpak-spawn", ["--host", "node", "scripts/build/build.mjs"], opts);
else res = await execFile("node", ["scripts/build/build.mjs"], opts);
return !res.stderr.includes("Build failed");
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(build));

View File

@ -0,0 +1,91 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { VENCORD_USER_AGENT } from "@utils/constants";
import IpcEvents from "@utils/IpcEvents";
import { ipcMain } from "electron";
import { writeFile } from "fs/promises";
import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { get } from "../simpleGet";
import { calculateHashes, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, string][];
async function githubGet(endpoint: string) {
return get(API_BASE + endpoint, {
headers: {
Accept: "application/vnd.github+json",
// "All API requests MUST include a valid User-Agent header.
// Requests with no User-Agent header will be rejected."
"User-Agent": VENCORD_USER_AGENT,
// todo: perhaps add support for (optional) api token?
// unauthorised rate limit is 60 reqs/h
// https://github.com/settings/tokens/new?description=Vencord%20Updater
}
});
}
async function calculateGitChanges() {
const isOutdated = await fetchUpdates();
if (!isOutdated) return [];
const res = await githubGet(`/compare/${gitHash}...HEAD`);
const data = JSON.parse(res.toString("utf-8"));
return data.commits.map(c => ({
// github api only sends the long sha
hash: c.sha.slice(0, 7),
author: c.author.login,
message: c.commit.message
}));
}
async function fetchUpdates() {
const release = await githubGet("/releases/latest");
const data = JSON.parse(release.toString());
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
if (hash === gitHash)
return false;
data.assets.forEach(({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
PendingUpdates.push([name, browser_download_url]);
}
});
return true;
}
async function applyUpdates() {
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
);
PendingUpdates = [];
return true;
}
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates));

View File

@ -0,0 +1,19 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import(IS_STANDALONE ? "./http" : "./git");

39
src/modules.d.ts vendored Normal file
View File

@ -0,0 +1,39 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="standalone-electron-types"/>
declare module "~plugins" {
const plugins: Record<string, import("@utils/types").Plugin>;
export default plugins;
}
declare module "~git-hash" {
const hash: string;
export default hash;
}
declare module "~git-remote" {
const remote: string;
export default remote;
}
declare module "~fileContent/*" {
const content: string;
export default content;
}

107
src/patchWin32Updater.ts Normal file
View File

@ -0,0 +1,107 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { app, autoUpdater } from "electron";
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
import { basename, dirname, join } from "path";
const { setAppUserModelId } = app;
// Apparently requiring Discords updater too early leads into issues,
// copied this workaround from powerCord
app.setAppUserModelId = function (id: string) {
app.setAppUserModelId = setAppUserModelId;
setAppUserModelId.call(this, id);
patchUpdater();
};
function isNewer($new: string, old: string) {
const newParts = $new.slice(4).split(".").map(Number);
const oldParts = old.slice(4).split(".").map(Number);
for (let i = 0; i < oldParts.length; i++) {
if (newParts[i] > oldParts[i]) return true;
if (newParts[i] < oldParts[i]) return false;
}
return false;
}
function patchLatest() {
const currentAppPath = dirname(process.execPath);
const currentVersion = basename(currentAppPath);
const discordPath = join(currentAppPath, "..");
const latestVersion = readdirSync(discordPath).reduce((prev, curr) => {
return (curr.startsWith("app-") && isNewer(curr, prev))
? curr
: prev;
}, currentVersion as string);
if (latestVersion === currentVersion) return;
const app = join(discordPath, latestVersion, "resources", "app");
if (existsSync(app)) return;
console.info("[Vencord] Detected Host Update. Repatching...");
const patcherPath = join(__dirname, "patcher.js");
mkdirSync(app);
writeFileSync(join(app, "package.json"), JSON.stringify({
name: "discord",
main: "index.js"
}));
writeFileSync(join(app, "index.js"), `require(${JSON.stringify(patcherPath)});`);
}
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
// need to reinject
function patchUpdater() {
const main = require.main!;
const buildInfo = require(join(process.resourcesPath, "build_info.json"));
try {
if (buildInfo?.newUpdater) {
const autoStartScript = join(main.filename, "..", "autoStart", "win32.js");
const { update } = require(autoStartScript);
// New Updater Injection
require.cache[autoStartScript]!.exports.update = function () {
patchLatest();
update.apply(this, arguments);
};
} else {
const hostUpdaterScript = join(main.filename, "..", "hostUpdater.js");
const { quitAndInstall } = require(hostUpdaterScript);
// Old Updater Injection
require.cache[hostUpdaterScript]!.exports.quitAndInstall = function () {
patchLatest();
quitAndInstall.apply(this, arguments);
};
}
} catch {
// OpenAsar uses electrons autoUpdater on Windows
const { quitAndInstall } = autoUpdater;
autoUpdater.quitAndInstall = function () {
patchLatest();
quitAndInstall.call(this);
};
}
}

View File

@ -1,73 +1,180 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions } from "electron";
import installExt, { REACT_DEVELOPER_TOOLS } from "electron-devtools-installer";
import { join } from "path";
import { initIpc } from './ipcMain';
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { initIpc } from "./ipcMain";
import { installExt } from "./ipcMain/extensions";
import { readSettings } from "./ipcMain/index";
console.log("[Vencord] Starting up...");
class BrowserWindow extends electron.BrowserWindow {
constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) {
const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, "preload.js");
options.webPreferences.sandbox = false;
// Our injector file at app/index.js
const injectorPath = require.main!.filename;
process.env.DISCORD_PRELOAD = original;
// special discord_arch_electron injection method
const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
super(options);
initIpc(this);
} else super(options);
}
}
Object.assign(BrowserWindow, electron.BrowserWindow);
// esbuild may rename our BrowserWindow, which leads to it being excluded
// from getFocusedWindow(), so this is necessary
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
// The original app.asar
const asarPath = join(dirname(injectorPath), "..", asarName);
// Replace electrons exports with our custom BrowserWindow
const electronPath = require.resolve("electron");
delete require.cache[electronPath]!.exports;
require.cache[electronPath]!.exports = {
...electron,
BrowserWindow
};
const discordPkg = require(join(asarPath, "package.json"));
require.main!.filename = join(asarPath, discordPkg.main);
// Patch appSettings to force enable devtools
Object.defineProperty(global, "appSettings", {
set: (v: typeof global.appSettings) => {
v.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true);
// @ts-ignore
delete global.appSettings;
global.appSettings = v;
},
configurable: true
});
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
if (!process.argv.includes("--vanilla")) {
// Repatch after host updates on Windows
if (process.platform === "win32")
require("./patchWin32Updater");
electron.app.whenReady().then(() => {
installExt(REACT_DEVELOPER_TOOLS)
.then(() => console.info("Installed React DevTools"))
.catch((err) => console.error("Failed to install React DevTools", err));
class BrowserWindow extends electron.BrowserWindow {
constructor(options: BrowserWindowConstructorOptions) {
if (options?.webPreferences?.preload && options.title) {
const original = options.webPreferences.preload;
options.webPreferences.preload = join(__dirname, "preload.js");
options.webPreferences.sandbox = false;
// Remove CSP
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
if (responseHeaders) {
delete responseHeaders["content-security-policy-report-only"];
delete responseHeaders["content-security-policy"];
process.env.DISCORD_PRELOAD = original;
// Fix hosts that don't properly set the content type, such as
// raw.githubusercontent.com
if (url.endsWith(".css"))
responseHeaders["content-type"] = ["text/css"];
super(options);
initIpc(this);
} else super(options);
}
cb({ cancel: false, responseHeaders });
});
}
Object.assign(BrowserWindow, electron.BrowserWindow);
// esbuild may rename our BrowserWindow, which leads to it being excluded
// from getFocusedWindow(), so this is necessary
// https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62
Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true });
// Drop science and sentry requests
electron.session.defaultSession.webRequest.onBeforeRequest(
{ urls: ["https://*/api/v*/science", "https://sentry.io/*"] },
(_, callback) => callback({ cancel: true })
// Replace electrons exports with our custom BrowserWindow
const electronPath = require.resolve("electron");
delete require.cache[electronPath]!.exports;
require.cache[electronPath]!.exports = {
...electron,
BrowserWindow
};
// Patch appSettings to force enable devtools
onceDefined(global, "appSettings", s =>
s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true)
);
});
process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
electron.app.whenReady().then(() => {
// Source Maps! Maybe there's a better way but since the renderer is executed
// from a string I don't think any other form of sourcemaps would work
electron.protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => {
let url = unsafeUrl.slice("vencord://".length);
if (url.endsWith("/")) url = url.slice(0, -1);
switch (url) {
case "renderer.js.map":
case "preload.js.map":
case "patcher.js.map": // doubt
cb(join(__dirname, url));
break;
default:
cb({ statusCode: 403 });
}
});
try {
const settings = JSON.parse(readSettings());
if (settings.enableReactDevtools)
installExt("fmkadmapgofadopljbjfkapdkoienihi")
.then(() => console.info("[Vencord] Installed React Developer Tools"))
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
type PolicyResult = Record<string, string[]>;
const parsePolicy = (policy: string): PolicyResult => {
const result: PolicyResult = {};
policy.split(";").forEach(directive => {
const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g);
if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) {
result[directiveKey] = directiveValue;
}
});
return result;
};
const stringifyPolicy = (policy: PolicyResult): string =>
Object.entries(policy)
.filter(([, values]) => values?.length)
.map(directive => directive.flat().join(" "))
.join("; ");
function patchCsp(headers: Record<string, string[]>, header: string) {
if (header in headers) {
const csp = parsePolicy(headers[header][0]);
for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) {
csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"];
}
// TODO: Restrict this to only imported packages with fixed version.
// Perhaps auto generate with esbuild
csp["script-src"] ??= [];
csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com");
headers[header] = [stringifyPolicy(csp)];
}
}
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => {
if (responseHeaders) {
if (resourceType === "mainFrame")
patchCsp(responseHeaders, "content-security-policy");
// Fix hosts that don't properly set the css content type, such as
// raw.githubusercontent.com
if (resourceType === "stylesheet")
responseHeaders["content-type"] = ["text/css"];
}
cb({ cancel: false, responseHeaders });
});
});
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}
console.log("[Vencord] Loading original Discord app.asar");
// Legacy Vencord Injector requires "../app.asar". However, because we
// restore the require.main above this is messed up, so monkey patch Module._load to
// redirect such requires
// FIXME: remove this eventually
if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
console.warn("[Vencord] [--> WARNING <--] You have a legacy Vencord install. Please reinject");
const Module = require("module");
const loadModule = Module._load;
Module._load = function (path: string) {
if (path === "../app.asar") {
Module._load = loadModule;
arguments[0] = require.main!.filename;
}
return loadModule.apply(this, arguments);
};
} else {
require(require.main!.filename);
}

View File

@ -0,0 +1,61 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import { makeLazy } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
export default definePlugin({
name: "BetterNotesBox",
description: "Hide notes or disable spellcheck (Configure in settings!!)",
authors: [Devs.Ven],
patches: [
{
find: "hideNote:",
all: true,
predicate: makeLazy(() => Vencord.Settings.plugins.BetterNotesBox.hide),
replacement: {
match: /hideNote:.+?(?=[,}])/g,
replace: "hideNote:true",
}
}, {
find: "Messages.NOTE_PLACEHOLDER",
replacement: {
match: /\.NOTE_PLACEHOLDER,/,
replace: "$&spellCheck:!Vencord.Settings.plugins.BetterNotesBox.noSpellCheck,"
}
}
],
options: {
hide: {
type: OptionType.BOOLEAN,
description: "Hide notes",
default: false,
restartNeeded: true
},
noSpellCheck: {
type: OptionType.BOOLEAN,
description: "Disable spellcheck in notes",
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
}
});

View File

@ -1,15 +0,0 @@
import definePlugin from "../utils/types";
import { Devs } from '../utils/constants';
export default definePlugin({
name: "STFU",
description: "Disables the 'HOLD UP' banner in the console",
authors: [Devs.Ven],
patches: [{
find: "setDevtoolsCallbacks",
replacement: {
match: /\.setDevtoolsCallbacks\(.+?else/,
replace: ".setDevtoolsCallbacks(null,null);else"
}
}]
});

Some files were not shown because too many files have changed in this diff Show More