Compare commits

...

190 Commits

Author SHA1 Message Date
A user
62b2acebe6 Add support for Flatpak for Git updating (#274)
Co-authored-by: Ven <vendicated@riseup.net>
2022-12-02 16:55:53 +01:00
Justice Almanzar
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
12944qwerty
4760af7f0e add ViewRaw plugin & MiniPopover API (#275)
Co-authored-by: Vendicated <vendicated@riseup.net>
2022-12-02 16:38:52 +01:00
Vendicated
06d32ae414 browser: remove firefox extension id 2022-12-02 14:24:23 +01:00
Vendicated
2564ab73f5 ci: unlisted firefox builds for now 2022-12-02 14:21:44 +01:00
Vendicated
5e97cc0fc3 QuickCss: Hide MenuBar; explicitly enable contextIsolation
Closes #260
2022-12-02 14:11:20 +01:00
Vendicated
b9e9d9bd64 Add --vanilla flag, strip csp on mainFrame only 2022-12-02 14:10:40 +01:00
Vendicated
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
Roman / Linnea Gräf
0ff6d3dd41 Add Firefox extension build (#277) 2022-12-01 19:16:09 +01:00
Vendicated
734054ff68 feat(Settings): Allow moving Vencord section to different places 2022-12-01 03:38:17 +01:00
Vendicated
f94cbfb2f4 Add basic themes tab 2022-12-01 03:01:44 +01:00
Sofia
fc09460d82 feat(plugin): add ServerListIndicators (#272) 2022-11-29 00:25:07 +01:00
Vendicated
e884738f42 MemberCount: Fix misleading count, add tooltip 2022-11-28 23:01:09 +01:00
megumin
c583bad6bf the shiggy wiggy (#270) 2022-11-28 18:59:42 +00:00
Vendicated
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
Vendicated
d0a40bc0ed chore: update deps 2022-11-28 15:59:15 +01:00
Vendicated
3b4879f9d9 perf(settings): Cache proxies 2022-11-28 15:44:53 +01:00
Vendicated
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
Vendicated
7a4402f142 BlurNSFW: Support videos 2022-11-28 01:08:58 +01:00
Vendicated
3e9672c6b8 oop 2022-11-28 00:58:26 +01:00
Vendicated
a9fee6248e BlurNSFW: Add amount setting 2022-11-28 00:55:50 +01:00
Vendicated
f0ee16f173 [skip ci] update genPluginList 2022-11-28 00:45:41 +01:00
Vendicated
3db3c63b42 BlurNsfw plugin 2022-11-28 00:42:42 +01:00
megumin
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
Hana
47c181beec [skip ci] Add support for DVM installation path. (#265) 2022-11-27 14:32:54 +01:00
megumin
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
megumin
6b55dee9fb feat(settings): new settings design (#261) 2022-11-25 22:38:55 +00:00
Vendicated
a85ec594a7 [skip ci] docs docs docs 2022-11-25 19:25:35 +01:00
Vendicated
c2c6c9fccb CallTimer: Fix lag 2022-11-25 18:28:15 +01:00
Vendicated
b60f6cb18d WhoReacted: Make more reliable & don't spam api 2022-11-25 18:07:29 +01:00
Vendicated
bb398970ef HideAttachments: Fix embeds
Closes #259
2022-11-25 18:06:31 +01:00
Vendicated
50a96e8047 CallTimer: Fix typo 2022-11-25 16:16:07 +01:00
Vendicated
c5b5b754e2 CallTimer 2022-11-25 15:59:47 +01:00
KraXen72
0f644dff73 loadingQuotes quote fix (#255) 2022-11-24 14:26:38 +01:00
Snare-Hawk
6210d3a597 Make ReviewDB Look More Native (#256) 2022-11-24 14:26:18 +01:00
Nico
e7573382fe fix(betterNotes): add restart needed for hide notes patch (#258) 2022-11-24 14:02:11 +01:00
Vendicated
f4d7a1f4fb New Plugin: BetterNotesBox 2022-11-24 02:02:15 +01:00
Vendicated
5dd0a3a746 New Plugin: HideAttachments 2022-11-24 01:00:13 +01:00
Ven
c9fac8ffff fix tags 2022-11-23 20:04:25 +01:00
KraXen72
f93607fc66 add new quotes to loadingQuotes (#254)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-23 20:00:19 +01:00
Luna
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
Nuckyz
91f1d68e29 feat(plugins): Keep Current Channel plugin (#248) 2022-11-23 02:51:45 +01:00
Nuckyz
7e4f4f1794 feat(plugins): Volume Booster plugin (#249) 2022-11-22 23:22:54 +01:00
megumin
9f7ec0aa8d settings: better button text for plugin settings modal (#251) 2022-11-22 22:05:46 +00:00
Vendicated
0239bb0aac Commands: Show plugin name instead of 'Built-In' 2022-11-22 22:42:22 +01:00
Vendicated
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
CanadaHonk
1f72a0fc27 fix(arRPC): fix error on null activity (#244) 2022-11-21 20:40:40 +01:00
megumin
31ec1ec1b4 better platformindicators settings (#243) 2022-11-21 20:12:46 +01:00
Nickyux
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
Kareem Olim
b42b8d755f Platform indicators: ignore unnecessary element (#240) 2022-11-21 19:25:21 +01:00
megumin
bfe1fd9912 fix: add keys to plugins grid (#237) 2022-11-21 18:45:22 +01:00
Cloudburst
c45d89697a make userscript autoincrement version :trollface: (#233) 2022-11-21 16:32:56 +01:00
Vendicated
0a92bd6521 PlatformIndicators: Fix server list 2022-11-21 15:59:19 +01:00
Kareem Olim
33c33eb0fd feat(plugin): PlatformIndicators (#227)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-21 15:44:30 +01:00
obscurity
dcf1148bb4 feat(plugin): TimeBarAllActivities (#228) 2022-11-21 11:53:28 +01:00
obscurity
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
Sofia
bb14d4989d feat(plugin): NoUnblockToJump (#229) 2022-11-21 03:40:15 +01:00
CanadaHonk
9bcdc8451f feat(arRPC): update for server 2.2 (#230) 2022-11-21 00:57:30 +01:00
Nuckyz
46b14cb2e0 feat(plugins):WhoReacted keep reaction count (#231) 2022-11-21 00:56:17 +01:00
CanadaHonk
9240865f65 feat(arRPC): update for server 2.0 (#224) 2022-11-20 16:21:42 +01:00
CanadaHonk
e85d763f22 feat(plugin): WebRichPresence (arRPC) (#223) 2022-11-20 14:31:00 +01:00
Vendicated
82911386db oop 2022-11-19 22:17:55 +01:00
Vendicated
e63ed9cac4 onekocord 2022-11-19 22:13:16 +01:00
Sofia
ba45ecda56 feat(plugin): Last.fm rich presence (#220)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-19 18:40:52 +01:00
megumin
7ff2d2ba8a fix startup timings page (#219) 2022-11-19 15:52:17 +00:00
Kareem Olim
a5154d6283 feat(plugin): Quick mention button (#218)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-19 16:11:11 +01:00
Kareem Olim
5ce2dc1bb4 feat(plugin): Read all notifications button (#217)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-19 14:54:48 +01:00
Vendicated
8f2c247f27 Fix commands showing up multiple times Part 2 2022-11-18 23:31:53 +01:00
Vendicated
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
Berlin
503a2ec517 Add option to ignore incoming blocked messages (#179) 2022-11-18 05:12:45 +01:00
Vendicated
83b3b1f16b fix settings debug info on web 2022-11-17 13:49:51 +01:00
Vendicated
2628bdce42 WebContextMenus: Port copy/open link items to Discord Web 2022-11-17 01:30:23 +01:00
Vendicated
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
Vendicated
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
Vendicated
a788813383 VencordWeb: Migrate to manifest v3 2022-11-16 16:23:52 +01:00
Vendicated
e1de6f88fe Unexplode Modals on canary 2022-11-16 14:52:05 +01:00
Vendicated
ae86848cf6 Fix ReviewDB 2022-11-16 01:02:23 +01:00
Manti
84ec839b04 Add ReviewDB Plugin (#187)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-16 00:40:46 +01:00
Vendicated
b30508aef8 better handling for settings ui errors 2022-11-15 17:29:31 +01:00
Nico
eabbf7d9bd fix(fakeNitro): add missing predicate for sticker bypass (#215) 2022-11-15 09:34:53 +01:00
Vendicated
be088f9072 Don't unnecessarily create functions many times 2022-11-15 09:30:33 +01:00
Vendicated
2ca98a87d2 Fix Settings UI on canary 2022-11-15 09:28:06 +01:00
Vendicated
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
rushii
0dbec8d0cd feat: message logger plugin (#49)
Co-authored-by: Ven <vendicated@riseup.net>
2022-11-13 23:13:32 +01:00
Vendicated
e5b23ff556 EmoteYoink: Don't depend on ReverseImageSearch whoopsie 2022-11-13 04:12:37 +01:00
Vendicated
9110d1f9bd Emote Clone plugin 2022-11-13 03:46:46 +01:00
Vendicated
81edc14070 fix PronounDB crash with new profile in dms, force start dependencies 2022-11-12 17:20:19 +01:00
Vendicated
b48c8d8a4a NitroSpoof: Fix inbuilt sticker logic; cleanup 2022-11-12 16:25:28 +01:00
Vendicated
8380328465 InteractionKeybinds: Scroll to message if offscreen
Closes https://github.com/Vendicated/Vencord/issues/200
2022-11-12 00:29:36 +01:00
megumin
30ca4f1cf9 feat: Badge API (#206) 2022-11-11 23:50:09 +01:00
Nuckyz
62e0787cf2 fix(plugins): Fix IgnoreActivities (#205) 2022-11-11 19:02:03 +01:00
Vendicated
cc7c14ec88 Reporting: do not error for patches with all:true 2022-11-11 19:01:01 +01:00
Vendicated
a86452e774 fix react hook error 2022-11-11 18:58:30 +01:00
Vendicated
dddb28192c even more plugin fixes 2022-11-11 18:49:47 +01:00
Vendicated
2133823bd3 more plugin fixes 2022-11-11 16:43:40 +01:00
Vendicated
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
Vendicated
409e54a9d8 ci(reporter): Post results to discord webhook 2022-11-11 13:27:44 +01:00
Vendicated
31fb19b8c9 ci: Hopefully fix reporting 2022-11-11 13:06:04 +01:00
Vendicated
a26f636c9b ci: Automated plugin test with puppeteer 2022-11-11 12:37:37 +01:00
Vendicated
8ba9c96f20 Fix most plugins 2022-11-11 00:11:44 +01:00
Jānis
57f3feba68 spotifyControls: make album of local tracks unclickable (#203) 2022-11-10 19:33:00 +01:00
megumin
010523eeac feat(plugins): add vc effect event to moyai plugin (#199) 2022-11-10 14:04:06 +01:00
Nico
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
Vendicated
58636a9a82 CorruptMp4s: Depend on CommandsAPI ~ PronounDB: Add pronoundb link 2022-11-09 23:01:59 +01:00
Vendicated
0bc894d065 CorruptMp4s: Better default 2022-11-09 21:17:21 +01:00
Vendicated
6f38c4b7fe new plugin(CorruptMp4s): Mp4s with infinite/negative duration 2022-11-09 21:15:52 +01:00
Vendicated
c1d2f0078f StickerSpoof: Fix not correctly cleaning previous frame 2022-11-09 20:29:35 +01:00
Vendicated
3c8084ec36 Add VSCode debug config 2022-11-09 19:26:46 +01:00
Nico
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
Vendicated
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
Vendicated
1e6b967d24 Fix moyai and fart 2022-11-09 12:47:16 +01:00
Vendicated
460f329e4f fix double click actions using outdated content 2022-11-08 18:09:11 +01:00
Nuckyz
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
Vendicated
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
Vendicated
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
Nico
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
Vendicated
a13c0df1cd build: Add metadata header to all bundles 2022-11-07 21:29:31 +01:00
Vendicated
0af4579204 Add tracer, fix MessageActions slow startup 2022-11-07 21:05:33 +01:00
Vendicated
851d07f31a fix(ReverseImageSearch): Don't apply to non image files 2022-11-07 18:52:34 +01:00
Vendicated
963a7332b4 Migrate proxied components to and fix LazyComponent 2022-11-06 18:37:01 +01:00
Vendicated
440baf6028 Improve proxyLazy 2022-11-06 18:00:59 +01:00
megumin
9663e229a6 feat(plugins): add Startup Timings (#189) 2022-11-05 11:09:05 +01:00
megumin
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
Vendicated
65620f4976 Webpack: Do not emit errors if devtools open 2022-11-03 20:36:17 +01:00
Vendicated
cb7469afad Simplemarkdown pleeeeease 2022-11-03 19:15:51 +01:00
Vendicated
2c3dee4120 qol improvements 2022-11-03 19:12:50 +01:00
Vendicated
c20dc269d2 Modify CSP instead of deleting it 2022-11-02 22:15:55 +01:00
Vendicated
a7795533df Remove clipboardImageFix - Discord fixed the bug woooooo 2022-11-02 20:54:39 +01:00
Vendicated
5e1b42120c Fix plugins on new update 2022-11-02 20:13:55 +01:00
Vendicated
676f5c7e30 ViewIcons: size 2048 -> 512, to fit on screen 2022-11-02 17:30:15 +01:00
Vendicated
13c73699e9 Fix Webpack modules that are not arrow funcs, Part II 2022-11-01 15:06:15 +01:00
Vendicated
64aed87de4 Fix Webpack modules that are not arrow funcs 2022-11-01 14:28:25 +01:00
Nickyux
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
rushii
0c25278c59 fix renamed app.asar detection on Windows (#185) 2022-11-01 01:47:07 +01:00
Vendicated
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
Snek
b905743077 removed channel type (#170)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-30 19:17:46 +01:00
Nico
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
Vendicated
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
Vendicated
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
Cynthia Foxwell
37105ac416 feat(plugin): ClipboardImageFix (#173) 2022-10-29 20:25:40 +02:00
Vendicated
f6e0efe20a Reverse image search plugin 2022-10-29 15:25:34 +02:00
Vendicated
1764206e19 Add MenuItemDeobfuscator 2022-10-29 15:25:10 +02:00
Nuckyz
6b0caaae37 fix(ShowHiddenChannels): Fix New unreads box for hidden channels #168 2022-10-27 20:26:54 +02:00
Snek
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
Nuckyz
9548978d80 fix(IgnoreActivities): id -> exePath (#164)
Co-authored-by: Ven <vendicated@riseup.net>
2022-10-27 10:27:52 +02:00
megumin
13882b5732 feat: custom components in settings (#165) 2022-10-26 23:42:26 +02:00
Jakup
49e72bab32 moarKaomojis plugin (#137) 2022-10-26 15:38:41 +02:00
Nuckyz
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
Vendicated
c65f757bc4 Fix betterUploadButton 2022-10-26 15:10:11 +02:00
Vendicated
b87f0bf3f9 Settings: Cache default value 2022-10-26 14:28:27 +02:00
Vendicated
670b7d7d01 Make tsc happy 2022-10-26 13:54:23 +02:00
Vendicated
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
Vendicated
fe5a78ddc9 Update README 2022-10-25 21:40:23 +02:00
megumin
5e7c155f6e feat(settings): add beforeSave check (#161) 2022-10-25 18:49:50 +01:00
Nico
e06ba68c40 fix(vcDoubleClick): add required parentheses (#160) 2022-10-25 18:15:21 +02:00
Nico
d6fe937a70 fix(vcDoubleClick): exclude text channel mentions (#159) 2022-10-25 18:09:21 +02:00
Nico
2f46b934c9 feat: add new plugin ForceOwnerCrown (#157) 2022-10-25 17:32:05 +02:00
Vendicated
2af324c302 Improve genPluginList 2022-10-25 12:29:55 +02:00
Vendicated
4bddcee40b Add autogenerated plugin list, closes #151 2022-10-25 12:20:29 +02:00
Nico
559edbfffe Fix vcDoubleClick, add support for stage channels (#158) 2022-10-25 10:53:06 +02:00
Nuckyz
6c38362401 Ignore Activities: Fix button not working (#156)
explode
2022-10-25 00:18:30 +02:00
Nuckyz
00402c69d6 feat(plugin): Ignore Activities (#142) 2022-10-25 00:05:40 +02:00
Nico
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
213 changed files with 12514 additions and 1463 deletions

View File

@ -2,7 +2,26 @@
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "browser"],
"plugins": ["header", "simple-import-sort", "unused-imports"],
"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
@ -88,6 +107,8 @@
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "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']

View File

@ -1,8 +1,15 @@
name: Build latest
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
@ -15,42 +22,43 @@ jobs:
- uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 18
uses: actions/setup-node@v2
- name: Use Node.js 19
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 19
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build web
run: pnpm buildWeb
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: Get some values needed for the release
id: vars
shell: bash
- name: Rename extensions for more user friendliness
run: |
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
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
- uses: dev-drprasad/delete-tag-and-release@085c6969f18bad0de1b9f3fe6692a3cd01f64fe5 # v0.2.0
with:
delete_release: true
tag_name: devbuild
- 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 }}
- name: Create the release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: devbuild
name: Dev Build ${{ steps.vars.outputs.sha_short }}
draft: false
prerelease: false
files: |
dist/*
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 }}

View File

@ -23,5 +23,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint & Test if it compiles
- name: Lint & Test if desktop version compiles
run: pnpm test
- name: Lint & Test if web version compiles
run: pnpm testWeb

3
.gitignore vendored
View File

@ -18,3 +18,6 @@ lerna-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"]
}
]
}

View File

@ -4,21 +4,26 @@ A Discord client mod that does things differently
## Features
- Works on Discord's latest update that breaks all other mods
- Browser Support (experimental): Run Vencord in your Browser instead of the desktop app
- Custom Css and Themes: Manually edit `%appdata%/Vencord/settings/quickCss.css` / `~/.config/Vencord/settings/quickCss.css` with your favourite editor and the client will automatically apply your changes. To import BetterDiscord themes, just add `@import url(theUrl)` on the top of this file. (Make sure the url is a github raw URL or similar and only contains plain text, and NOT a nice looking website)
- Many Useful™ plugins - [List](https://github.com/Vendicated/Vencord/tree/main/src/plugins)
- Experiments
- Proper context isolation -> Works in newer Electron versions (Confirmed working on versions 13-22)
- 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!)
- 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 / Uninstalling
Read [Megu's Installation Guide!](docs/1_INSTALLING.md)
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
Run the same commands as in the regular install method. Now run
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
```sh
pnpm buildWeb
@ -28,7 +33,11 @@ You will find the built extension at dist/extension.zip. Now just install this e
## 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!

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IpcEvents from "../src/utils/IpcEvents";
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;

View File

@ -1,24 +1,48 @@
if (typeof browser === "undefined") {
var browser = chrome;
/*
* 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 };
}
browser.webRequest.onHeadersReceived.addListener(({ responseHeaders, url }) => {
const cspIdx = responseHeaders.findIndex(h => h.name === "content-security-policy");
if (cspIdx !== -1)
responseHeaders.splice(cspIdx, 1);
if (url.endsWith(".css")) {
const contentType = responseHeaders.find(h => h.name === "content-type");
if (contentType)
contentType.value = "text/css";
else
responseHeaders.push({
name: "content-type",
value: "text/json"
});
}
var cspHeaders = [
"content-security-policy",
"content-security-policy-report-only",
];
function removeCSPHeaders(details) {
return {
responseHeaders
responseHeaders: details.responseHeaders.filter(header =>
!cspHeaders.includes(header.name.toLowerCase()))
};
}, { urls: ["*://*.discord.com/*"] }, ["blocking", "responseHeaders"]);
}
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"]
);

View File

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

@ -1,3 +1,6 @@
> **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!

View File

@ -15,7 +15,7 @@ You don't need to run `pnpm build` every time you make a change. Instead, use `p
3. In `index.ts`, copy-paste the following template code:
```ts
import definePlugin from "../../utils/types";
import definePlugin from "@utils/types";
export default definePlugin({
name: "Epic Plugin",

View File

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.0.0",
"version": "1.0.1",
"description": "A Discord client mod that does things differently",
"keywords": [],
"homepage": "https://github.com/Vendicated/Vencord#readme",
@ -24,28 +24,52 @@
"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": {
"console-menu": "^0.1.0",
"fflate": "^0.7.4"
},
"devDependencies": {
"@types/node": "^18.7.13",
"@types/react": "^18.0.17",
"@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/parser": "^5.39.0",
"@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.5",
"eslint": "^8.24.0",
"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.1.0",
"typescript": "^4.8.4"
"type-fest": "^3.3.0",
"typescript": "^4.9.3"
},
"packageManager": "pnpm@7.13.4"
"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;
}

1238
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -19,22 +19,17 @@
import esbuild from "esbuild";
import { commonOpts, gitHash, globPlugins, isStandalone } from "./common.mjs";
import { commonOpts, globPlugins, isStandalone, watch } from "./common.mjs";
const defines = {
IS_STANDALONE: isStandalone
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);
const header = `
// Vencord ${gitHash}
// Standalone: ${defines.IS_STANDALONE}
// Platform: ${defines["process.platform"] || "Universal"}
`.trim();
/**
* @type {esbuild.BuildOptions}
*/
@ -47,25 +42,25 @@ const nodeCommonOpts = {
bundle: true,
external: ["electron", ...commonOpts.external],
define: defines,
banner: {
js: header
}
};
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//# sourceMappingURL=vencord://preload.js.map" },
sourcemap: "external",
footer: { js: "//# sourceURL=VencordPreload\n" + sourceMapFooter("preload") },
sourcemap,
}),
esbuild.build({
...nodeCommonOpts,
entryPoints: ["src/patcher.ts"],
outfile: "dist/patcher.js",
footer: { js: "//# sourceURL=VencordPatcher\n//# sourceMappingURL=vencord://patcher.js.map" },
sourcemap: "external",
footer: { js: "//# sourceURL=VencordPatcher\n" + sourceMapFooter("patcher") },
sourcemap,
}),
esbuild.build({
...commonOpts,
@ -73,16 +68,16 @@ await Promise.all([
outfile: "dist/renderer.js",
format: "iife",
target: ["esnext"],
footer: { js: "//# sourceURL=VencordRenderer\n//# sourceMappingURL=vencord://renderer.js.map" },
footer: { js: "//# sourceURL=VencordRenderer\n" + sourceMapFooter("renderer") },
globalName: "Vencord",
sourcemap: "external",
sourcemap,
plugins: [
globPlugins,
...commonOpts.plugins
],
define: {
IS_WEB: "false",
IS_STANDALONE: isStandalone
...defines,
IS_WEB: false
}
}),
]).catch(err => {

View File

@ -20,13 +20,13 @@
import esbuild from "esbuild";
import { zip } from "fflate";
import { readFileSync, writeFileSync } from "fs";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { readFile } from "fs/promises";
import { join } from "path";
import { join, resolve } from "path";
// wtf is this assert syntax
import PackageJSON from "../../package.json" assert { type: "json" };
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins } from "./common.mjs";
import { commonOpts, fileIncludePlugin, gitHashPlugin, gitRemotePlugin, globPlugins, watch } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
@ -46,7 +46,8 @@ const commonOptions = {
target: ["esnext"],
define: {
IS_WEB: "true",
IS_STANDALONE: "true"
IS_STANDALONE: "true",
IS_DEV: JSON.stringify(watch)
}
};
@ -61,7 +62,7 @@ await Promise.all(
...commonOptions,
outfile: "dist/Vencord.user.js",
banner: {
js: readFileSync("browser/userscript.meta.js", "utf-8").replace("%version%", PackageJSON.version)
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
@ -71,20 +72,39 @@ await Promise.all(
]
);
zip({
dist: {
"Vencord.js": readFileSync("dist/browser.js")
},
...Object.fromEntries(await Promise.all(["background.js", "content.js", "manifest.json"].map(async f => [
f,
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))
]))),
}, {}, (err, data) => {
};
if (shouldZip) {
zip(entries, {}, (err, data) => {
if (err) {
console.error(err);
process.exitCode = 1;
} else {
writeFileSync("dist/extension.zip", data);
console.info("Extension written to dist/extension.zip");
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);

View File

@ -17,7 +17,6 @@
*/
import { exec, execSync } from "child_process";
import esbuild from "esbuild";
import { existsSync } from "fs";
import { readdir, readFile } from "fs/promises";
import { join } from "path";
@ -25,6 +24,14 @@ 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
/**
@ -79,7 +86,6 @@ export const globPlugins = {
}
};
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
/**
* @type {esbuild.Plugin}
*/
@ -142,7 +148,7 @@ export const fileIncludePlugin = {
};
/**
* @type {esbuild.BuildOptions}
* @type {import("esbuild").BuildOptions}
*/
export const commonOpts = {
logLevel: "info",
@ -151,6 +157,12 @@ export const commonOpts = {
minify: !watch,
sourcemap: watch ? "inline" : "",
legalComments: "linked",
banner,
plugins: [fileIncludePlugin, gitHashPlugin, gitRemotePlugin],
external: ["~plugins", "~git-hash", "~git-remote"]
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);
})();

View File

@ -59,6 +59,7 @@ const LINUX_DISCORD_DIRS = [
"/usr/lib64",
"/opt",
`${process.env.HOME}/.local/share`,
`${process.env.HOME}/.dvm`,
"/var/lib/flatpak/app",
`${process.env.HOME}/.local/share/flatpak/app`,
];

View File

@ -39,6 +39,7 @@ const {
getDarwinDirs,
getLinuxDirs,
ENTRYPOINT,
question
} = require("./common");
switch (process.platform) {
@ -62,15 +63,14 @@ async function install(installations) {
// Attempt to give flatpak perms
if (selected.isFlatpak) {
try {
const { branch } = selected;
const cwd = process.cwd();
const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`;
const userCmd = `flatpak override --user ${branch} --filesystem=${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("Successfully gave write perms to Discord Flatpak.");
console.log("Gave write perms to Discord Flatpak.");
} catch (e) {
console.log("Failed to give write perms to Discord Flatpak.");
console.log(
@ -79,6 +79,29 @@ async function install(installations) {
);
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) {

View File

@ -18,20 +18,19 @@
export * as Api from "./api";
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 { PlainSettings, Settings } from "./api/settings";
import { startAllPlugins } from "./plugins";
export { PlainSettings,Settings };
import "./webpack/patchWebpack";
import "./utils/quickCss";
import { patches, PMLogger, startAllPlugins } from "./plugins";
import { checkForUpdates, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { Router } from "./webpack/common";
@ -61,6 +60,19 @@ async function init() {
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")
);
}
}
init();

View File

@ -16,10 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import IPC_EVENTS from "@utils/IpcEvents";
import { IpcRenderer, ipcRenderer } from "electron";
import IPC_EVENTS from "./utils/IpcEvents";
function assertEventAllowed(event: string) {
if (!(event in IPC_EVENTS)) throw new Error(`Event ${event} not allowed.`);
}

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

@ -16,15 +16,15 @@
* 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 { lazyWebpack, mergeDefaults } from "../../utils/misc";
import { filters, waitFor } from "../../webpack";
import { Argument } from "./types";
const createBotMessage = lazyWebpack(filters.byCode('username:"Clyde"'));
const MessageSender = lazyWebpack(filters.byProps(["receiveMessage"]));
const createBotMessage = findByCodeLazy('username:"Clyde"');
const MessageSender = findByPropsLazy("receiveMessage");
let SnowflakeUtils: any;
waitFor("fromTimestamp", m => SnowflakeUtils = m);

View File

@ -16,9 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { makeCodeblock } from "../../utils/misc";
import { generateId, sendBotMessage } from "./commandHelpers";
import { ApplicationCommandInputType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
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";
@ -79,7 +80,12 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com
}
} as never;
function modifyOpt(opt: Option | Command) {
/**
* 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) => {
@ -88,11 +94,36 @@ function modifyOpt(opt: Option | Command) {
else if (opt === ReqPlaceholder) opts[i] = RequiredMessageOption;
opt.choices?.forEach(x => x.displayName ||= x.name);
modifyOpt(opts[i]);
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(command: Command, plugin: string) {
export function registerCommand<C extends Command>(command: C, plugin: string) {
if (!BUILT_IN) {
console.warn(
"[CommandsAPI]",
@ -106,13 +137,19 @@ export function registerCommand(command: Command, plugin: string) {
throw new Error(`Command '${command.name}' already exists.`);
command.isVencordCommand = true;
command.id ??= generateId();
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;
modifyOpt(command);
prepareOption(command);
if (command.options?.[0]?.type === ApplicationCommandOptionType.SUB_COMMAND) {
registerSubCommands(command, plugin);
return;
}
commands[command.name] = command;
BUILT_IN.push(command);
}

View File

@ -81,6 +81,7 @@ export interface Argument {
name: string;
value: string;
focused: undefined;
options: Argument[];
}
export interface Command {

View File

@ -16,10 +16,10 @@
* 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";
import Logger from "../utils/logger";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
export interface Emoji {
@ -37,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);
}
}
}
@ -87,10 +99,14 @@ type ClickListener = (message: Message, channel: Channel, event: MouseEvent) =>
const listeners = new Set<ClickListener>();
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

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { waitFor } from "../webpack";
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

@ -16,11 +16,14 @@
* 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
@ -57,5 +60,17 @@ 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 { Commands,DataStore, MessageAccessories, MessageEvents, Notices };
export { Badges, Commands, DataStore, MessageAccessories, MessageEvents, MessagePopover, Notices, ServerList };

View File

@ -16,17 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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";
import IpcEvents from "../utils/IpcEvents";
import { mergeDefaults } from "../utils/misc";
import { OptionType } from "../utils/types";
import { React } from "../webpack/common";
const logger = new Logger("Settings");
export interface Settings {
notifyAboutUpdates: boolean;
useQuickCss: boolean;
enableReactDevtools: boolean;
themeLinks: string[];
plugins: {
[plugin: string]: {
enabled: boolean;
@ -38,35 +41,38 @@ export interface Settings {
const DefaultSettings: Settings = {
notifyAboutUpdates: true,
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;
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>();
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: Settings, root = settings, path = ""): Settings {
return new Proxy(settings, {
function makeProxy(settings: any, root = settings, path = ""): Settings {
return proxyCache[path] ??= new Proxy(settings, {
get(target, p: string) {
const v = target[p];
// 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.")) {
@ -76,9 +82,13 @@ function makeProxy(settings: Settings, root = settings, path = ""): Settings {
if (!setting) return v;
if ("default" in setting)
// normal setting with a default value
return setting.default;
if (setting.type === OptionType.SELECT)
return setting.options.find(o => o.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;
@ -131,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;
@ -165,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

@ -16,13 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Logger from "../utils/logger";
import { Margins, React } from "../webpack/common";
import Logger from "@utils/Logger";
import { LazyComponent } from "@utils/misc";
import { Margins, React } from "@webpack/common";
import { ErrorCard } from "./ErrorCard";
interface Props {
/** 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;
}
@ -32,15 +39,10 @@ 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: "",
@ -71,6 +73,8 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
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}
@ -96,4 +100,16 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
</ErrorCard>
);
}
}
};
}) as
React.ComponentType<React.PropsWithChildren<Props>> & {
wrap<T extends JSX.IntrinsicAttributes = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Props): React.ComponentType<T>;
};
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
<ErrorBoundary {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundary>
);
export default ErrorBoundary;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Card } from "../webpack/common";
import { Card } from "@webpack/common";
interface Props {
style?: React.CSSProperties;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { React } from "../webpack/common";
import type { React } from "@webpack/common";
export function Flex(props: React.PropsWithChildren<{
flexDirection?: React.CSSProperties["flexDirection"];
@ -24,9 +24,11 @@ export function Flex(props: React.PropsWithChildren<{
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

@ -16,21 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { React } from "../webpack/common";
import { React } from "@webpack/common";
interface Props {
href: string;
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>
);

View File

@ -16,16 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import monacoHtml from "~fileContent/monacoWin.html";
import { debounce } from "@utils/debounce";
import IpcEvents from "@utils/IpcEvents";
import { Queue } from "@utils/Queue";
import { find } from "@webpack";
import { IpcEvents } from "../utils";
import { debounce } from "../utils/debounce";
import { Queue } from "../utils/Queue";
import { find } from "../webpack/webpack";
import monacoHtml from "~fileContent/monacoWin.html";
const queue = new Queue();
const setCss = debounce((css: string) => {
queue.add(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
queue.push(() => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, css));
});
export async function launchMonacoEditor() {
@ -33,10 +33,12 @@ export async function launchMonacoEditor() {
win.setCss = setCss;
win.getCurrentCss = () => VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS);
win.getTheme = () => find(m => m.ProtoClass?.typeName.endsWith("PreloadedUserSettings"))
.getCurrentValue().appearance.theme === 1
? "vs-dark"
: "vs-light";
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

@ -16,28 +16,31 @@
* 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 { generateId } from "../../api/Commands";
import { useSettings } from "../../api/settings";
import { lazyWebpack, proxyLazy } from "../../utils";
import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "../../utils/modal";
import { OptionType, Plugin } from "../../utils/types";
import { filters } from "../../webpack";
import { Button, FluxDispatcher, Forms, React, Text, Tooltip, UserStore, UserUtils } from "../../webpack/common";
import ErrorBoundary from "../ErrorBoundary";
import { Flex } from "../Flex";
import {
ISettingElementProps,
SettingBooleanComponent,
SettingInputComponent,
SettingCustomComponent,
SettingNumericComponent,
SettingSelectComponent,
SettingSliderComponent,
SettingTextComponent
} from "./components";
const UserSummaryItem = lazyWebpack(filters.byCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
const AvatarStyles = lazyWebpack(filters.byProps(["moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"]));
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 {
@ -59,6 +62,16 @@ function makeDummyUser(user: { name: string, id: BigInt; }) {
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>[]>([]);
@ -67,23 +80,35 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
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(() => null) : makeDummyUser(user);
setAuthors(a => [...a, author || makeDummyUser(user)]);
const author = user.id
? await UserUtils.fetchUser(`${user.id}`).catch(() => makeDummyUser(user))
: makeDummyUser(user);
setAuthors(a => [...a, author]);
}
})();
}, []);
function saveAndClose() {
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];
@ -100,9 +125,8 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
return <Forms.FormText>There are no settings for this plugin.</Forms.FormText>;
}
const options: JSX.Element[] = [];
for (const [key, setting] of Object.entries(plugin.options)) {
function onChange(newValue) {
const options = Object.entries(plugin.options).map(([key, setting]) => {
function onChange(newValue: any) {
setTempSettings(s => ({ ...s, [key]: newValue }));
}
@ -110,31 +134,19 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
setErrors(e => ({ ...e, [key]: hasError }));
}
const props = { onChange, pluginSettings, id: key, onError };
switch (setting.type) {
case OptionType.SELECT: {
options.push(<SettingSelectComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.STRING: {
options.push(<SettingInputComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.NUMBER:
case OptionType.BIGINT: {
options.push(<SettingNumericComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.BOOLEAN: {
options.push(<SettingBooleanComponent key={key} option={setting} {...props} />);
break;
}
case OptionType.SLIDER: {
options.push(<SettingSliderComponent key={key} option={setting} {...props} />);
break;
}
}
}
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>;
}
@ -195,13 +207,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
</Forms.FormSection>
</ModalContent>
<ModalFooter>
<Flex>
<Flex flexDirection="column" style={{ width: "100%" }}>
<Flex style={{ marginLeft: "auto" }}>
<Button
onClick={onClose}
size={Button.Sizes.SMALL}
color={Button.Colors.RED}
>
Exit Without Saving
Cancel
</Button>
<Tooltip text="You must fix all errors before saving" shouldShow={!canSubmit()}>
{({ onMouseEnter, onMouseLeave }) => (
@ -213,11 +226,13 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
onMouseLeave={onMouseLeave}
disabled={!canSubmit()}
>
Save & Exit
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

@ -16,8 +16,9 @@
* 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 { PluginOptionBoolean } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingBooleanComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {

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

@ -16,8 +16,9 @@
* 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 { OptionType, PluginOptionNumber } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
import { ISettingElementProps } from ".";
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);

View File

@ -16,8 +16,9 @@
* 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 { PluginOptionSelect } from "@utils/types";
import { Forms, React, Select } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingSelectComponent({ option, pluginSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {

View File

@ -16,8 +16,9 @@
* 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 { PluginOptionSlider } from "@utils/types";
import { Forms, React, Slider } from "@webpack/common";
import { ISettingElementProps } from ".";
export function makeRange(start: number, end: number, step = 1) {

View File

@ -16,11 +16,12 @@
* 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 { PluginOptionString } from "@utils/types";
import { Forms, React, TextInput } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingInputComponent({ option, pluginSettings, id, onChange, onError }: ISettingElementProps<PluginOptionString>) {
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);

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionBase } from "../../../utils/types";
import { PluginOptionBase } from "@utils/types";
export interface ISettingElementProps<T extends PluginOptionBase> {
option: T;
@ -30,7 +30,9 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
}
export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent";
export * from "./SettingNumericComponent";
export * from "./SettingSelectComponent";
export * from "./SettingSliderComponent";
export * from "./SettingTextComponent";

View File

@ -16,30 +16,32 @@
* 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 { showNotice } from "../../api/Notices";
import { Settings, useSettings } from "../../api/settings";
import { startDependenciesRecursive, startPlugin, stopPlugin } from "../../plugins";
import { Logger, Modals } from "../../utils";
import { ChangeList } from "../../utils/ChangeList";
import { classes, lazyWebpack } from "../../utils/misc";
import { Plugin } from "../../utils/types";
import { filters } from "../../webpack";
import { Alerts, Button, Forms, Margins, Parser, React, Switch, Text, TextInput, Toasts, Tooltip } from "../../webpack/common";
import ErrorBoundary from "../ErrorBoundary";
import { ErrorCard } from "../ErrorCard";
import { Flex } from "../Flex";
import PluginModal from "./PluginModal";
import * as styles from "./styles";
const logger = new Logger("PluginSettings", "#a6d189");
const Select = lazyWebpack(filters.byCode("optionClassName", "popoutPosition", "autoFocus", "maxVisibleItems"));
const InputStyles = lazyWebpack(filters.byProps(["inputDefault", "inputWrapper"]));
const InputStyles = findByPropsLazy("inputDefault", "inputWrapper");
const CogWheel = lazyWebpack(filters.byCode("18.564C15.797 19.099 14.932 19.498 14 19.738V22H10V19.738C9.069"));
const InfoIcon = lazyWebpack(filters.byCode("4.4408921e-16 C4.4771525,-1.77635684e-15 4.4408921e-16"));
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({
@ -89,7 +91,7 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
}
function openModal() {
Modals.openModalLazy(async () => {
openModalLazy(async () => {
return modalProps => {
return <PluginModal {...modalProps} plugin={plugin} onRestartNeeded={() => onRestartNeeded(plugin.name)} />;
};
@ -145,7 +147,19 @@ function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, onMouseLe
onChange={toggleEnabled}
disabled={disabled}
value={isEnabled()}
note={<Text variant="text-md/normal" style={{ height: 40, overflow: "hidden" }}>{plugin.description}</Text>}
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" }}>
@ -208,11 +222,6 @@ export default ErrorBoundary.wrap(function Settings() {
return o;
}, []);
function hasDependents(plugin: Plugin) {
const enabledDependants = depMap[plugin.name]?.filter(d => settings.plugins[d].enabled);
return !!enabledDependants?.length;
}
const sortedPlugins = React.useMemo(() => Object.values(Plugins)
.sort((a, b) => a.name.localeCompare(b.name)), []);
@ -235,9 +244,9 @@ export default ErrorBoundary.wrap(function Settings() {
};
return (
<Forms.FormSection tag="h1" title="Vencord">
<Forms.FormSection>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Plugins
Filters
</Forms.FormTitle>
<ReloadRequiredCard plugins={[...changes.getChanges()]} style={{ marginBottom: 16 }} />
@ -252,7 +261,7 @@ export default ErrorBoundary.wrap(function Settings() {
{ label: "Show Enabled", value: "enabled" },
{ label: "Show Disabled", value: "disabled" }
]}
serialize={v => String(v)}
serialize={String}
select={onStatusChange}
isSelected={v => v === searchValue.status}
closeOnSelect={true}
@ -260,6 +269,8 @@ export default ErrorBoundary.wrap(function Settings() {
</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))
@ -270,6 +281,7 @@ export default ErrorBoundary.wrap(function Settings() {
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>
@ -288,12 +300,12 @@ export default ErrorBoundary.wrap(function Settings() {
const tooltipText = plugin.required
? "This plugin is required for Vencord to function."
: makeDependencyList(dependencyCheck(plugin.name, depMap));
return <Tooltip text={tooltipText}>
return <Tooltip text={tooltipText} key={plugin.name}>
{({ onMouseLeave, onMouseEnter }) => (
<PluginCard
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
onRestartNeeded={name => changes.add(name)}
onRestartNeeded={name => changes.handleChange(name)}
disabled={plugin.required || !!dependency}
plugin={plugin}
/>
@ -305,6 +317,9 @@ export default ErrorBoundary.wrap(function Settings() {
</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[]) {

View File

@ -1,119 +0,0 @@
/*
* 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 { ChangeList } from "../utils/ChangeList";
import IpcEvents from "../utils/IpcEvents";
import { useAwaiter } from "../utils/misc";
import { Alerts, Button, Forms, Margins, Parser, React, Switch } from "../webpack/common";
import ErrorBoundary from "./ErrorBoundary";
import { Flex } from "./Flex";
import { launchMonacoEditor } from "./Monaco";
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()
}));
}, []);
return (
<Forms.FormSection tag="h1" title="Vencord">
<Forms.FormTitle tag="h5">
Settings
</Forms.FormTitle>
<Forms.FormText>
Settings Directory: <code style={{ userSelect: "text", cursor: "text" }}>{settingsDir}</code>
</Forms.FormText>
{!IS_WEB && <Flex className={Margins.marginBottom20} style={{ marginTop: 8 }}>
<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_MONACO_EDITOR)}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}
>
Open QuickCSS File
</Button>
</Flex>}
{IS_WEB && <Button
onClick={launchMonacoEditor}
size={Button.Sizes.SMALL}
disabled={settingsDir === "Loading..."}
>
Open QuickCSS File
</Button>}
<Forms.FormDivider />
<Switch
value={settings.useQuickCss}
onChange={(v: boolean) => settings.useQuickCss = v}
note="Loads styles from your QuickCss file"
>
Use QuickCss
</Switch>
{!IS_WEB && <Switch
value={settings.enableReactDevtools}
onChange={(v: boolean) => settings.enableReactDevtools = v}
note="Requires a full restart"
>
Enable React Developer Tools
</Switch>}
{!IS_WEB && <Switch
value={settings.notifyAboutUpdates}
onChange={(v: boolean) => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp"
>
Get notified about new Updates
</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

@ -16,15 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import gitHash from "~git-hash";
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 { 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 ErrorBoundary from "./ErrorBoundary";
import { ErrorCard } from "./ErrorCard";
import { Flex } from "./Flex";
import { Link } from "./Link";
import gitHash from "~git-hash";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
@ -178,7 +179,7 @@ function Newer(props: CommonProps) {
}
function Updater() {
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
if (err)
@ -191,7 +192,7 @@ function Updater() {
};
return (
<Forms.FormSection tag="h1" title="Vencord Updater">
<Forms.FormSection>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText>{repoPending ? repo : err ? "Failed to retrieve - check console" : (
@ -209,4 +210,7 @@ function Updater() {
);
}
export default IS_WEB ? null : ErrorBoundary.wrap(Updater);
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

@ -16,6 +16,6 @@
* 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 Settings } from "./Settings";
export { default as Updater } from "./Updater";
export { default as VencordSettings } from "./VencordSettings";

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;
};

4
src/globals.d.ts vendored
View File

@ -16,11 +16,12 @@
* 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 refrence it inside the patched code
* but NEVER reference it inside the patched code
*
* @example
* // BAD
@ -31,6 +32,7 @@ declare global {
* 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;

View File

@ -18,6 +18,9 @@
import "./updater";
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";
@ -25,9 +28,6 @@ import { join } from "path";
import monacoHtml from "~fileContent/../components/monacoWin.html;base64";
import { debounce } from "../utils/debounce";
import IpcEvents from "../utils/IpcEvents";
import { Queue } from "../utils/Queue";
import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants";
mkdirSync(SETTINGS_DIR, { recursive: true });
@ -66,14 +66,14 @@ const settingsWriteQueue = new Queue();
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) =>
cssWriteQueue.add(() => writeFile(QUICKCSS_PATH, 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.handle(IpcEvents.SET_SETTINGS, (_, s) => {
settingsWriteQueue.add(() => writeFile(SETTINGS_FILE, s));
settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s));
});
@ -89,8 +89,12 @@ export function initIpc(mainWindow: BrowserWindow) {
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}`);

View File

@ -16,22 +16,25 @@
* 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 IpcEvents from "../../utils/IpcEvents";
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[]) {
return execFile("git", args, {
cwd: VENCORD_SRC_DIR
});
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() {
@ -61,9 +64,13 @@ async function pull() {
}
async function build() {
const res = await execFile("node", ["scripts/build/build.mjs"], {
cwd: VENCORD_SRC_DIR
});
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");
}

View File

@ -16,6 +16,8 @@
* 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";
@ -23,13 +25,11 @@ import { join } from "path";
import gitHash from "~git-hash";
import gitRemote from "~git-remote";
import { VENCORD_USER_AGENT } from "../../utils/constants";
import IpcEvents from "../../utils/IpcEvents";
import { get } from "../simpleGet";
import { calculateHashes, serializeErrors } from "./common";
const API_BASE = `https://api.github.com/repos/${gitRemote}`;
let PendingUpdates = [] as [string, Buffer][];
let PendingUpdates = [] as [string, string][];
async function githubGet(endpoint: string) {
return get(API_BASE + endpoint, {
@ -46,6 +46,9 @@ async function githubGet(endpoint: string) {
}
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"));
@ -63,18 +66,20 @@ async function fetchUpdates() {
const data = JSON.parse(release.toString());
const hash = data.name.slice(data.name.lastIndexOf(" ") + 1);
if (hash === gitHash)
return true;
return false;
await Promise.all(data.assets.map(async ({ name, browser_download_url }) => {
data.assets.forEach(({ name, browser_download_url }) => {
if (["patcher.js", "preload.js", "renderer.js"].some(s => name.startsWith(s))) {
PendingUpdates.push([name, await get(browser_download_url)]);
PendingUpdates.push([name, browser_download_url]);
}
}));
});
return true;
}
async function applyUpdates() {
await Promise.all(PendingUpdates.map(([name, data]) => writeFile(join(__dirname, name), data)));
await Promise.all(PendingUpdates.map(
async ([name, data]) => writeFile(join(__dirname, name), await get(data)))
);
PendingUpdates = [];
return true;
}

2
src/modules.d.ts vendored
View File

@ -20,7 +20,7 @@
/// <reference types="standalone-electron-types"/>
declare module "~plugins" {
const plugins: Record<string, import("./utils/types").Plugin>;
const plugins: Record<string, import("@utils/types").Plugin>;
export default plugins;
}

View File

@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { onceDefined } from "@utils/onceDefined";
import electron, { app, BrowserWindowConstructorOptions } from "electron";
import { readFileSync } from "fs";
import { dirname, join } from "path";
@ -30,7 +31,7 @@ console.log("[Vencord] Starting up...");
const injectorPath = require.main!.filename;
// special discord_arch_electron injection method
const asarName = injectorPath.endsWith("app.asar/index.js") ? "_app.asar" : "app.asar";
const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar";
// The original app.asar
const asarPath = join(dirname(injectorPath), "..", asarName);
@ -41,6 +42,7 @@ require.main!.filename = join(asarPath, discordPkg.main);
// @ts-ignore Untyped method? Dies from cringe
app.setAppPath(asarPath);
if (!process.argv.includes("--vanilla")) {
// Repatch after host updates on Windows
if (process.platform === "win32")
require("./patchWin32Updater");
@ -74,15 +76,9 @@ require.cache[electronPath]!.exports = {
};
// 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
});
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");
@ -111,20 +107,57 @@ electron.app.whenReady().then(() => {
.catch(err => console.error("[Vencord] Failed to install React Developer Tools", err));
} catch { }
// Remove CSP
electron.session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, url }, cb) => {
if (responseHeaders) {
delete responseHeaders["content-security-policy-report-only"];
delete responseHeaders["content-security-policy"];
// Fix hosts that don't properly set the content type, such as
// 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 (url.endsWith(".css"))
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
@ -143,6 +176,5 @@ if (readFileSync(injectorPath, "utf-8").includes('require("../app.asar")')) {
return loadModule.apply(this, arguments);
};
} else {
console.log(require.main!.filename);
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

@ -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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.obscurity],
patches: [
{
find: "renderTimeBar=function",
replacement: {
match: /renderTimeBar=function\((.{1,3})\){.{0,50}?var/,
replace: "renderTimeBar=function($1){var"
}
}
],
});

View File

@ -16,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
import { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
enum Methods {
Random,
@ -67,7 +67,9 @@ export default definePlugin({
anonymise(file: string) {
let name = "image";
const ext = file.match(/\..+$/g)?.[0] ?? "";
const extIdx = file.lastIndexOf(".");
const ext = extIdx !== -1 ? file.slice(extIdx) : "";
switch (Settings.plugins.AnonymiseFileNames.method) {
case Methods.Random:
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

161
src/plugins/apiBadges.tsx Normal file
View File

@ -0,0 +1,161 @@
/*
* 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 { BadgePosition, ProfileBadge } from "@api/Badges";
import DonateButton from "@components/DonateButton";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Heart } from "@components/Heart";
import { Devs } from "@utils/constants";
import IpcEvents from "@utils/IpcEvents";
import Logger from "@utils/Logger";
import { closeModal, Modals, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Forms, Margins } from "@webpack/common";
const CONTRIBUTOR_BADGE = "https://media.discordapp.net/stickers/1026517526106087454.webp";
/** List of vencord contributor IDs */
const contributorIds: string[] = Object.values(Devs).map(d => d.id.toString());
const ContributorBadge: ProfileBadge = {
tooltip: "Vencord Contributor",
image: CONTRIBUTOR_BADGE,
position: BadgePosition.START,
props: {
style: {
borderRadius: "50%",
transform: "scale(0.9)" // The image is a bit too big compared to default badges
}
},
shouldShow: ({ user }) => contributorIds.includes(user.id),
onClick: () => VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/Vendicated/Vencord")
};
const DonorBadges = {} as Record<string, Pick<ProfileBadge, "image" | "tooltip">>;
export default definePlugin({
name: "BadgeAPI",
description: "API to add badges to users.",
authors: [Devs.Megu],
required: true,
patches: [
/* Patch the badges array */
{
find: "PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP.format({date:",
replacement: {
match: /&&((\w{1,3})\.push\({tooltip:\w{1,3}\.\w{1,3}\.Messages\.PREMIUM_GUILD_SUBSCRIPTION_TOOLTIP\.format.+?;)(?:return\s\w{1,3};?})/,
replace: (_, m, badgeArray) => `&&${m} return Vencord.Api.Badges.inject(${badgeArray}, arguments[0]);}`,
}
},
/* Patch the badge list component on user profiles */
{
find: "Messages.PROFILE_USER_BADGES,role:",
replacement: {
match: /src:(\w{1,3})\[(\w{1,3})\.key\],/,
// <img src={badge.image ?? imageMap[badge.key]} {...badge.props} />
replace: (_, imageMap, badge) => `src: ${badge}.image ?? ${imageMap}[${badge}.key], ...${badge}.props,`
}
}
],
async start() {
Vencord.Api.Badges.addBadge(ContributorBadge);
const badges = await fetch("https://gist.githubusercontent.com/Vendicated/51a3dd775f6920429ec6e9b735ca7f01/raw/badges.csv").then(r => r.text());
const lines = badges.trim().split("\n");
if (lines.shift() !== "id,tooltip,image") {
new Logger("BadgeAPI").error("Invalid badges.csv file!");
return;
}
for (const line of lines) {
const [id, tooltip, image] = line.split(",");
DonorBadges[id] = { image, tooltip };
}
},
addDonorBadge(badges: ProfileBadge[], userId: string) {
const badge = DonorBadges[userId];
if (badge) {
badges.unshift({
...badge,
position: BadgePosition.START,
props: {
style: {
borderRadius: "50%",
transform: "scale(0.9)" // The image is a bit too big compared to default badges
}
},
onClick() {
const modalKey = openModal(props => (
<ErrorBoundary noop onError={() => {
closeModal(modalKey);
VencordNative.ipc.invoke(IpcEvents.OPEN_EXTERNAL, "https://github.com/sponsors/Vendicated");
}}>
<Modals.ModalRoot {...props}>
<Modals.ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<Forms.FormTitle
tag="h2"
style={{
width: "100%",
textAlign: "center",
margin: 0
}}
>
<Heart />
Vencord Donor
</Forms.FormTitle>
</Flex>
</Modals.ModalHeader>
<Modals.ModalContent>
<Flex>
<img
role="presentation"
src="https://cdn.discordapp.com/emojis/1026533070955872337.png"
alt=""
style={{ margin: "auto" }}
/>
<img
role="presentation"
src="https://cdn.discordapp.com/emojis/1026533090627174460.png"
alt=""
style={{ margin: "auto" }}
/>
</Flex>
<div style={{ padding: "1em" }}>
<Forms.FormText>
This Badge is a special perk for Vencord Donors
</Forms.FormText>
<Forms.FormText className={Margins.marginTop20}>
Please consider supporting the development of Vencord by becoming a donor. It would mean a lot!!
</Forms.FormText>
</div>
</Modals.ModalContent>
<Modals.ModalFooter>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<DonateButton />
</Flex>
</Modals.ModalFooter>
</Modals.ModalRoot>
</ErrorBoundary>
));
},
});
}
}
});

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "CommandsAPI",
@ -47,6 +47,15 @@ export default definePlugin({
match: /,(.{1,2})\.execute\((.{1,2}),(.{1,2})\)]/,
replace: (_, cmd, args, ctx) => `,Vencord.Api.Commands._handleCommand(${cmd}, ${args}, ${ctx})]`
}
},
// Show plugin name instead of "Built-In"
{
find: "().source,children",
replacement: {
// ...children: p?.name
match: /(?<=:(.{1,3})\.displayDescription\}.{0,200}\(\)\.source,children:)[^}]+/,
replace: "$1.plugin||($&)"
}
}
],
});

View File

@ -0,0 +1,82 @@
/*
* 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 { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// duplicate values have multiple branches with different types. Just include all to be safe
const nameMap = {
radio: "MenuRadioItem",
separator: "MenuSeparator",
checkbox: "MenuCheckboxItem",
groupstart: "MenuGroup",
control: "MenuControlItem",
compositecontrol: "MenuControlItem",
item: "MenuItem",
customitem: "MenuItem",
};
migratePluginSettings("MenuItemDeobfuscatorAPI", "MenuItemDeobfuscatorApi");
export default definePlugin({
name: "MenuItemDeobfuscatorAPI",
description: "Deobfuscates Discord's Menu Item module",
authors: [Devs.Ven],
patches: [
{
find: '"Menu API',
replacement: {
match: /function.{0,80}type===(.{1,3})\..{1,3}\).{0,50}navigable:.+?Menu API/s,
replace: (m, mod) => {
let nicenNames = "";
const redefines = [] as string[];
// if (t.type === m.MenuItem)
const typeCheckRe = /\(.{1,3}\.type===(.{1,5})\)/g;
// push({type:"item"})
const pushTypeRe = /type:"(\w+)"/g;
let typeMatch: RegExpExecArray | null;
// for each if (t.type === ...)
while ((typeMatch = typeCheckRe.exec(m)) !== null) {
// extract the current menu item
const item = typeMatch[1];
// Set the starting index of the second regex to that of the first to start
// matching from after the if
pushTypeRe.lastIndex = typeCheckRe.lastIndex;
// extract the first type: "..."
const type = pushTypeRe.exec(m)?.[1];
if (type && type in nameMap) {
const name = nameMap[type];
nicenNames += `Object.defineProperty(${item},"name",{value:"${name}"});`;
redefines.push(`${name}:${item}`);
}
}
if (redefines.length < 6) {
console.warn("[ApiMenuItemDeobfuscator] Expected to at least remap 6 items, only remapped", redefines.length);
}
// Merge all our redefines with the actual module
return `${nicenNames}Object.assign(${mod},{${redefines.join(",")}});${m}`;
},
},
},
],
});

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessageAccessoriesAPI",
@ -27,9 +27,9 @@ export default definePlugin({
{
find: "_messageAttachmentToEmbedMedia",
replacement: {
match: /\(\)\.container\)},(.+?)\)};return/,
replace: (_, accessories) =>
`().container)},Vencord.Api.MessageAccessories._modifyAccessories([${accessories}],this.props))};return`,
match: /(\(\)\.container\)?,children:)(\[[^\]]+\])(}\)\};return)/,
replace: (_, pre, accessories, post) =>
`${pre}Vencord.Api.MessageAccessories._modifyAccessories(${accessories},this.props)${post}`,
},
},
],

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessageEventsAPI",
@ -28,14 +28,14 @@ export default definePlugin({
find: "sendMessage:function",
replacement: [{
match: /(?<=_sendMessage:function\([^)]+\)){/,
replace: "{Vencord.Api.MessageEvents._handlePreSend(...arguments);"
replace: "{if(Vencord.Api.MessageEvents._handlePreSend(...arguments)){return;};"
}, {
match: /(?<=\beditMessage:function\([^)]+\)){/,
replace: "{Vencord.Api.MessageEvents._handlePreEdit(...arguments);"
}]
},
{
find: "if(e.altKey){",
find: '("interactionUsernameProfile',
replacement: {
match: /var \w=(\w)\.id,\w=(\w)\.id;return .{1,2}\.useCallback\(\(?function\((.{1,2})\){/,
replace: (m, message, channel, event) =>

View File

@ -0,0 +1,33 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "MessagePopoverAPI",
description: "API to add buttons to message popovers.",
authors: [Devs.KingFish],
patches: [{
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
match: /(message:(.).{0,100}Fragment,\{children:\[)(.{0,90}renderPopout:.{0,200}message_reaction_emoji_picker.+?return (.{1,3})\(.{0,30}"add-reaction")/,
replace: "$1...Vencord.Api.MessagePopover._buildPopoverElements($2,$4),$3"
}
}],
});

View File

@ -16,11 +16,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
migratePluginSettings("NoticesAPI", "NoticesApi");
export default definePlugin({
name: "ApiNotices",
name: "NoticesAPI",
description: "Fixes notices being automatically dismissed",
authors: [Devs.Ven],
required: true,

View File

@ -0,0 +1,42 @@
/*
* 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 { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "ServerListAPI",
authors: [Devs.kemo],
description: "Api required for plugins that modify the server list",
patches: [
{
find: "Messages.DISCODO_DISABLED",
replacement: {
match: /(Messages\.DISCODO_DISABLED\);return)(.*?homeIcon.*?)(\}function)/,
replace: "$1[$2].concat(Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.Above))$3"
}
},
{
find: "Messages.SERVERS",
replacement: {
match: /(Messages\.SERVERS,children:)(.+?default:return null\}\}\)\))/,
replace: "$1Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($2)"
}
}
]
});

107
src/plugins/arRPC.tsx Normal file
View File

@ -0,0 +1,107 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 OpenAsar
*
* 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 { popNotice, showNotice } from "@api/Notices";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { filters, findByCodeLazy, mapMangledModuleLazy } from "@webpack";
import { FluxDispatcher, Forms, Toasts } from "@webpack/common";
const assetManager = mapMangledModuleLazy(
"getAssetImage: size must === [number, number] for Twitch",
{
getAsset: filters.byCode("apply("),
}
);
const rpcManager = findByCodeLazy(".APPLICATION_RPC(");
async function lookupAsset(applicationId: string, key: string): Promise<string> {
return (await assetManager.getAsset(applicationId, [key, undefined]))[0];
}
const apps: any = {};
async function lookupApp(applicationId: string): Promise<string> {
const socket: any = {};
await rpcManager.lookupApp(socket, applicationId);
return socket.application;
}
let ws: WebSocket;
export default definePlugin({
name: "WebRichPresence (arRPC)",
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko],
target: "WEB",
settingsAboutComponent: () => (
<>
<Forms.FormTitle tag="h3">How to use arRPC</Forms.FormTitle>
<Forms.FormText>
<Link href="https://github.com/OpenAsar/arrpc/tree/main#server">Follow the instructions in the GitHub repo</Link> to get the server running, and then enable the plugin.
</Forms.FormText>
</>
),
async start() {
if (ws) ws.close();
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
ws.onmessage = async e => { // on message, set status to data
const data = JSON.parse(e.data);
if (data.activity?.assets?.large_image) data.activity.assets.large_image = await lookupAsset(data.activity.application_id, data.activity.assets.large_image);
if (data.activity?.assets?.small_image) data.activity.assets.small_image = await lookupAsset(data.activity.application_id, data.activity.assets.small_image);
if (data.activity) {
const appId = data.activity.application_id;
apps[appId] ||= await lookupApp(appId);
const app = apps[appId];
data.activity.name ||= app.name;
}
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data });
};
const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 1000)); // check if open after 1s
if (!connectionSuccessful) {
showNotice("Failed to connect to arRPC, is it running?", "Retry", () => { // show notice about failure to connect, with retry/ignore
popNotice();
this.start();
});
return;
}
Toasts.show({ // show toast on success
message: "Connected to arRPC",
type: Toasts.Type.SUCCESS,
id: Toasts.genId(),
options: {
duration: 1000,
position: Toasts.Position.BOTTOM
}
});
},
stop() {
FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); // clear status
ws.close(); // close WebSocket
}
});

View File

@ -16,19 +16,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
export default definePlugin({
name: "BANger",
description: "Replaces the GIF in the ban dialogue with a custom one.",
authors: [
{
name: "Xinto",
id: 423915768191647755n
},
Devs.Glitch
],
authors: [Devs.Xinto, Devs.Glitch],
patches: [
{
find: "BAN_CONFIRM_TITLE.",

View File

@ -16,8 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "BetterGifAltText",
@ -28,7 +29,7 @@ export default definePlugin({
{
find: "onCloseImage=",
replacement: {
match: /(return .{1,2}\.createElement.{0,50}isWindowFocused)/,
match: /(return.{0,10}\.jsx.{0,50}isWindowFocused)/,
replace:
"Vencord.Plugins.plugins.BetterGifAltText.altify(e);$1",
},
@ -36,9 +37,9 @@ export default definePlugin({
{
find: 'preload:"none","aria',
replacement: {
match: /\?.{0,5}\.Messages\.GIF/,
match: /(?<==(.{1,3})\.alt.{0,20})\?.{0,5}\.Messages\.GIF/,
replace:
"?(e.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify(e))",
"?($1.alt='GIF',Vencord.Plugins.plugins.BetterGifAltText.altify($1))",
},
},
],

View File

@ -16,36 +16,47 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Toasts } from "../webpack/common";
import { migratePluginSettings, Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Clipboard, Toasts } from "@webpack/common";
migratePluginSettings("BetterRoleDot", "ClickableRoleDot");
export default definePlugin({
name: "ClickableRoleDot",
name: "BetterRoleDot",
authors: [Devs.Ven],
description:
"Makes RoleDots (Accessibility Feature) copy colour to clipboard on click",
"Copy role colour on RoleDot (accessibility setting) click. Also allows using both RoleDot and coloured names simultaneously",
patches: [
{
find: "M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V16C20 18.2091 18.2091 20 16 20H4C1.79086 20 0 18.2091 0 16V4Z",
replacement: {
match: /(viewBox:"0 0 20 20")/,
replace: "$1,onClick:()=>Vencord.Plugins.plugins.ClickableRoleDot.copyToClipBoard(e.color)",
match: /viewBox:"0 0 20 20"/,
replace: "$&,onClick:()=>Vencord.Plugins.plugins.BetterRoleDot.copyToClipBoard(e.color),style:{cursor:'pointer'}",
},
},
{
find: '"username"===',
all: true,
predicate: () => Settings.plugins.BetterRoleDot.bothStyles,
replacement: {
match: /"(?:username|dot)"===\w\b/g,
replace: "true",
},
},
],
copyToClipBoard(color: string) {
if (IS_WEB) {
navigator.clipboard.writeText(color)
.then(() => this.notifySuccess);
} else {
DiscordNative.clipboard.copy(color);
this.notifySuccess();
options: {
bothStyles: {
type: OptionType.BOOLEAN,
description: "Show both role dot and coloured names",
default: false,
}
},
notifySuccess() {
copyToClipBoard(color: string) {
Clipboard.copy(color);
Toasts.show({
message: "Copied to Clipboard!",
type: Toasts.Type.SUCCESS,
@ -55,5 +66,5 @@ export default definePlugin({
position: Toasts.Position.BOTTOM
}
});
}
},
});

View File

@ -16,20 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "BetterUploadButton",
authors: [Devs.obscurity],
authors: [Devs.obscurity, Devs.Ven],
description: "Upload with a single click, open menu with right click",
patches: [
{
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
replacement: {
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:([^,]+),onClick:([^,]+)}}/,
replace:
"CHAT_ATTACH_UPLOAD_OR_INVITE,onClick:$1,onContextMenu:$2}}",
// Discord merges multiple props here with Object.assign()
// This patch passes a third object to it with which we override onClick and onContextMenu
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0)\},(.{1,3})\)/,
replace: (m, onDblClick, otherProps) =>
`${m.slice(0, -1)},{onClick:${onDblClick},onContextMenu:${otherProps}.onClick})`,
},
},
],

77
src/plugins/blurNsfw.ts Normal file
View File

@ -0,0 +1,77 @@
/*
* 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 definePlugin, { OptionType } from "@utils/types";
let style: HTMLStyleElement;
function setCss() {
style.textContent = `
.vc-nsfw-img [class^=imageWrapper] img,
.vc-nsfw-img [class^=wrapperPaused] video {
filter: blur(${Settings.plugins.BlurNSFW.blurAmount}px);
transition: filter 0.2s;
}
.vc-nsfw-img [class^=imageWrapper]:hover img,
.vc-nsfw-img [class^=wrapperPaused]:hover video {
filter: unset;
}
`;
}
export default definePlugin({
name: "BlurNSFW",
description: "Blur attachments in NSFW channels until hovered",
authors: [Devs.Ven],
patches: [
{
find: "().embedWrapper,embed",
replacement: [{
match: /(\.renderEmbed=.+?(.)=.\.props)(.+?\(\)\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}, {
match: /(\.renderAttachments=.+?(.)=this\.props)(.+?\(\)\.embedWrapper)/g,
replace: "$1,vcProps=$2$3+(vcProps.channel.nsfw?' vc-nsfw-img':'')"
}]
}
],
options: {
blurAmount: {
type: OptionType.NUMBER,
description: "Blur Amount",
default: 10,
onChange: setCss
}
},
start() {
style = document.createElement("style");
style.id = "VcBlurNsfw";
document.head.appendChild(style);
setCss();
},
stop() {
style?.remove();
}
});

101
src/plugins/callTimer.tsx Normal file
View File

@ -0,0 +1,101 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { React } from "@webpack/common";
function formatDuration(ms: number) {
// here be dragons (moment fucking sucks)
const human = Settings.plugins.CallTimer.format === "human";
const format = (n: number) => human ? n : n.toString().padStart(2, "0");
const unit = (s: string) => human ? s : "";
const delim = human ? " " : ":";
// thx copilot
const d = Math.floor(ms / 86400000);
const h = Math.floor((ms % 86400000) / 3600000);
const m = Math.floor(((ms % 86400000) % 3600000) / 60000);
const s = Math.floor((((ms % 86400000) % 3600000) % 60000) / 1000);
let res = "";
if (d) res += `${d}d `;
if (h || res) res += `${format(h)}${unit("h")}${delim}`;
if (m || res || !human) res += `${format(m)}${unit("m")}${delim}`;
res += `${format(s)}${unit("s")}`;
return res;
}
export default definePlugin({
name: "CallTimer",
description: "Adds a timer to vcs",
authors: [Devs.Ven],
startTime: 0,
interval: void 0 as NodeJS.Timeout | undefined,
options: {
format: {
type: OptionType.SELECT,
description: "The timer format. This can be any valid moment.js format",
options: [
{
label: "30d 23:00:42",
value: "stopwatch",
default: true
},
{
label: "30d 23h 00m 42s",
value: "human"
}
]
}
},
patches: [{
find: ".renderConnectionStatus=",
replacement: {
match: /(?<=renderConnectionStatus=.+\(\)\.channel,children:)\w/,
replace: "[$&, Vencord.Plugins.plugins.CallTimer.renderTimer(this.props.channel.id)]"
}
}],
renderTimer(channelId: string) {
return <ErrorBoundary noop>
<this.Timer channelId={channelId} />
</ErrorBoundary>;
},
Timer({ channelId }: { channelId: string; }) {
const [time, setTime] = React.useState(0);
const startTime = React.useMemo(() => Date.now(), [channelId]);
React.useEffect(() => {
const interval = setInterval(() => setTime(Date.now() - startTime), 1000);
return () => {
clearInterval(interval);
setTime(0);
};
}, [channelId]);
return <p style={{ margin: 0 }}>Connected for {formatDuration(time)}</p>;
}
});

View File

@ -21,24 +21,23 @@ import {
addPreSendListener,
MessageObject,
removePreEditListener,
removePreSendListener,
} from "../../api/MessageEvents";
import definePlugin from "../../utils/types";
removePreSendListener
} from "@api/MessageEvents";
import { migratePluginSettings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { defaultRules } from "./defaultRules";
// From lodash
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
migratePluginSettings("ClearURLs", "clearURLs");
export default definePlugin({
name: "clearURLs",
name: "ClearURLs",
description: "Removes tracking garbage from URLs",
authors: [
{
name: "adryd",
id: 0n,
},
],
authors: [Devs.adryd],
dependencies: ["MessageEventsAPI"],
escapeRegExp(str: string) {

View File

@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Devs } from "../utils/constants";
import definePlugin from "../utils/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
const WEB_ONLY = (f: string) => () => {
throw new Error(`'${f}' is Discord Desktop only.`);
@ -37,6 +37,7 @@ export default definePlugin({
wreq: Vencord.Webpack.wreq,
wpsearch: Vencord.Webpack.search,
wpex: Vencord.Webpack.extract,
wpexs: (code: string) => Vencord.Webpack.extract(Vencord.Webpack.findModuleId(code)!),
findByProps: Vencord.Webpack.findByProps,
find: Vencord.Webpack.find,
Plugins: Vencord.Plugins,

105
src/plugins/corruptMp4s.ts Normal file
View File

@ -0,0 +1,105 @@
/*
* 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 { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
import { findOption } from "@api/Commands/commandHelpers";
import { ApplicationCommandInputType } from "@api/Commands/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCode, findByProps } from "@webpack";
const DRAFT_TYPE = 0;
export default definePlugin({
name: "CorruptMp4s",
description: "Create corrupt mp4s with extremely high or negative duration",
authors: [Devs.Ven],
dependencies: ["CommandsAPI"],
commands: [{
name: "corrupt",
description: "Create a corrupt mp4 with extremely high or negative duration",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
name: "mp4",
description: "the video to corrupt",
type: ApplicationCommandOptionType.ATTACHMENT,
required: true
},
{
name: "kind",
description: "the kind of corruption",
type: ApplicationCommandOptionType.STRING,
choices: [
{
name: "infinite",
value: "infinite",
label: "Very high duration"
},
{
name: "negative",
value: "negative",
label: "Negative duration"
}
]
}
],
execute: async (args, ctx) => {
const UploadStore = findByProps("getUploads");
const upload = UploadStore.getUploads(ctx.channel.id, DRAFT_TYPE)[0];
const video = upload?.item?.file as File | undefined;
if (video?.type !== "video/mp4")
return void sendBotMessage(ctx.channel.id, {
content: "Please upload a mp4 file"
});
const corruption = findOption<string>(args, "kind", "infinite");
const buf = new Uint8Array(await video.arrayBuffer());
let found = false;
// adapted from https://github.com/GeopJr/exorcism/blob/c9a12d77ccbcb49c987b385eafae250906efc297/src/App.svelte#L41-L48
for (let i = 0; i < buf.length; i++) {
if (buf[i] === 0x6d && buf[i + 1] === 0x76 && buf[i + 2] === 0x68 && buf[i + 3] === 0x64) {
let start = i + 18;
buf[start++] = 0x00;
buf[start++] = 0x01;
buf[start++] = corruption === "negative" ? 0xff : 0x7f;
buf[start++] = 0xff;
buf[start++] = 0xff;
buf[start++] = corruption === "negative" ? 0xf0 : 0xff;
found = true;
break;
}
}
if (!found) {
return void sendBotMessage(ctx.channel.id, {
content: "Could not find signature. Is this even a mp4?"
});
}
const newName = video.name.replace(/\.mp4$/i, ".corrupt.mp4");
const promptToUpload = findByCode("UPLOAD_FILE_LIMIT_ERROR");
const file = new File([buf], newName, { type: "video/mp4" });
setImmediate(() => promptToUpload([file], ctx.channel, DRAFT_TYPE));
}
}]
});

80
src/plugins/dictionary.ts Normal file
View File

@ -0,0 +1,80 @@
/*
* 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 { ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
import { ApplicationCommandInputType } from "@api/Commands/types";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "UrbanDictionary",
description: "Searches for a word on Urban Dictionary",
authors: [Devs.jewdev],
dependencies: ["CommandsAPI"],
commands: [
{
name: "urban",
description: "Returns the definition of a word from Urban Dictionary",
inputType: ApplicationCommandInputType.BUILT_IN,
options: [
{
type: ApplicationCommandOptionType.STRING,
name: "word",
description: "The word to search for on Urban Dictionary",
required: true
}
],
execute: async (args, ctx) => {
try {
const { list: [definition] } = await (await fetch(`https://api.urbandictionary.com/v0/define?term=${args[0].value}`)).json();
if (!definition)
return void sendBotMessage(ctx.channel.id, { content: "No results found." });
const linkify = text => text.replace(/\[(.+?)\]/g, (_, word) => `[${word}](https://www.urbandictionary.com/define.php?term=${encodeURIComponent(word)})`);
return void sendBotMessage(ctx.channel.id, {
embeds: [
{
type: "rich",
author: {
name: `Definition of ${definition.word}`,
url: definition.permalink
},
description: linkify(definition.definition),
fields: [
{
name: "Example",
value: linkify(definition.example)
}
],
color: 0xFF9900,
footer: { text: `👍 ${definition.thumbs_up.toString()} | 👎 ${definition.thumbs_down.toString()} | Uploaded by ${definition.author}`, icon_url: "https://www.urbandictionary.com/favicon.ico" },
timestamp: new Date(definition.written_on).toISOString()
}
] as any
});
} catch (error) {
return void sendBotMessage(ctx.channel.id, {
content: `Something went wrong: \`${error}\``
});
}
}
}
]
});

246
src/plugins/emoteCloner.tsx Normal file
View File

@ -0,0 +1,246 @@
/*
* 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 { migratePluginSettings, Settings } from "@api/settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import Logger from "@utils/Logger";
import { makeLazy } from "@utils/misc";
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
const GuildEmojiStore = findByPropsLazy("getGuilds", "getGuildEmoji");
const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS(");
function getGuildCandidates(isAnimated: boolean) {
const meId = UserStore.getCurrentUser().id;
return Object.values(GuildStore.getGuilds()).filter(g => {
const canCreate = g.ownerId === meId ||
BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS;
if (!canCreate) return false;
const emojiSlots = g.getMaxEmojiSlots();
const { emojis } = GuildEmojiStore.getGuilds()[g.id];
let count = 0;
for (const emoji of emojis)
if (emoji.animated === isAnimated) count++;
return count < emojiSlots;
}).sort((a, b) => a.name.localeCompare(b.name));
}
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
const data = await fetch(`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`)
.then(r => r.blob());
const reader = new FileReader();
reader.onload = () => {
uploadEmoji({
guildId,
name,
image: reader.result
}).then(() => {
Toasts.show({
message: `Successfully cloned ${name}!`,
type: Toasts.Type.SUCCESS,
id: Toasts.genId()
});
}).catch((e: any) => {
new Logger("EmoteCloner").error("Failed to upload emoji", e);
Toasts.show({
message: "Oopsie something went wrong :( Check console!!!",
type: Toasts.Type.FAILURE,
id: Toasts.genId()
});
});
};
reader.readAsDataURL(data);
}
const getFontSize = (s: string) => {
// [18, 18, 16, 16, 14, 12, 10]
const sizes = [20, 20, 18, 18, 16, 14, 12];
return sizes[s.length] ?? 4;
};
const nameValidator = /^\w+$/i;
function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: string; isAnimated: boolean; }) {
const [isCloning, setIsCloning] = React.useState(false);
const [name, setName] = React.useState(emojiName);
const [x, invalidateMemo] = React.useReducer(x => x + 1, 0);
const guilds = React.useMemo(() => getGuildCandidates(isAnimated), [isAnimated, x]);
return (
<>
<Forms.FormTitle className={Margins.marginTop20}>Custom Name</Forms.FormTitle>
<CheckedTextInput
value={name}
onChange={setName}
validate={v =>
(v.length > 1 && v.length < 32 && nameValidator.test(v))
|| "Name must be between 2 and 32 characters and only contain alphanumeric characters"
}
/>
<div style={{
display: "flex",
flexWrap: "wrap",
gap: "1em",
padding: "1em 0.5em",
justifyContent: "center",
alignItems: "center"
}}>
{guilds.map(g => (
<Tooltip text={g.name}>
{({ onMouseLeave, onMouseEnter }) => (
<div
onMouseLeave={onMouseLeave}
onMouseEnter={onMouseEnter}
role="button"
aria-label={"Clone to " + g.name}
aria-disabled={isCloning}
style={{
borderRadius: "50%",
backgroundColor: "var(--background-secondary)",
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
width: "4em",
height: "4em",
cursor: isCloning ? "not-allowed" : "pointer",
filter: isCloning ? "brightness(50%)" : "none"
}}
onClick={isCloning ? void 0 : async () => {
setIsCloning(true);
doClone(g.id, id, name, isAnimated).finally(() => {
invalidateMemo();
setIsCloning(false);
});
}}
>
{g.icon ? (
<img
aria-hidden
style={{
borderRadius: "50%",
width: "100%",
height: "100%",
}}
src={g.getIconURL(512, true)}
alt={g.name}
/>
) : (
<Forms.FormText
style={{
fontSize: getFontSize(g.acronym),
width: "100%",
overflow: "hidden",
whiteSpace: "nowrap",
textAlign: "center",
cursor: isCloning ? "not-allowed" : "pointer",
}}
>
{g.acronym}
</Forms.FormText>
)}
</div>
)}
</Tooltip>
))}
</div>
</>
);
}
migratePluginSettings("EmoteCloner", "EmoteYoink");
export default definePlugin({
name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server",
authors: [Devs.Ven],
dependencies: ["MenuItemDeobfuscatorAPI"],
patches: [{
// Literally copy pasted from ReverseImageSearch lol
find: "open-native-link",
replacement: {
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
replace: "$&,Vencord.Plugins.plugins.EmoteCloner.makeMenu(arguments[2])"
},
},
// Also copy pasted from Reverse Image Search
{
// pass the target to the open link menu so we can grab its data
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
noWarn: true,
replacement: {
match: /(?<props>.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
replace: "$&,$<props>.target"
}
}],
makeMenu(htmlElement: HTMLImageElement) {
if (htmlElement?.dataset.type !== "emoji")
return null;
const { id } = htmlElement.dataset;
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
if (!name || !id)
return null;
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
return <Menu.MenuItem
id="emote-cloner"
key="emote-cloner"
label="Clone"
action={() =>
openModal(modalProps => (
<ModalRoot {...modalProps}>
<ModalHeader>
<img
role="presentation"
aria-hidden
src={`https://cdn.discordapp.com/emojis/${id}.${isAnimated ? "gif" : "png"}`}
alt=""
height={24}
width={24}
style={{ marginRight: "0.5em" }}
/>
<Forms.FormText>Clone {name}</Forms.FormText>
</ModalHeader>
<ModalContent>
<CloneModal id={id} name={name} isAnimated={isAnimated} />
</ModalContent>
</ModalRoot>
))
}
>
</Menu.MenuItem>;
},
});

View File

@ -16,22 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { lazyWebpack } from "../utils";
import { Devs } from "../utils/constants";
import definePlugin, { OptionType } from "../utils/types";
import { Settings } from "../Vencord";
import { filters } from "../webpack";
import { Forms, React } from "../webpack/common";
import { Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Forms, React } from "@webpack/common";
const KbdStyles = lazyWebpack(filters.byProps(["key", "removeBuildOverride"]));
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
export default definePlugin({
name: "Experiments",
authors: [
Devs.Megu,
Devs.Ven,
{ name: "Nickyux", id: 427146305651998721n },
{ name: "BanTheNons", id: 460478012794863637n },
Devs.Nickyux,
Devs.BanTheNons
],
description: "Enable Access to Experiments in Discord!",
patches: [{

309
src/plugins/fakeNitro.ts Normal file
View File

@ -0,0 +1,309 @@
/*
* 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 { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
import { migratePluginSettings, Settings } from "@api/settings";
import { Devs } from "@utils/constants";
import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { ChannelStore, UserStore } from "@webpack/common";
const DRAFT_TYPE = 0;
const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
interface BaseSticker {
available: boolean;
description: string;
format_type: number;
id: string;
name: string;
tags: string;
type: number;
}
interface GuildSticker extends BaseSticker {
guild_id: string;
}
interface DiscordSticker extends BaseSticker {
pack_id: string;
}
type Sticker = GuildSticker | DiscordSticker;
interface StickerPack {
id: string;
name: string;
sku_id: string;
description: string;
cover_sticker_id: string;
banner_asset_id: string;
stickers: Sticker[];
}
migratePluginSettings("FakeNitro", "NitroBypass");
export default definePlugin({
name: "FakeNitro",
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.obscurity],
description: "Allows you to stream in nitro quality and send fake emojis/stickers.",
dependencies: ["MessageEventsAPI"],
patches: [
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
replacement: [
"canUseAnimatedEmojis",
"canUseEmojisEverywhere"
].map(func => {
return {
match: new RegExp(`${func}:function\\(.+?}`),
replace: `${func}:function(e){return true;}`
};
})
},
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: {
match: /canUseStickersEverywhere:function\(.+?}/,
replace: "canUseStickersEverywhere:function(e){return true;}"
},
},
{
find: "\"SENDABLE\"",
predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
replacement: {
match: /(\w+)\.available\?/,
replace: "true?"
}
},
{
find: "canUseAnimatedEmojis:function",
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
replacement: [
"canUseHighVideoUploadQuality",
"canStreamHighQuality",
"canStreamMidQuality"
].map(func => {
return {
match: new RegExp(`${func}:function\\(.+?}`),
replace: `${func}:function(e){return true;}`
};
})
},
{
find: "STREAM_FPS_OPTION.format",
predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
replacement: {
match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
replace: ""
}
},
],
options: {
enableEmojiBypass: {
description: "Allow sending fake emojis",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
},
emojiSize: {
description: "Size of the emojis when sending",
type: OptionType.SLIDER,
default: 48,
markers: [32, 48, 64, 128, 160, 256, 512],
},
enableStickerBypass: {
description: "Allow sending fake stickers",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
},
stickerSize: {
description: "Size of the stickers when sending",
type: OptionType.SLIDER,
default: 160,
markers: [32, 64, 128, 160, 256, 512],
},
enableStreamQualityBypass: {
description: "Allow streaming in nitro quality",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true,
}
},
get guildId() {
return window.location.href.split("channels/")[1].split("/")[0];
},
get canUseEmotes() {
return (UserStore.getCurrentUser().premiumType ?? 0) > 0;
},
get canUseStickers() {
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;
},
getStickerLink(stickerId: string) {
return `https://media.discordapp.net/stickers/${stickerId}.png?size=${Settings.plugins.FakeNitro.stickerSize}`;
},
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {
const [{ parseURL }, {
GIFEncoder,
quantize,
applyPalette
}] = await Promise.all([importApngJs(), getGifEncoder()]);
const { frames, width, height } = await parseURL(stickerLink);
const gif = new GIFEncoder();
const resolution = Settings.plugins.FakeNitro.stickerSize;
const canvas = document.createElement("canvas");
canvas.width = resolution;
canvas.height = resolution;
const ctx = canvas.getContext("2d", {
willReadFrequently: true
})!;
const scale = resolution / Math.max(width, height);
ctx.scale(scale, scale);
let lastImg: HTMLImageElement | null = null;
for (const { left, top, width, height, disposeOp, img, delay } of frames) {
ctx.drawImage(img, left, top, width, height);
const { data } = ctx.getImageData(0, 0, resolution, resolution);
const palette = quantize(data, 256);
const index = applyPalette(data, palette);
gif.writeFrame(index, resolution, resolution, {
transparent: true,
palette,
delay,
});
if (disposeOp === ApngDisposeOp.BACKGROUND) {
ctx.clearRect(left, top, width, height);
} else if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) {
ctx.drawImage(lastImg, left, top, width, height);
}
lastImg = img;
}
gif.finish();
const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
},
start() {
const settings = Settings.plugins.FakeNitro;
if (!settings.enableEmojiBypass && !settings.enableStickerBypass) {
return;
}
const EmojiStore = findByPropsLazy("getCustomEmojiById");
const StickerStore = findByPropsLazy("getAllGuildStickers") as {
getPremiumPacks(): StickerPack[];
getAllGuildStickers(): Map<string, Sticker[]>;
getStickerById(id: string): Sticker | undefined;
};
function getWordBoundary(origStr: string, offset: number) {
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
}
this.preSend = addPreSendListener((channelId, messageObj, extra) => {
const { guildId } = this;
stickerBypass: {
if (!settings.enableStickerBypass)
break stickerBypass;
const sticker = StickerStore.getStickerById(extra?.stickerIds?.[0]!);
if (!sticker)
break stickerBypass;
if (sticker.available !== false && (this.canUseStickers || (sticker as GuildSticker)?.guild_id === guildId))
break stickerBypass;
let link = this.getStickerLink(sticker.id);
if (sticker.format_type === 2) {
this.sendAnimatedSticker(this.getStickerLink(sticker.id), sticker.id, channelId);
return { cancel: true };
} else {
if ("pack_id" in sticker) {
const packId = sticker.pack_id === "847199849233514549"
// Discord moved these stickers into a different pack at some point, but
// Distok still uses the old id
? "749043879713701898"
: sticker.pack_id;
link = `https://distok.top/stickers/${packId}/${sticker.id}.gif`;
}
delete extra.stickerIds;
messageObj.content += " " + link;
}
}
if (!this.canUseEmotes && settings.enableEmojiBypass) {
for (const emoji of messageObj.validNonShortcutEmojis) {
if (!emoji.require_colons) continue;
if (emoji.guildId === guildId && !emoji.animated) continue;
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
});
}
}
return { cancel: false };
});
if (!this.canUseEmotes && settings.enableEmojiBypass) {
this.preEdit = addPreEditListener((_, __, messageObj) => {
const { guildId } = this;
for (const [emojiStr, _, emojiId] of messageObj.content.matchAll(/(?<!\\)<a?:(\w+):(\d+)>/ig)) {
const emoji = EmojiStore.getCustomEmojiById(emojiId);
if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
if (!emoji.require_colons) continue;
const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
});
}
});
}
},
stop() {
removePreSendListener(this.preSend);
removePreEditListener(this.preEdit);
}
});

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