From 4832035a18dfca311290508adab6b9921522d572 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 25 Sep 2022 16:43:02 -0400 Subject: [PATCH 01/57] Add headless API --- assets/thirdParty.png | Bin 0 -> 5899 bytes plugin/src/App/Components/TextButton.lua | 4 +- plugin/src/App/Notifications.lua | 38 +++++----- plugin/src/App/init.lua | 39 ++++++++-- plugin/src/Assets.lua | 1 + plugin/src/HeadlessAPI.lua | 92 +++++++++++++++++++++++ 6 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 assets/thirdParty.png create mode 100644 plugin/src/HeadlessAPI.lua diff --git a/assets/thirdParty.png b/assets/thirdParty.png new file mode 100644 index 0000000000000000000000000000000000000000..cdb1a32cd4efba7260646ed89ca07030c2dc888e GIT binary patch literal 5899 zcmZ8l1yodB*CvJ(kbwc|7%2&*bC8-5hE7Ftqz7rFBnPBn6ltVHX^|F5X_TQuLV6gw zLt1{`_x-+qz5l=NzW1)P&bj;9&$G|j_pB4GtF1~&&P0xfherulgX-VB75}^dD)F?y*m8oO!k^{@e%5X08 z7Y{w>dz;=JqlE!-|k+`gfbPBEvKdh|z}(9YS(aCKMMy zfci`hX3=x&_-+I7vbx^R<3$fdwXe>C43{_hEJ>7c#XyZ8z}99lJWs*^=!#hy6HYY& z0eQ(hqc^Bhzp#K2zH)9PU|tdSXX$O%_p&2Hcp;IU10p&4JRJe#@bK2VfJbArd?_5x zEQdGnd_Eu3Z{V@i@~RLiPLOW`XkGDAi|ywKwOmiaDPOxPFkds`4D^;9rApewKtY+8 ztyl>_V$*c=rIO@E{xKG9h!F;h%Q1@bD~5`8ujyE=dh{)QAZ3T0@`2Oh!2d+dxo1TR z2Ix{B7cqch7u1+l`%PEgj#29M(%yKfF;>MChFGBI6lv0R2!d6@+Jt}EfJAbX^*Q%Q zQFl}jMk0WA3O$`%rTSkSKJlYyxrL>lMUaZUV*IZhnElP2o){}wWuEac zQIpe*vReot%hkzMO8tMOBPdm0fRYKuaJscq{3|+<)IS%Jd2C~0FMscgx)1Dl4=iQo zU$?~f<-ai`g(xp{Q5zc(@Tw#92(Z;p?fxICh7}}wpQ~G80K(Ct!ihIlsP!!>Ae|T% z0a;09^Ls$~e<+U*2*(cEr7yl>1ou(fMV(^+S1583Eppbm16kKL)9LYM+-x4pa=t!m zwc{Kim$=^t-}5I$+GWCc<*4z33j9$=4S(j^F0D8{TN5S9E$ox z5=g6-t=LK0)WPqMqSXG6xKuqBwJ$`L937GhvI_Lo-iDAfC@ydF&8u_{ajn^5`_IVV z>9z`6_uh*YRpyu*su+FdQj?celpPVZxJR>K8VzrrFJji)XP}^kD~rVN)ytxyhW0*c z9rf^gaJE}f0b%n?;gqdWFfx8~Y#)8R&$E)Je^oE!DwbedvtF~Kf}#1z`$Qf+$$b4| zsYa8R4=A&FFckI>o=N~(#cI`oK@EI(f5xF@_6MT z9lfwhgn{*~KLn>uOz*SySp02+9|#eUQYvHC5iNuaCa+>UJ#UvpiTJWV(+sVo@=DRj z@y*;Zj0rq`m2z+QhpC1xEv{HSJi#Lzxmr|Q*kJ-4(Dn+IPosW>8Ev8k44gf#Hjw1?m zxKpGEW656C6)id~JY*VUoSqUmzd#l;K=Be>2{FDY7I&?73gE&~BDuXrI?nI&msz^L zk7Q|A)>#zFC7bvjE6&ULkM5@qtP2Kt2q!)TGADYKz<<>DW8qbP?e+V})o!8@r1g>& zh&8o(WhTMnZ2v-?RtK+F(B7MlbR@d1XsBQ-o^Kb~y2jgRpCIqt-ETje%9Ao1aoHoC+pS!CBpEJqJ_~W4a&nKe()#Z|rG^DQ>J+|h zS|*AC@lV{n$^%Az)h{QP5+g=ATIVKLUe)6}>adVl^HtF{S zNS272%G;B@S|(9Hx)b8|JqORShHQ{_Us;8td_m>}&(wim2S-|Uh72v?SFLDVo|$8< zZOD0{jr#N80dvEUqG65qyw2~PUuuaZRm~>YgFhBb#MRxIc?&*29Z$*yw8msYV?BvX zHe@h@ck^Zn+hxT0Jk*oc@G6oeyBCC|DX79a76uCAIegS?-h3bW3`za~Nw%gUmsTui zTf=UkS_VxM45uze)apHW_qZtAtE=jE6x_U~&IVai)sH6(c`ZhtHjx#%1D@nzEK`KxD1O9nAI_a5d$ql7VjHNauZQLcNOdtSIM*~ZB>!bF2xJ`TBwDY)g8o7$4oU@nbDT9T!>0%FswxG;-9s3~ED$L$;k}hpgaf zwt@0Akx{re$>Nk$nTpiI|3O;EB57>pl+#ml9fY=nzPMhGJP=}jSr}du`NQ;t{q(U& z41E*w*~HV*gZc^jvSv59Y;$Nt+4zQQcCq2Nv)C}~34#CPWlxBxgjOR7Pa&zcz_K5( z060ZWwL1u4jQ zy?#>bYCLpS{N8Eh7$cf4Hqvc_8PJlKmN`ClUk%EXu%o4fJ-;W@6BB?|EhtFeE`Q=v zb@>}OS)H0y`b5l>hLJ$;kDcxMTtElCp$1iXXPWmG=Wn7AJC4 zZmNp;(9xl}HKQ~Hgw;SK&3V4AoVHD@ep-Le>HN`OI;Zg?Z;eY^E$P;FyBFld(}8$aR2+5xRBJhf>gwE8Rluw1*wLV3)f`*O9`*%>F;X- z^uoEpNqajB-9n}m3l1^9EwXNRR5?m+@Ou(8xyY1MLBlmx(~U-~30lLt>NCq^>3zil z2WMj0Y4_(ZB0bJBlD-$9q_Yba3z{#!7Ws=jk7fNNphXQTyD5hP2QqBw8Fu$?5N$To z?eoAj_3<*$CN(gdp~K1Ao^ads(`k&6oX?PG0+-qFylTgpjP;WtLxZw8xraf&6t-r( z#(6JZvVTN&baDwvZO^I>vG>&c?b)Zy1m?FGG#%gMC4+Hgf1eelI=*McU^kW6I+tN> zIs2^ZJu&rQz@t)$8#DR*>r3D>dmGDH;MDyL0INco?&*_-HUP}RHkXrAki%yqtj%sA2Oy;m-yuf+t$e#Lb z`(a_6**$yB#VxD-Ssl$(tVXKfSx#^Zt~fH}{Q6gQYHb%)sFe-b+}Jp*(JP;f3LX-* zg@PsB*(tRBPdX0QfA~%#t)hjA%fwXlvnB5>yg+axQ0@yStsg>03apjT-V@Hh7|rW0 zU(v}IkHN*`cz?{hdInAD@&xr9nEfuha6Im^lzz51?YcAR)iZ6?1ETy@VidY)mffKT z=QJUq;Q;$1fVl9BbXdqU1f4E`Wly+*tZ4KRtp+uafpG4FSJ=k@SDjunqJT~%JzZ)g zWQB0U?>Jsv7lhUMZiq=wC`(&bxk=kR+NBV=SaZ1A({1BpSo(Uqlz0B3XV$-WciZA} z;Ya;eq5nv6ZFmt@gMBv{$IQ#?ir4MHFn~*UHb;iiCxi05IPk$$B=mX~UvnaSUu-kx z0O~1>sgybR*xvJUbH8G4wY;@8)Jao>f3c2wm_!WJZ6=kdyL%wZu~K0J1iQvy$6)S~ z#~O&&Mbu`8M~|~V8GdaFUf-WxA6q5==`Am5{%2k1WV=y9&)X#}ZBjfFBd@#v=V*YB zX+t~T;;Pqd=TeR;PF^Atv{pHoW%TB!b@y~IjKps{^gea$0{(3VecVj(m&|(R zgcHJBGEEH9BoElz-vJTTOm>r{#OKub;kWM|1VXEt_(tMauO2Ncgs{$aU%IVoGxZ5n zO-*D!{%*-n8Ea5neswq(xLGo14RzGO9(})}O+4|-w^uFu;;95`K6p(p)Uef7jJ$Q( zuuOA~O+(xW?;DTiZc5YQP#?XrLgnqA9ceonILS};2gmv z2k%SnudE)aMTqe`s0<9k|9TCjPOI5?X*PHc4)@nCaX3rAVUInpt_-dRud z(7kR2tP;#(-=O~+YauaZR2qB|`{9?1fXovCnVzYenH>gK{%OLrb3O1`{+m1}?E_+Z z3L8-YOLM*^13EF>)t6yMFPc3pyxoIpfGts zMdvBgqlif6pEqI1=TZ2xoEY7Xy_GkHy}l~8{{!S3e&JmihEM_lXJowJM3l|XRBo=` zlo>szzV@5F(>Ff4=0~r_JI*ra#4M%>HJ!@GPv-`*%tn62N`2`mx?lqXRm<&XCZr~B zc5i>k6O34Hakg*Ri8wVNcZ1|P6aDNs&{A^}-WBTaZWidkeQ)Z&U=V@GP>C+|!ERw; zu>AJS-JX1XTRO(Ic&tHncaw`PXYeJ0rN<|pHGruo@bs%o-1%*egC3T)XpZ2^~4VjJ&xUUza4y7!q|lcaBNS&{glS-FfYGdR{>TQj{t%3k%YtoO>I`bX!c zJ{xY(*{;eJGza5sn>7ZuqnwTp5`^ULn|sXIl9sP9L{l@uUa#8b4t)qAVBa?HO)zV8 zCTTk#0kmaUkqFV1i?e)vi5Zo;4stv<@L^%NaL@{z3fn6BseM$knwc?ZUZGG-xhJY? zGZdw}ipzD>BQnypy}zkJ3wR#GT0^nw0hphyeMd~Pby_IzAIuVcnc8n)z)YW?_nB;! zO%#0XF6{2!<&REQqW`j(Uz=(9y*vcZ}by+=(9wCcpH(EB5>x) z0g@&LAB2&f5>>Dncj(a^nIPUiEg5H_bCu3~u4AU%=qc$8UW*-N8GvV@B@F89K-dF`f-#)kR)&#pMwe1?!wJyg(is=P5qL_qEmUlNpHMqA6X%m|m zC%ZKax-tH!9*2r26h~I$LwjFyzXALx+m%;*Qhm&d9@oiXD;+D3Wl<(K$4c7aDWudu z69`$1sxU+Yw3jR7KNQD}^_HTxV-^faM~IM;d?QSk+m&!$0ug}@}4o?*u2Uj)c~xM2o9c9@L3+D&*U#{ z=x~zrM=^m3jsf=#a^Dk}rIuW5hIB>O78dP`w7)PMC3%~qIVxWBj8G@XkLN5}R%qPm z+?vA@_=0uMYk@wuQ&3$w_ALjLS3R=YLmQD~^dHYntIj`X@R2efALAmwt*f8S& zY*rQmevYcG9L{JjSKJ|B!(+q$;^`aMs=iQTXfqe(hEk5$+#GnHW!+;#_j3?DYqeny=ep5^bP<)J(Bu6e#=DO+<#t@Fjka8n3Rb-oa zU{Fy$j@+g8_)8M@@$em;J8NOb-XF%NdyR%po#xIai9P&=4=k z^QyX3N05ky5N@{nuapKB*Iipcd6iP|vRH`p9sylI4|3SezAg6d|7n>=d?2sZ^zj)T zFy*ACi{ls4D~r%5MBZ^vQctId6#M!ohza7x>sH8c#ZaNoTFChc!1(BC=+Hd}Udoub z4{>+52m(RN&qU5Sw1afswH-4v$nQezO6{vZ_S&MmuStq@lcb}STHrT_+<0)9HuSSH HBJ_U%F5okJ literal 0 HcmV?d00001 diff --git a/plugin/src/App/Components/TextButton.lua b/plugin/src/App/Components/TextButton.lua index d33ddf9f2..89fcb5047 100644 --- a/plugin/src/App/Components/TextButton.lua +++ b/plugin/src/App/Components/TextButton.lua @@ -42,7 +42,7 @@ end function TextButton:render() return Theme.with(function(theme) local textSize = TextService:GetTextSize( - self.props.text, 18, Enum.Font.GothamSemibold, + self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge) ) @@ -85,7 +85,7 @@ function TextButton:render() Text = e("TextLabel", { Text = self.props.text, - Font = Enum.Font.GothamSemibold, + Font = Enum.Font.GothamMedium, TextSize = 18, TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor), TextTransparency = self.props.transparency, diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index 5e1b0882c..32380c95d 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -91,7 +91,7 @@ function Notification:render() local textBounds = TextService:GetTextSize( self.props.text, 15, - Enum.Font.GothamSemibold, + Enum.Font.GothamMedium, Vector2.new(350, 700) ) @@ -102,7 +102,7 @@ function Notification:render() local size = self.binding:map(function(value) return UDim2.fromOffset( (35+40+textBounds.X)*value, - math.max(14+20+textBounds.Y, 32+20) + math.max(16+20+textBounds.Y, 54) ) end) @@ -129,15 +129,15 @@ function Notification:render() }, { Logo = e("ImageLabel", { ImageTransparency = transparency, - Image = Assets.Images.PluginButton, + Image = if self.props.thirdParty then Assets.Images.ThirdPartyPlugin else Assets.Images.PluginButton, BackgroundTransparency = 1, Size = UDim2.new(0, 32, 0, 32), Position = UDim2.new(0, 0, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), }), - Info = e("TextLabel", { + Message = e("TextLabel", { Text = self.props.text, - Font = Enum.Font.GothamSemibold, + Font = Enum.Font.GothamMedium, TextSize = 15, TextColor3 = theme.Notification.InfoColor, TextTransparency = transparency, @@ -145,21 +145,21 @@ function Notification:render() TextWrapped = true, Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), - Position = UDim2.fromOffset(35, 0), + Position = UDim2.fromOffset(38, 0), LayoutOrder = 1, BackgroundTransparency = 1, }), - Time = e("TextLabel", { - Text = time:FormatLocalTime("LTS", "en-us"), - Font = Enum.Font.Code, + Info = e("TextLabel", { + Text = if self.props.source then "From: " .. self.props.source else time:FormatLocalTime("LTS", "en-us"), + Font = Enum.Font.Gotham, TextSize = 12, TextColor3 = theme.Notification.InfoColor, TextTransparency = transparency, TextXAlignment = Enum.TextXAlignment.Left, - Size = UDim2.new(1, -35, 0, 14), - Position = UDim2.new(0, 35, 1, -14), + Size = UDim2.new(1, -38, 0, 14), + Position = UDim2.new(0, 38, 1, -14), LayoutOrder = 1, BackgroundTransparency = 1, @@ -167,8 +167,8 @@ function Notification:render() }), Padding = e("UIPadding", { - PaddingLeft = UDim.new(0, 17), - PaddingRight = UDim.new(0, 15), + PaddingLeft = UDim.new(0, 12), + PaddingRight = UDim.new(0, 12), }), }) }) @@ -181,16 +181,18 @@ function Notifications:render() local notifs = {} for index, notif in ipairs(self.props.notifications) do - notifs[notif] = e(Notification, { + local props = { soundPlayer = self.props.soundPlayer, - text = notif.text, - timestamp = notif.timestamp, - timeout = notif.timeout, layoutOrder = (notif.timestamp - baseClock), onClose = function() self.props.onClose(index) end, - }) + } + for key, value in notif do + props[key] = value + end + + notifs[notif] = e(Notification, props) end return Roact.createFragment(notifs) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 9f7e2ecf6..c8ea22b68 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -16,6 +16,7 @@ local strict = require(Plugin.strict) local Dictionary = require(Plugin.Dictionary) local ServeSession = require(Plugin.ServeSession) local ApiContext = require(Plugin.ApiContext) +local HeadlessAPI = require(Plugin.HeadlessAPI) local preloadAssets = require(Plugin.preloadAssets) local soundPlayer = require(Plugin.soundPlayer) local Theme = require(script.Theme) @@ -48,6 +49,11 @@ function App:init() self.host, self.setHost = Roact.createBinding(priorHost or "") self.port, self.setPort = Roact.createBinding(priorPort or "") + self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self, Config, Settings) + + -- selene: allow(global_usage) + _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar + self.patchInfo, self.setPatchInfo = Roact.createBinding({ changes = 0, timestamp = os.time(), @@ -78,6 +84,25 @@ function App:addNotification(text: string, timeout: number?) }) end +function App:addThirdPartyNotification(source: string, text: string, timeout: number?) + if not Settings:get("showNotifications") then + return + end + + local notifications = table.clone(self.state.notifications) + table.insert(notifications, { + text = text, + timestamp = DateTime.now().UnixTimestampMillis, + timeout = timeout or 3, + thirdParty = true, + source = source, + }) + + self:setState({ + notifications = notifications, + }) +end + function App:closeNotification(index: number) local notifications = table.clone(self.state.notifications) table.remove(notifications, index) @@ -126,7 +151,7 @@ function App:setPriorEndpoint(host: string, port: string) Settings:set("priorEndpoints", priorEndpoints) end -function App:getHostAndPort() +function App:getHostAndPort(): (string, string) local host = self.host:getValue() local port = self.port:getValue() @@ -179,7 +204,7 @@ function App:releaseSyncLock() Log.trace("Could not relase sync lock because it is owned by {}", lock.Value) end -function App:startSession() +function App:startSession(host: string?, port: string?) local claimedLock, priorOwner = self:claimSyncLock() if not claimedLock then local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner)) @@ -195,14 +220,16 @@ function App:startSession() return end - local host, port = self:getHostAndPort() + if host == nil or port == nil then + host, port = self:getHostAndPort() + end local sessionOptions = { openScriptsExternally = Settings:get("openScriptsExternally"), twoWaySync = Settings:get("twoWaySync"), } - local baseUrl = ("http://%s:%s"):format(host, port) + local baseUrl = string.format("http://%s:%s", host :: string, port :: string) local apiContext = ApiContext.new(baseUrl) local serveSession = ServeSession.new({ @@ -240,6 +267,8 @@ function App:startSession() end) serveSession:onStatusChanged(function(status, details) + self.headlessAPI.Connected = status == ServeSession.Status.Connected + if status == ServeSession.Status.Connecting then self:setPriorEndpoint(host, port) @@ -249,7 +278,7 @@ function App:startSession() }) self:addNotification("Connecting to session...") elseif status == ServeSession.Status.Connected then - local address = ("%s:%s"):format(host, port) + local address = string.format("%s:%s", host :: string, port :: string) self:setState({ appStatus = AppStatus.Connected, projectName = details, diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 08f86b3b8..a5484aa5c 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -20,6 +20,7 @@ local Assets = { PluginButton = "rbxassetid://3405341609", PluginButtonConnected = "rbxassetid://9529783993", PluginButtonWarning = "rbxassetid://9529784530", + ThirdPartyPlugin = "rbxassetid://11064843298", Icons = { Close = "rbxassetid://6012985953", Back = "rbxassetid://6017213752", diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua new file mode 100644 index 000000000..0bb540a80 --- /dev/null +++ b/plugin/src/HeadlessAPI.lua @@ -0,0 +1,92 @@ +local API = {} + +function API.new(app, config, settings) + local Rojo = {} + + Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false + Rojo.Version = table.clone(config.version) + Rojo.ProtocolVersion = config.protocolVersion + + Rojo._notifRateLimit = {} + + function Rojo:Test(...) + print("Rojo:Test called by", Rojo:_getCaller(), "with args", ...) + end + + function Rojo:_getCaller() + local traceback = string.split(debug.traceback(), "\n") + local topLevel = traceback[#traceback - 1] + + local debugPlugin = string.match(topLevel, "^PluginDebugService%.user_(.-)%.") + if debugPlugin then + return debugPlugin + end + + local localPlugin = string.match(topLevel, "^user_(.-)%.") + if localPlugin then + return localPlugin + end + + local cloudPlugin = string.match(topLevel, "cloud_%d-%.(.-)%.") + if cloudPlugin then + return cloudPlugin + end + + return "Command Bar" + end + + function Rojo:ConnectAsync(host: string?, port: number?) + app:startSession(host, port) + end + + function Rojo:DisconnectAsync() + app:endSession() + end + + function Rojo:GetSetting(setting: string): any + return settings:get(setting) + end + + function Rojo:SetSetting(setting: string, value: any) + return settings:set(setting, value) + end + + function Rojo:Notify(msg: string, timeout: number?) + local source = Rojo:_getCaller() + + if Rojo._notifRateLimit[source] == nil then + Rojo._notifRateLimit[source] = 0 + elseif Rojo._notifRateLimit[source] > 45 then + return -- Rate limited + end + + Rojo._notifRateLimit[source] += 1 + task.delay(30, function() + Rojo._notifRateLimit[source] -= 1 + end) + + app:addThirdPartyNotification(source, msg, timeout) + return + end + + function Rojo:GetHostAndPort(): (string, string) + return app:getHostAndPort() + end + + local ReadOnly = setmetatable({}, { + __index = function(_, key) + if string.find(key, "^_") then + return nil -- Don't expose private members + end + return Rojo[key] + end, + __newindex = function(_, key, value) + error(string.format("Attempted to set Rojo.%s to %q but it's a read-only value", key, value), 2) + return + end, + }) + + return Rojo, ReadOnly +end + +return API From be89259bbdd3e35160c207a91abb252a6bca7bdf Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 25 Sep 2022 17:10:30 -0400 Subject: [PATCH 02/57] Add Address and ProjectName when connected --- plugin/src/App/init.lua | 8 ++++++++ plugin/src/HeadlessAPI.lua | 2 ++ 2 files changed, 10 insertions(+) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index c8ea22b68..9d7fae3fb 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -268,6 +268,10 @@ function App:startSession(host: string?, port: string?) serveSession:onStatusChanged(function(status, details) self.headlessAPI.Connected = status == ServeSession.Status.Connected + if not self.headlessAPI.Connected then + self.headlessAPI.Address = nil + self.headlessAPI.ProjectName = nil + end if status == ServeSession.Status.Connecting then self:setPriorEndpoint(host, port) @@ -279,6 +283,10 @@ function App:startSession(host: string?, port: string?) self:addNotification("Connecting to session...") elseif status == ServeSession.Status.Connected then local address = string.format("%s:%s", host :: string, port :: string) + + self.headlessAPI.Address = address + self.headlessAPI.ProjectName = details + self:setState({ appStatus = AppStatus.Connected, projectName = details, diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 0bb540a80..5c0c0ec0f 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -4,6 +4,8 @@ function API.new(app, config, settings) local Rojo = {} Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false + Rojo.Address = nil + Rojo.ProjectName = nil Rojo.Version = table.clone(config.version) Rojo.ProtocolVersion = config.protocolVersion From f3de0c35eff67c09bad6e3a4b34fab43467fe624 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 25 Sep 2022 17:22:59 -0400 Subject: [PATCH 03/57] Nicer notification formatting and info --- plugin/src/App/Notifications.lua | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index 32380c95d..310e5b882 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -87,6 +87,9 @@ end function Notification:render() local time = DateTime.fromUnixTimestampMillis(self.props.timestamp) + local infoText = + time:FormatLocalTime("LTS", "en-us") .. + (if self.props.source then " • From: " .. self.props.source else "") local textBounds = TextService:GetTextSize( self.props.text, @@ -95,13 +98,22 @@ function Notification:render() Vector2.new(350, 700) ) + local infoTextBounds = TextService:GetTextSize( + infoText, + 12, + Enum.Font.Gotham, + Vector2.new(350, 14) + ) + + local textWidth = math.max(infoTextBounds.X, textBounds.X) + local transparency = self.binding:map(function(value) return 1 - value end) local size = self.binding:map(function(value) return UDim2.fromOffset( - (35+40+textBounds.X)*value, + (35+40+textWidth)*value, math.max(16+20+textBounds.Y, 54) ) end) @@ -123,7 +135,7 @@ function Notification:render() size = UDim2.new(1, 0, 1, 0), }, { TextContainer = e("Frame", { - Size = UDim2.new(0, 35+textBounds.X, 1, -20), + Size = UDim2.new(1, 0, 1, -20), Position = UDim2.new(0, 0, 0, 10), BackgroundTransparency = 1 }, { @@ -151,7 +163,7 @@ function Notification:render() BackgroundTransparency = 1, }), Info = e("TextLabel", { - Text = if self.props.source then "From: " .. self.props.source else time:FormatLocalTime("LTS", "en-us"), + Text = infoText, Font = Enum.Font.Gotham, TextSize = 12, TextColor3 = theme.Notification.InfoColor, From 2a25193941a2fe57bc93530785c5fbc0a04606bc Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 27 Sep 2022 01:59:48 -0400 Subject: [PATCH 04/57] Add CreateApiContext and more caller info --- plugin/src/App/init.lua | 2 +- plugin/src/HeadlessAPI.lua | 64 ++++++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 9d7fae3fb..42ce97d53 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -49,7 +49,7 @@ function App:init() self.host, self.setHost = Roact.createBinding(priorHost or "") self.port, self.setPort = Roact.createBinding(priorPort or "") - self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self, Config, Settings) + self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self) -- selene: allow(global_usage) _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 5c0c0ec0f..caa2d2327 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -1,13 +1,20 @@ +local Parent = script:FindFirstAncestor("Rojo") +local Plugin = Parent.Plugin + +local Config = require(Plugin.Config) +local Settings = require(Plugin.Settings) +local ApiContext = require(Plugin.ApiContext) + local API = {} -function API.new(app, config, settings) +function API.new(app) local Rojo = {} Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false Rojo.Address = nil Rojo.ProjectName = nil - Rojo.Version = table.clone(config.version) - Rojo.ProtocolVersion = config.protocolVersion + Rojo.Version = table.clone(Config.version) + Rojo.ProtocolVersion = Config.protocolVersion Rojo._notifRateLimit = {} @@ -15,6 +22,28 @@ function API.new(app, config, settings) print("Rojo:Test called by", Rojo:_getCaller(), "with args", ...) end + function Rojo:_getCallerFull() + local traceback = string.split(debug.traceback(), "\n") + local topLevel = traceback[#traceback - 1] + + local debugPlugin = string.match(topLevel, "^(PluginDebugService%.user_.-)%.") + if debugPlugin then + return debugPlugin + end + + local localPlugin = string.match(topLevel, "^(user_.-)%.") + if localPlugin then + return localPlugin + end + + local cloudPlugin = string.match(topLevel, "(cloud_%d-%..-)%.") + if cloudPlugin then + return cloudPlugin + end + + return "RobloxStudio.CommandBar" + end + function Rojo:_getCaller() local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1] @@ -34,7 +63,26 @@ function API.new(app, config, settings) return cloudPlugin end - return "Command Bar" + return "CommandBar" + end + + function Rojo:_getCallerType() + local traceback = string.split(debug.traceback(), "\n") + local topLevel = traceback[#traceback - 1] + + if string.find(topLevel, "^PluginDebugService%.user_") then + return "Debug" + end + + if string.find(topLevel, "^user_") then + return "Local" + end + + if string.find(topLevel, "cloud_%d+%.") then + return "Cloud" + end + + return "CommandBar" end function Rojo:ConnectAsync(host: string?, port: number?) @@ -46,11 +94,11 @@ function API.new(app, config, settings) end function Rojo:GetSetting(setting: string): any - return settings:get(setting) + return Settings:get(setting) end function Rojo:SetSetting(setting: string, value: any) - return settings:set(setting, value) + return Settings:set(setting, value) end function Rojo:Notify(msg: string, timeout: number?) @@ -75,6 +123,10 @@ function API.new(app, config, settings) return app:getHostAndPort() end + function Rojo:CreateApiContext(baseUrl: string) + return ApiContext.new(baseUrl) + end + local ReadOnly = setmetatable({}, { __index = function(_, key) if string.find(key, "^_") then From 17b2752b38ca4edda4ddc1ce61c56953d9e87ea0 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 27 Sep 2022 22:21:23 -0400 Subject: [PATCH 05/57] Add Changed event --- plugin/src/App/init.lua | 10 +++++----- plugin/src/HeadlessAPI.lua | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 42ce97d53..57ae92efd 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -267,10 +267,10 @@ function App:startSession(host: string?, port: string?) end) serveSession:onStatusChanged(function(status, details) - self.headlessAPI.Connected = status == ServeSession.Status.Connected + self.headlessAPI:_updateProperty("Connected", status == ServeSession.Status.Connected) if not self.headlessAPI.Connected then - self.headlessAPI.Address = nil - self.headlessAPI.ProjectName = nil + self.headlessAPI:_updateProperty("Address", nil) + self.headlessAPI:_updateProperty("ProjectName", nil) end if status == ServeSession.Status.Connecting then @@ -284,8 +284,8 @@ function App:startSession(host: string?, port: string?) elseif status == ServeSession.Status.Connected then local address = string.format("%s:%s", host :: string, port :: string) - self.headlessAPI.Address = address - self.headlessAPI.ProjectName = details + self.headlessAPI:_updateProperty("Address", address) + self.headlessAPI:_updateProperty("ProjectName", details) self:setState({ appStatus = AppStatus.Connected, diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index caa2d2327..f17435ce3 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -10,16 +10,20 @@ local API = {} function API.new(app) local Rojo = {} + Rojo._notifRateLimit = {} + Rojo._changedEvent = Instance.new("BindableEvent") + + Rojo.Changed = Rojo._changedEvent.Event Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false Rojo.Address = nil Rojo.ProjectName = nil Rojo.Version = table.clone(Config.version) Rojo.ProtocolVersion = Config.protocolVersion - Rojo._notifRateLimit = {} - - function Rojo:Test(...) - print("Rojo:Test called by", Rojo:_getCaller(), "with args", ...) + function Rojo:_updateProperty(property: string, value: any?) + local oldValue = Rojo[property] + Rojo[property] = value + Rojo._changedEvent:Fire(property, value, oldValue) end function Rojo:_getCallerFull() @@ -85,6 +89,10 @@ function API.new(app) return "CommandBar" end + function Rojo:Test(...) + print("Rojo:Test called by", Rojo:_getCaller(), "with args", ...) + end + function Rojo:ConnectAsync(host: string?, port: number?) app:startSession(host, port) end From 6284d83ed7faefa204de98a5eb9c76f5ffa2d4a2 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 27 Sep 2022 23:27:33 -0400 Subject: [PATCH 06/57] Use full caller as key to avoid conflicts --- plugin/src/HeadlessAPI.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index f17435ce3..a8910e517 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -110,7 +110,7 @@ function API.new(app) end function Rojo:Notify(msg: string, timeout: number?) - local source = Rojo:_getCaller() + local source = Rojo:_getCallerFull() if Rojo._notifRateLimit[source] == nil then Rojo._notifRateLimit[source] = 0 @@ -123,7 +123,7 @@ function API.new(app) Rojo._notifRateLimit[source] -= 1 end) - app:addThirdPartyNotification(source, msg, timeout) + app:addThirdPartyNotification(Rojo:_getCaller(), msg, timeout) return end From d7cee5c5beb536a8dd31d6f03c3437653278dfde Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 28 Sep 2022 17:01:17 -0400 Subject: [PATCH 07/57] Add api permissions --- plugin/src/App/Components/ScrollingFrame.lua | 1 + plugin/src/App/Components/Toggle.lua | 134 ++++++++++++++++ plugin/src/App/PermissionPopup.lua | 125 +++++++++++++++ plugin/src/App/init.lua | 75 +++++++++ plugin/src/HeadlessAPI.lua | 160 +++++++++++++++---- plugin/src/Settings.lua | 1 + 6 files changed, 467 insertions(+), 29 deletions(-) create mode 100644 plugin/src/App/Components/Toggle.lua create mode 100644 plugin/src/App/PermissionPopup.lua diff --git a/plugin/src/App/Components/ScrollingFrame.lua b/plugin/src/App/Components/ScrollingFrame.lua index 2231aacb2..ae3017991 100644 --- a/plugin/src/App/Components/ScrollingFrame.lua +++ b/plugin/src/App/Components/ScrollingFrame.lua @@ -25,6 +25,7 @@ local function ScrollingFrame(props) ElasticBehavior = Enum.ElasticBehavior.Always, ScrollingDirection = Enum.ScrollingDirection.Y, + LayoutOrder = props.layoutOrder, Size = props.size, Position = props.position, AnchorPoint = props.anchorPoint, diff --git a/plugin/src/App/Components/Toggle.lua b/plugin/src/App/Components/Toggle.lua new file mode 100644 index 000000000..924fe075f --- /dev/null +++ b/plugin/src/App/Components/Toggle.lua @@ -0,0 +1,134 @@ +local TextService = game:GetService("TextService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Theme = require(Plugin.App.Theme) + +local Checkbox = require(Plugin.App.Components.Checkbox) + +local e = Roact.createElement + +local DIVIDER_FADE_SIZE = 0.1 + +local function getTextBounds(text, textSize, font, lineHeight, bounds) + local textBounds = TextService:GetTextSize(text, textSize, font, bounds) + + local lineCount = textBounds.Y / textSize + local lineHeightAbsolute = textSize * lineHeight + + return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize)) +end + +local Toggle = Roact.Component:extend("Toggle") + +function Toggle:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function Toggle:render() + return Theme.with(function(theme) + theme = theme.Settings + + return e("Frame", { + Size = self.contentSize:map(function(value) + return UDim2.new(1, 0, 0, 20 + value.Y + 20) + end), + LayoutOrder = self.props.layoutOrder, + ZIndex = -self.props.layoutOrder, + BackgroundTransparency = 1, + + [Roact.Change.AbsoluteSize] = function(object) + self.setContainerSize(object.AbsoluteSize) + end, + }, { + Input = e(Checkbox, { + active = self.props.active, + transparency = self.props.transparency, + position = UDim2.new(1, 0, 0.5, 0), + anchorPoint = Vector2.new(1, 0.5), + onClick = self.props.onClick, + }), + + Text = e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Name = e("TextLabel", { + Text = self.props.name, + Font = Enum.Font.GothamBold, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + + Size = UDim2.new(1, 0, 0, 17), + + LayoutOrder = 1, + BackgroundTransparency = 1, + }), + + Description = e("TextLabel", { + Text = self.props.description, + Font = Enum.Font.Gotham, + LineHeight = 1.2, + TextSize = 14, + TextColor3 = theme.Setting.DescriptionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + TextWrapped = true, + + Size = self.containerSize:map(function(value) + local offset = self.props.options ~= nil and 120 or 40 + local textBounds = getTextBounds( + self.props.description, 14, Enum.Font.Gotham, 1.2, + Vector2.new(value.X - offset, math.huge) + ) + return UDim2.new(1, -offset, 0, textBounds.Y) + end), + + LayoutOrder = 2, + BackgroundTransparency = 1, + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 6), + + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), + + Padding = e("UIPadding", { + PaddingTop = UDim.new(0, 20), + PaddingBottom = UDim.new(0, 20), + }), + }), + + Divider = e("Frame", { + BackgroundColor3 = theme.DividerColor, + BackgroundTransparency = self.props.transparency, + Size = UDim2.new(1, 0, 0, 1), + BorderSizePixel = 0, + }, { + Gradient = e("UIGradient", { + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1), + NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1, 1), + }), + }), + }), + }) + end) +end + +return Toggle diff --git a/plugin/src/App/PermissionPopup.lua b/plugin/src/App/PermissionPopup.lua new file mode 100644 index 000000000..72947daab --- /dev/null +++ b/plugin/src/App/PermissionPopup.lua @@ -0,0 +1,125 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Theme = require(Plugin.App.Theme) + +local Toggle = require(Plugin.App.Components.Toggle) +local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) +local TextButton = require(Plugin.App.Components.TextButton) + +local e = Roact.createElement + +local PermissionPopup = Roact.Component:extend("PermissionPopup") + +function PermissionPopup:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.infoSize, self.setInfoSize = Roact.createBinding(Vector2.new(0, 0)) + + local response = {} + for _, api in self.props.apis do + response[api] = if self.props.initialState[api] == nil then true else self.props.initialState[api] + end + + self:setState({ + response = response, + }) +end + +function PermissionPopup:render() + return Theme.with(function(theme) + theme = theme.Settings + + local apiToggles = {} + for index, api in self.props.apis do + apiToggles[api] = e(Toggle, { + active = self.state.response[api], + name = api, + description = self.props.apiDescriptions[api], + transparency = self.props.transparency, + layoutOrder = index, + onClick = function() + self:setState(function(state) + state.response[api] = not state.response[api] + return state + end) + end, + }) + end + + return e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 5), + HorizontalAlignment = Enum.HorizontalAlignment.Right, + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + PaddingTop = UDim.new(0, 15), + PaddingBottom = UDim.new(0, 15), + }), + + Info = e("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Text = string.format("A third-party plugin, %s, is asking to use the following parts of the Rojo API. Please grant/deny access.", self.props.name or "[Unknown]"), + Font = Enum.Font.GothamMedium, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + TextTransparency = self.props.transparency, + LayoutOrder = 1, + + [Roact.Change.AbsoluteSize] = function(rbx) + self.setInfoSize(rbx.AbsoluteSize) + end, + }), + + Submit = e(TextButton, { + text = "Submit", + style = "Solid", + transparency = self.props.transparency, + layoutOrder = 3, + onClick = function() + self.props.responseEvent:Fire(self.state.response) + end, + }), + + ScrollingFrame = e(ScrollingFrame, { + size = self.infoSize:map(function(infoSize) + return UDim2.new(1, 0, 1, -infoSize.Y-44) + end), + layoutOrder = 2, + contentSize = self.contentSize, + transparency = self.props.transparency, + }, { + APIToggles = Roact.createFragment(apiToggles), + + Padding = e("UIPadding", { + PaddingRight = UDim.new(0, 15), + }), + + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), + }), + }) + end) +end + +return PermissionPopup diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 57ae92efd..da11f7de6 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -23,6 +23,7 @@ local Theme = require(script.Theme) local Page = require(script.Page) local Notifications = require(script.Notifications) +local PermissionPopup = require(script.PermissionPopup) local StudioPluginAction = require(script.Components.Studio.StudioPluginAction) local StudioToolbar = require(script.Components.Studio.StudioToolbar) local StudioToggleButton = require(script.Components.Studio.StudioToggleButton) @@ -64,6 +65,7 @@ function App:init() guiEnabled = false, notifications = {}, toolbarIcon = Assets.Images.PluginButton, + popups = {}, }) end @@ -204,6 +206,39 @@ function App:releaseSyncLock() Log.trace("Could not relase sync lock because it is owned by {}", lock.Value) end +function App:requestPermission(source: string, name: string, apis: {string}, initialState: {[string]: boolean?}): {[string]: boolean} + local responseEvent = Instance.new("BindableEvent") + + self:setState(function(state) + state.popups[source] = { + responseEvent = responseEvent, + initialState = initialState, + name = name, + apis = apis, + content = e(PermissionPopup, { + responseEvent = responseEvent, + initialState = initialState, + source = source, + name = name, + apis = apis, + apiDescriptions = self.headlessAPI._apiDescriptions, + transparency = Roact.createBinding(0), + }), + } + return state + end) + + local response = responseEvent.Event:Wait() + responseEvent:Destroy() + + self:setState(function(state) + state.popups[source] = nil + return state + end) + + return response +end + function App:startSession(host: string?, port: string?) local claimedLock, priorOwner = self:claimSyncLock() if not claimedLock then @@ -364,10 +399,50 @@ function App:render() return e(Page, props) end + local popups = {} + for id, popup in self.state.popups do + popups["Rojo_"..id] = e(StudioPluginGui, { + id = id, + title = popup.name .. " Popup", + active = true, + + initDockState = Enum.InitialDockState.Top, + initEnabled = true, + overridePreviousState = true, + floatingSize = Vector2.new(400, 300), + minimumSize = Vector2.new(390, 240), + + zIndexBehavior = Enum.ZIndexBehavior.Sibling, + + onClose = function() + popup.responseEvent:Fire(popup.initialState) + popup.responseEvent:Destroy() + + self:setState(function(state) + state[id] = nil + return state + end) + end, + }, { + Content = popup.content, + + Background = Theme.with(function(theme) + return e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundColor3 = theme.BackgroundColor, + ZIndex = 0, + BorderSizePixel = 0, + }) + end), + }) + end + return e(StudioPluginContext.Provider, { value = self.props.plugin, }, { e(Theme.StudioProvider, nil, { + popups = Roact.createFragment(popups), + gui = e(StudioPluginGui, { id = pluginName, title = pluginName, diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index a8910e517..33168e5ba 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -1,22 +1,44 @@ +local MarketplaceService = game:GetService("MarketplaceService") + local Parent = script:FindFirstAncestor("Rojo") local Plugin = Parent.Plugin +local Packages = Parent.Packages + +local Log = require(Packages.Log) local Config = require(Plugin.Config) local Settings = require(Plugin.Settings) local ApiContext = require(Plugin.ApiContext) +local cloudIdInfoCache = {} +local apiPermissionAllowlist = { + Version = true, + ProtocolVersion = true, + RequestAccess = true, +} + local API = {} function API.new(app) local Rojo = {} Rojo._notifRateLimit = {} + Rojo._permissions = Settings:get("apiPermissions") or {} Rojo._changedEvent = Instance.new("BindableEvent") + Rojo._apiDescriptions = {} Rojo.Changed = Rojo._changedEvent.Event + Rojo._apiDescriptions.Changed = "An event that fires when a headless API property changes" + Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false + Rojo._apiDescriptions.Connected = "Whether or not the plugin is connected to a Rojo server" + Rojo.Address = nil + Rojo._apiDescriptions.Address = "The address (host:port) that the plugin is connected to" + Rojo.ProjectName = nil + Rojo._apiDescriptions.ProjectName = "The name of the project that the plugin is connected to" + Rojo.Version = table.clone(Config.version) Rojo.ProtocolVersion = Config.protocolVersion @@ -26,59 +48,58 @@ function API.new(app) Rojo._changedEvent:Fire(property, value, oldValue) end - function Rojo:_getCallerFull() + function Rojo:_getCallerSource() local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1] - local debugPlugin = string.match(topLevel, "^(PluginDebugService%.user_.-)%.") - if debugPlugin then - return debugPlugin - end - - local localPlugin = string.match(topLevel, "^(user_.-)%.") + local localPlugin = string.match(topLevel, "(user_.-)%.") if localPlugin then return localPlugin end - local cloudPlugin = string.match(topLevel, "(cloud_%d-%..-)%.") + local cloudPlugin = string.match(topLevel, "(cloud_%d-)%.") if cloudPlugin then return cloudPlugin end - return "RobloxStudio.CommandBar" + return "RobloxStudio_CommandBar" end - function Rojo:_getCaller() + function Rojo:_getCallerName() local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1] - local debugPlugin = string.match(topLevel, "^PluginDebugService%.user_(.-)%.") - if debugPlugin then - return debugPlugin - end - - local localPlugin = string.match(topLevel, "^user_(.-)%.") + local localPlugin = string.match(topLevel, "user_(.-)%.") if localPlugin then return localPlugin end - local cloudPlugin = string.match(topLevel, "cloud_%d-%.(.-)%.") - if cloudPlugin then - return cloudPlugin + local cloudId, cloudInstance = string.match(topLevel, "cloud_(%d-)%.(.-)%.") + if cloudId then + local info = cloudIdInfoCache[cloudId] + if info then + return info.Name .. " by " .. info.Creator.Name + else + local success, newInfo = pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) + if success then + cloudIdInfoCache[cloudId] = newInfo + return newInfo.Name .. " by " .. newInfo.Creator.Name + end + end + + -- Fallback to the name of the instance uploaded inside this plugin + -- The reason this is not ideal is because creators often upload a folder named "Main" or something + return cloudInstance end - return "CommandBar" + return "Command Bar" end function Rojo:_getCallerType() local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1] - if string.find(topLevel, "^PluginDebugService%.user_") then - return "Debug" - end - - if string.find(topLevel, "^user_") then + if string.find(topLevel, "user_") then return "Local" end @@ -89,28 +110,95 @@ function API.new(app) return "CommandBar" end + function Rojo:_permissionCheck(key: string): boolean + if apiPermissionAllowlist[key] then return true end + + local source = Rojo:_getCallerSource() + if Rojo._permissions[source] == nil then + Rojo._permissions[source] = {} + end + + return not not Rojo._permissions[source][key] + end + + function Rojo:RequestAccess(apis: {string}): {[string]: boolean} + local source, name = Rojo:_getCallerSource(), Rojo:_getCallerName() + + if Rojo._permissions[source] == nil then + Rojo._permissions[source] = {} + end + + local alreadyAllowed = true + for _, api in apis do + if not Rojo._permissions[source][api] then + alreadyAllowed = false + break + end + end + + if alreadyAllowed then + local response = {} + for _, api in apis do + response[api] = true + end + return response + end + + local response = app:requestPermission(source, name, apis, Rojo._permissions[source]) + + for api, granted in response do + Log.warn(string.format( + "%s Rojo.%s for '%s'", + granted and "Granting permission to" or "Denying permission to", api, name + )) + Rojo._permissions[source][api] = granted + end + Settings:set("apiPermissions", Rojo._permissions) + + return response + end + function Rojo:Test(...) - print("Rojo:Test called by", Rojo:_getCaller(), "with args", ...) + local args = table.pack(...) + for i=1, args.n do + local v = args[i] + local t = type(v) + if t == "string" then + args[i] = string.format("%q", v) + else + args[i] = tostring(v) + end + end + + print(string.format( + "Rojo:Test(%s) called from '%s'", + table.concat(args, ", "), Rojo:_getCallerName() + )) end + Rojo._apiDescriptions.Test = "Prints the given arguments to the console" function Rojo:ConnectAsync(host: string?, port: number?) app:startSession(host, port) end + Rojo._apiDescriptions.ConnectAsync = "Connects to a Rojo server" function Rojo:DisconnectAsync() app:endSession() end + Rojo._apiDescriptions.DisconnectAsync = "Disconnects from the Rojo server" function Rojo:GetSetting(setting: string): any return Settings:get(setting) end + Rojo._apiDescriptions.GetSetting = "Gets a Rojo setting" function Rojo:SetSetting(setting: string, value: any) return Settings:set(setting, value) end + Rojo._apiDescriptions.SetSetting = "Sets a Rojo setting" function Rojo:Notify(msg: string, timeout: number?) - local source = Rojo:_getCallerFull() + local source = Rojo:_getCallerSource() if Rojo._notifRateLimit[source] == nil then Rojo._notifRateLimit[source] = 0 @@ -123,23 +211,37 @@ function API.new(app) Rojo._notifRateLimit[source] -= 1 end) - app:addThirdPartyNotification(Rojo:_getCaller(), msg, timeout) + app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout) return end + Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" function Rojo:GetHostAndPort(): (string, string) return app:getHostAndPort() end + Rojo._apiDescriptions.GetHostAndPort = "Gets the host and port that Rojo is set to" function Rojo:CreateApiContext(baseUrl: string) return ApiContext.new(baseUrl) end + Rojo._apiDescriptions.CreateApiContext = "Creates a new API context" local ReadOnly = setmetatable({}, { __index = function(_, key) + -- Don't expose private members if string.find(key, "^_") then - return nil -- Don't expose private members + return nil end + + -- Permissions check + local granted = Rojo:_permissionCheck(key) + if not granted then + error(string.format( + "Attempted to read Rojo.%s, but the plugin does not have permission to do so.\nPlease first use Rojo:RequestAccess({ \"%s\" }) to gain access to this API.", + key, key + ), 2) + end + return Rojo[key] end, __newindex = function(_, key, value) diff --git a/plugin/src/Settings.lua b/plugin/src/Settings.lua index 0f23c86b0..aff0b8ca5 100644 --- a/plugin/src/Settings.lua +++ b/plugin/src/Settings.lua @@ -17,6 +17,7 @@ local defaultSettings = { typecheckingEnabled = false, logLevel = "Info", priorEndpoints = {}, + apiPermissions = {}, } local Settings = {} From e3a8152575a0d5c8d644ff12911e7c359516be35 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 30 Sep 2022 14:14:28 -0400 Subject: [PATCH 08/57] Handle plugin state prior to headless --- plugin/src/App/init.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index da11f7de6..337c2b421 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -302,12 +302,6 @@ function App:startSession(host: string?, port: string?) end) serveSession:onStatusChanged(function(status, details) - self.headlessAPI:_updateProperty("Connected", status == ServeSession.Status.Connected) - if not self.headlessAPI.Connected then - self.headlessAPI:_updateProperty("Address", nil) - self.headlessAPI:_updateProperty("ProjectName", nil) - end - if status == ServeSession.Status.Connecting then self:setPriorEndpoint(host, port) @@ -352,6 +346,12 @@ function App:startSession(host: string?, port: string?) self:addNotification("Disconnected from session.") end end + + self.headlessAPI:_updateProperty("Connected", status == ServeSession.Status.Connected) + if not self.headlessAPI.Connected then + self.headlessAPI:_updateProperty("Address", nil) + self.headlessAPI:_updateProperty("ProjectName", nil) + end end) serveSession:start() From e0c7b51dc77cf4790dc4744a9d1effaf2c5e2332 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 30 Sep 2022 15:38:36 -0400 Subject: [PATCH 09/57] Support cloud plugins that have no folder --- plugin/src/HeadlessAPI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 33168e5ba..749b0c76d 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -74,7 +74,7 @@ function API.new(app) return localPlugin end - local cloudId, cloudInstance = string.match(topLevel, "cloud_(%d-)%.(.-)%.") + local cloudId, cloudInstance = string.match(topLevel, "cloud_(%d-)%.(.-)[^%w_%-]") if cloudId then local info = cloudIdInfoCache[cloudId] if info then From ea08d288854ad0f37015e57ee63eb5470569bfd5 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 30 Sep 2022 16:22:43 -0400 Subject: [PATCH 10/57] Add assertions --- plugin/src/HeadlessAPI.lua | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 749b0c76d..36d39f873 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -122,6 +122,9 @@ function API.new(app) end function Rojo:RequestAccess(apis: {string}): {[string]: boolean} + assert(type(apis) == "table", "Rojo:RequestAccess expects an array of API names") + assert(#apis > 0, "Rojo:RequestAccess expects an array of API names") + local source, name = Rojo:_getCallerSource(), Rojo:_getCallerName() if Rojo._permissions[source] == nil then @@ -158,6 +161,7 @@ function API.new(app) return response end + Rojo._apiDescriptions.Test = "Prints the given arguments to the console" function Rojo:Test(...) local args = table.pack(...) for i=1, args.n do @@ -175,29 +179,39 @@ function API.new(app) table.concat(args, ", "), Rojo:_getCallerName() )) end - Rojo._apiDescriptions.Test = "Prints the given arguments to the console" - function Rojo:ConnectAsync(host: string?, port: number?) + Rojo._apiDescriptions.ConnectAsync = "Connects to a Rojo server" + function Rojo:ConnectAsync(host: string?, port: string?) + assert(type(host) == "string" or host == nil, "Host must be type `string?`") + assert(type(port) == "string" or port == nil, "Port must be type `string?`") + app:startSession(host, port) end - Rojo._apiDescriptions.ConnectAsync = "Connects to a Rojo server" + Rojo._apiDescriptions.DisconnectAsync = "Disconnects from the Rojo server" function Rojo:DisconnectAsync() app:endSession() end - Rojo._apiDescriptions.DisconnectAsync = "Disconnects from the Rojo server" + Rojo._apiDescriptions.GetSetting = "Gets a Rojo setting" function Rojo:GetSetting(setting: string): any + assert(type(setting) == "string", "Setting must be type `string`") + return Settings:get(setting) end - Rojo._apiDescriptions.GetSetting = "Gets a Rojo setting" + Rojo._apiDescriptions.SetSetting = "Sets a Rojo setting" function Rojo:SetSetting(setting: string, value: any) + assert(type(setting) == "string", "Setting must be type `string`") + return Settings:set(setting, value) end - Rojo._apiDescriptions.SetSetting = "Sets a Rojo setting" + Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" function Rojo:Notify(msg: string, timeout: number?) + assert(type(msg) == "string", "Message must be type `string`") + assert(type(timeout) == "number" or timeout == nil, "Timeout must be type `number?`") + local source = Rojo:_getCallerSource() if Rojo._notifRateLimit[source] == nil then @@ -214,17 +228,18 @@ function API.new(app) app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout) return end - Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" + Rojo._apiDescriptions.GetHostAndPort = "Gets the host and port that Rojo is set to" function Rojo:GetHostAndPort(): (string, string) return app:getHostAndPort() end - Rojo._apiDescriptions.GetHostAndPort = "Gets the host and port that Rojo is set to" + Rojo._apiDescriptions.CreateApiContext = "Creates a new API context" function Rojo:CreateApiContext(baseUrl: string) + assert(type(baseUrl) == "string", "Base URL must be type `string`") + return ApiContext.new(baseUrl) end - Rojo._apiDescriptions.CreateApiContext = "Creates a new API context" local ReadOnly = setmetatable({}, { __index = function(_, key) From 471215fa7baf02c9e3e19f02ed45b177bf9e80cd Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 30 Sep 2022 16:44:06 -0400 Subject: [PATCH 11/57] Add rate limiting --- plugin/src/HeadlessAPI.lua | 58 ++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 36d39f873..604e05a9d 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -22,7 +22,7 @@ local API = {} function API.new(app) local Rojo = {} - Rojo._notifRateLimit = {} + Rojo._rateLimit = {} Rojo._permissions = Settings:get("apiPermissions") or {} Rojo._changedEvent = Instance.new("BindableEvent") Rojo._apiDescriptions = {} @@ -121,10 +121,41 @@ function API.new(app) return not not Rojo._permissions[source][key] end + local BUCKET, LIMIT = 10, 15 + function Rojo:_checkRateLimit(api: string): boolean + local source = Rojo:_getCallerSource() + + if Rojo._rateLimit[source] == nil then + Rojo._rateLimit[source] = { + [api] = 0, + } + + elseif Rojo._rateLimit[source][api] == nil then + Rojo._rateLimit[source][api] = 0 + + elseif Rojo._rateLimit[source][api] >= LIMIT then + -- No more than LIMIT requests per BUCKET seconds + return true + + end + + Rojo._rateLimit[source][api] += 1 + task.delay(BUCKET, function() + Rojo._rateLimit[source][api] -= 1 + end) + + return false + end + function Rojo:RequestAccess(apis: {string}): {[string]: boolean} assert(type(apis) == "table", "Rojo:RequestAccess expects an array of API names") assert(#apis > 0, "Rojo:RequestAccess expects an array of API names") + if Rojo:_checkRateLimit("RequestAccess") then + -- Because this opens a popup, we dont want to let users get spammed by it + return {} + end + local source, name = Rojo:_getCallerSource(), Rojo:_getCallerName() if Rojo._permissions[source] == nil then @@ -185,11 +216,19 @@ function API.new(app) assert(type(host) == "string" or host == nil, "Host must be type `string?`") assert(type(port) == "string" or port == nil, "Port must be type `string?`") + if Rojo:_checkRateLimit("ConnectAsync") then + return + end + app:startSession(host, port) end Rojo._apiDescriptions.DisconnectAsync = "Disconnects from the Rojo server" function Rojo:DisconnectAsync() + if Rojo:_checkRateLimit("DisconnectAsync") then + return + end + app:endSession() end @@ -204,6 +243,10 @@ function API.new(app) function Rojo:SetSetting(setting: string, value: any) assert(type(setting) == "string", "Setting must be type `string`") + if Rojo:_checkRateLimit("SetSetting") then + return + end + return Settings:set(setting, value) end @@ -212,19 +255,10 @@ function API.new(app) assert(type(msg) == "string", "Message must be type `string`") assert(type(timeout) == "number" or timeout == nil, "Timeout must be type `number?`") - local source = Rojo:_getCallerSource() - - if Rojo._notifRateLimit[source] == nil then - Rojo._notifRateLimit[source] = 0 - elseif Rojo._notifRateLimit[source] > 45 then - return -- Rate limited + if Rojo:_checkRateLimit("Notify") then + return end - Rojo._notifRateLimit[source] += 1 - task.delay(30, function() - Rojo._notifRateLimit[source] -= 1 - end) - app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout) return end From 6a5876c2871fdfb48c9b4034717c7f75c965eeb9 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 30 Sep 2022 16:50:06 -0400 Subject: [PATCH 12/57] Include source in Test output for debugging --- plugin/src/HeadlessAPI.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 604e05a9d..6883e1632 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -206,8 +206,8 @@ function API.new(app) end print(string.format( - "Rojo:Test(%s) called from '%s'", - table.concat(args, ", "), Rojo:_getCallerName() + "Rojo:Test(%s) called from '%s' (%s)", + table.concat(args, ", "), Rojo:_getCallerName(), Rojo:_getCallerSource() )) end From 2a8cb118d594a9cc6e076d7eb486401fc7ea3b7c Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 30 Sep 2022 16:56:27 -0400 Subject: [PATCH 13/57] Clearer names and titles --- plugin/src/App/init.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 337c2b421..a9914c40c 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -65,7 +65,7 @@ function App:init() guiEnabled = false, notifications = {}, toolbarIcon = Assets.Images.PluginButton, - popups = {}, + permissionPopups = {}, }) end @@ -210,7 +210,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini local responseEvent = Instance.new("BindableEvent") self:setState(function(state) - state.popups[source] = { + state.permissionPopups[source] = { responseEvent = responseEvent, initialState = initialState, name = name, @@ -232,7 +232,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini responseEvent:Destroy() self:setState(function(state) - state.popups[source] = nil + state.permissionPopups[source] = nil return state end) @@ -399,11 +399,11 @@ function App:render() return e(Page, props) end - local popups = {} - for id, popup in self.state.popups do - popups["Rojo_"..id] = e(StudioPluginGui, { + local permissionPopups = {} + for id, popup in self.state.permissionPopups do + permissionPopups["Rojo_"..id] = e(StudioPluginGui, { id = id, - title = popup.name .. " Popup", + title = popup.name .. " Permissions", active = true, initDockState = Enum.InitialDockState.Top, @@ -441,7 +441,7 @@ function App:render() value = self.props.plugin, }, { e(Theme.StudioProvider, nil, { - popups = Roact.createFragment(popups), + permissionPopups = Roact.createFragment(permissionPopups), gui = e(StudioPluginGui, { id = pluginName, From ebdffb008818dfe53466f24d2f935599b0894e8c Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sat, 8 Oct 2022 10:25:42 -0400 Subject: [PATCH 14/57] Sanitiza requested APIs --- plugin/src/HeadlessAPI.lua | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 6883e1632..cde2ad508 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -148,8 +148,7 @@ function API.new(app) end function Rojo:RequestAccess(apis: {string}): {[string]: boolean} - assert(type(apis) == "table", "Rojo:RequestAccess expects an array of API names") - assert(#apis > 0, "Rojo:RequestAccess expects an array of API names") + assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names") if Rojo:_checkRateLimit("RequestAccess") then -- Because this opens a popup, we dont want to let users get spammed by it @@ -162,8 +161,19 @@ function API.new(app) Rojo._permissions[source] = {} end - local alreadyAllowed = true + -- Sanitize request + local sanitizedApis = {} for _, api in apis do + if Rojo[api] then + table.insert(sanitizedApis, api) + else + warn(string.format("Rojo.%s is not a valid API", api)) + end + end + assert(#sanitizedApis > 0, "Rojo:RequestAccess expects an array of valid API names") + + local alreadyAllowed = true + for _, api in sanitizedApis do if not Rojo._permissions[source][api] then alreadyAllowed = false break @@ -172,13 +182,13 @@ function API.new(app) if alreadyAllowed then local response = {} - for _, api in apis do + for _, api in sanitizedApis do response[api] = true end return response end - local response = app:requestPermission(source, name, apis, Rojo._permissions[source]) + local response = app:requestPermission(source, name, sanitizedApis, Rojo._permissions[source]) for api, granted in response do Log.warn(string.format( From 4275783ba427c0cd9490fe265a01364b2b7f49ff Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 4 Jan 2023 17:28:06 -0500 Subject: [PATCH 15/57] Add permissions page --- .../src/App/StatusPages/Permissions/init.lua | 95 +++++++++++++++++++ .../src/App/StatusPages/Settings/Setting.lua | 23 +++-- plugin/src/App/StatusPages/Settings/init.lua | 51 +++++++++- plugin/src/App/StatusPages/init.lua | 3 +- plugin/src/App/init.lua | 15 +++ plugin/src/Assets.lua | 1 + 6 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 plugin/src/App/StatusPages/Permissions/init.lua diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua new file mode 100644 index 000000000..f88010aa6 --- /dev/null +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -0,0 +1,95 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) +local Log = require(Packages.Log) + +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) + +local IconButton = require(Plugin.App.Components.IconButton) +local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) +local Tooltip = require(Plugin.App.Components.Tooltip) + +local e = Roact.createElement + +local function Navbar(props) + return Theme.with(function(theme) + theme = theme.Settings.Navbar + + return e("Frame", { + Size = UDim2.new(1, 0, 0, 46), + LayoutOrder = props.layoutOrder, + BackgroundTransparency = 1, + }, { + Back = e(IconButton, { + icon = Assets.Images.Icons.Back, + iconSize = 24, + color = theme.BackButtonColor, + transparency = props.transparency, + + position = UDim2.new(0, 0, 0.5, 0), + anchorPoint = Vector2.new(0, 0.5), + + onClick = props.onBack, + }, { + Tip = e(Tooltip.Trigger, { + text = "Back" + }), + }), + + Text = e("TextLabel", { + Text = "Permissions", + Font = Enum.Font.Gotham, + TextSize = 18, + TextColor3 = theme.TextColor, + TextTransparency = props.transparency, + + Size = UDim2.new(1, 0, 1, 0), + + BackgroundTransparency = 1, + }) + }) + end) +end + +local PermissionsPage = Roact.Component:extend("PermissionsPage") + +function PermissionsPage:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function PermissionsPage:render() + return Theme.with(function(theme) + theme = theme.Settings + + return e(ScrollingFrame, { + size = UDim2.new(1, 0, 1, 0), + contentSize = self.contentSize, + transparency = self.props.transparency, + }, { + Navbar = e(Navbar, { + onBack = self.props.onBack, + transparency = self.props.transparency, + layoutOrder = 0, + }), + + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), + + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + }), + }) + end) +end + +return PermissionsPage diff --git a/plugin/src/App/StatusPages/Settings/Setting.lua b/plugin/src/App/StatusPages/Settings/Setting.lua index 8bf688f6b..d294264df 100644 --- a/plugin/src/App/StatusPages/Settings/Setting.lua +++ b/plugin/src/App/StatusPages/Settings/Setting.lua @@ -33,19 +33,22 @@ function Setting:init() self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) - self:setState({ - setting = Settings:get(self.props.id), - }) - - self.changedCleanup = Settings:onChanged(self.props.id, function(value) + if self.props.id then self:setState({ - setting = value, + setting = Settings:get(self.props.id), }) - end) + self.changedCleanup = Settings:onChanged(self.props.id, function(value) + self:setState({ + setting = value, + }) + end) + end end function Setting:willUnmount() - self.changedCleanup() + if self.changedCleanup then + self.changedCleanup() + end end function Setting:render() @@ -64,7 +67,9 @@ function Setting:render() self.setContainerSize(object.AbsoluteSize) end, }, { - Input = if self.props.options ~= nil then + Input = if self.props.customInput then + self.props.customInput + elseif self.props.options ~= nil then e(Dropdown, { options = self.props.options, active = self.state.setting, diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index 1d484423b..60ff7d87a 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -12,6 +12,7 @@ local Theme = require(Plugin.App.Theme) local IconButton = require(Plugin.App.Components.IconButton) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) local Tooltip = require(Plugin.App.Components.Tooltip) +local SlicedImage = require(Plugin.App.Components.SlicedImage) local Setting = require(script.Setting) local e = Roact.createElement @@ -95,12 +96,52 @@ function SettingsPage:render() layoutOrder = 1, }), + Permissions = e(Setting, { + name = "Third Party Permissions", + description = "Manage permissions for third party plugins", + transparency = self.props.transparency, + layoutOrder = 2, + customInput = e("TextButton", { + Text = "", + BackgroundTransparency = 1, + Size = UDim2.fromOffset(28, 28), + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + + [Roact.Event.Activated] = function() + self.props.onNavigatePermissions() + end, + }, { + Button = Theme.with(function(theme) + theme = theme.Checkbox + return e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = theme.Inactive.BorderColor, + transparency = self.props.transparency, + size = UDim2.new(1, 0, 1, 0), + }, { + Icon = e("ImageLabel", { + Image = Assets.Images.Icons.Expand, + ImageColor3 = theme.Inactive.IconColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }) + end), + }), + }), + ShowNotifications = e(Setting, { id = "showNotifications", name = "Show Notifications", description = "Popup notifications in viewport", transparency = self.props.transparency, - layoutOrder = 2, + layoutOrder = 3, }), PlaySounds = e(Setting, { @@ -108,7 +149,7 @@ function SettingsPage:render() name = "Play Sounds", description = "Toggle sound effects", transparency = self.props.transparency, - layoutOrder = 3, + layoutOrder = 4, }), TwoWaySync = e(Setting, { @@ -116,7 +157,7 @@ function SettingsPage:render() name = "Two-Way Sync", description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem", transparency = self.props.transparency, - layoutOrder = 4, + layoutOrder = 5, }), LogLevel = e(Setting, { @@ -124,7 +165,7 @@ function SettingsPage:render() name = "Log Level", description = "Plugin output verbosity level", transparency = self.props.transparency, - layoutOrder = 5, + layoutOrder = 6, options = invertedLevels, showReset = Settings:getBinding("logLevel"):map(function(value) @@ -140,7 +181,7 @@ function SettingsPage:render() name = "Typechecking", description = "Toggle typechecking on the API surface", transparency = self.props.transparency, - layoutOrder = 6, + layoutOrder = 7, }), Layout = e("UIListLayout", { diff --git a/plugin/src/App/StatusPages/init.lua b/plugin/src/App/StatusPages/init.lua index 03d64e453..cdcac6f11 100644 --- a/plugin/src/App/StatusPages/init.lua +++ b/plugin/src/App/StatusPages/init.lua @@ -1,7 +1,8 @@ return { NotConnected = require(script.NotConnected), Settings = require(script.Settings), + Permissions = require(script.Permissions), Connecting = require(script.Connecting), Connected = require(script.Connected), Error = require(script.Error), -} \ No newline at end of file +} diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 35cd041ad..15dd98ed6 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -35,6 +35,7 @@ local StatusPages = require(script.StatusPages) local AppStatus = strict("AppStatus", { NotConnected = "NotConnected", Settings = "Settings", + Permissions = "Permissions", Connecting = "Connecting", Connected = "Connected", Error = "Error", @@ -509,6 +510,20 @@ function App:render() appStatus = AppStatus.NotConnected, }) end, + + onNavigatePermissions = function() + self:setState({ + appStatus = AppStatus.Permissions, + }) + end, + }), + + Permissions = createPageElement(AppStatus.Permissions, { + onBack = function() + self:setState({ + appStatus = AppStatus.Settings, + }) + end, }), Error = createPageElement(AppStatus.Error, { diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index a5484aa5c..78ab84fed 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -25,6 +25,7 @@ local Assets = { Close = "rbxassetid://6012985953", Back = "rbxassetid://6017213752", Reset = "rbxassetid://10142422327", + Expand = "rbxassetid://12045401097", }, Checkbox = { Active = "rbxassetid://6016251644", From 7c1afd5aa9367e0b0a7f4fda205d1aa73b15d5c5 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 4 Jan 2023 18:20:57 -0500 Subject: [PATCH 16/57] Add permission management listings --- .../App/StatusPages/Permissions/Listing.lua | 164 ++++++++++++++++++ .../src/App/StatusPages/Permissions/init.lua | 39 +++++ plugin/src/App/init.lua | 15 ++ plugin/src/Assets.lua | 1 + plugin/src/HeadlessAPI.lua | 37 ++++ 5 files changed, 256 insertions(+) create mode 100644 plugin/src/App/StatusPages/Permissions/Listing.lua diff --git a/plugin/src/App/StatusPages/Permissions/Listing.lua b/plugin/src/App/StatusPages/Permissions/Listing.lua new file mode 100644 index 000000000..4f2e10725 --- /dev/null +++ b/plugin/src/App/StatusPages/Permissions/Listing.lua @@ -0,0 +1,164 @@ +local TextService = game:GetService("TextService") + +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Settings = require(Plugin.Settings) +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) + +local Checkbox = require(Plugin.App.Components.Checkbox) +local Dropdown = require(Plugin.App.Components.Dropdown) +local IconButton = require(Plugin.App.Components.IconButton) +local SlicedImage = require(Plugin.App.Components.SlicedImage) + +local e = Roact.createElement + +local DIVIDER_FADE_SIZE = 0.1 + +local function getTextBounds(text, textSize, font, lineHeight, bounds) + local textBounds = TextService:GetTextSize(text, textSize, font, bounds) + + local lineCount = textBounds.Y / textSize + local lineHeightAbsolute = textSize * lineHeight + + return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize)) +end + +local Listing = Roact.Component:extend("Listing") + +function Listing:init() + self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) +end + +function Listing:render() + return Theme.with(function(theme) + theme = theme.Settings + + return e("Frame", { + Size = self.contentSize:map(function(value) + return UDim2.new(1, 0, 0, 20 + value.Y + 20) + end), + LayoutOrder = self.props.layoutOrder, + ZIndex = -self.props.layoutOrder, + BackgroundTransparency = 1, + + [Roact.Change.AbsoluteSize] = function(object) + self.setContainerSize(object.AbsoluteSize) + end, + }, { + Settings = e("TextButton", { + Text = "", + BackgroundTransparency = 1, + Size = UDim2.fromOffset(28, 28), + Position = UDim2.fromScale(1, 0.5), + AnchorPoint = Vector2.new(1, 0.5), + + [Roact.Event.Activated] = function() + self.props.onClick() + end, + }, { + Button = Theme.with(function(theme) + theme = theme.Checkbox + return e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = theme.Inactive.BorderColor, + transparency = self.props.transparency, + size = UDim2.new(1, 0, 1, 0), + }, { + Icon = e("ImageLabel", { + Image = Assets.Images.Icons.Settings, + ImageColor3 = theme.Inactive.IconColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }) + end), + }), + + Text = e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, + }, { + Name = e("TextLabel", { + Text = self.props.name, + Font = Enum.Font.GothamBold, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + + Size = UDim2.new(1, 0, 0, 17), + + LayoutOrder = 1, + BackgroundTransparency = 1, + }), + + Description = e("TextLabel", { + Text = self.props.description, + Font = Enum.Font.Gotham, + LineHeight = 1.2, + TextSize = 14, + TextColor3 = theme.Setting.DescriptionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + TextWrapped = true, + + Size = self.containerSize:map(function(value) + local textBounds = getTextBounds( + self.props.description, 14, Enum.Font.Gotham, 1.2, + Vector2.new(value.X - 40, math.huge) + ) + return UDim2.new(1, - 40, 0, textBounds.Y) + end), + + LayoutOrder = 2, + BackgroundTransparency = 1, + }), + + Layout = e("UIListLayout", { + VerticalAlignment = Enum.VerticalAlignment.Center, + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 6), + + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), + + Padding = e("UIPadding", { + PaddingTop = UDim.new(0, 20), + PaddingBottom = UDim.new(0, 20), + }), + }), + + Divider = e("Frame", { + BackgroundColor3 = theme.DividerColor, + BackgroundTransparency = self.props.transparency, + Size = UDim2.new(1, 0, 0, 1), + BorderSizePixel = 0, + }, { + Gradient = e("UIGradient", { + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1), + NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1, 1), + }), + }), + }), + }) + end) +end + +return Listing diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index f88010aa6..30a0156f3 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -11,6 +11,7 @@ local Theme = require(Plugin.App.Theme) local IconButton = require(Plugin.App.Components.IconButton) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) local Tooltip = require(Plugin.App.Components.Tooltip) +local Listing = require(script.Listing) local e = Roact.createElement @@ -64,6 +65,42 @@ function PermissionsPage:render() return Theme.with(function(theme) theme = theme.Settings + local sources = {} + if next(self.props.headlessAPI._permissions) == nil then + sources.noSources = e("TextLabel", { + Text = "No third-party plugins have been granted permissions.", + Font = Enum.Font.Gotham, + TextSize = 18, + TextColor3 = theme.Setting.DescriptionColor, + TextTransparency = self.props.transparency, + TextWrapped = true, + + Size = UDim2.new(1, 0, 0, 48), + LayoutOrder = 0, + + BackgroundTransparency = 1, + }) + else + for source in self.props.headlessAPI._permissions do + local meta = self.props.headlessAPI:_getMetaFromSource(source) + sources[source] = e(Listing, { + layoutOrder = string.byte(source), + transparency = self.props.transparency, + + name = meta.Name, + description = string.format( + "%s plugin%s", + meta.Type, + if meta.Creator then " by " .. meta.Creator else "" + ), + + onClick = function() + self.props.onEdit(source, meta, self.props.headlessAPI._permissions[source] or {}) + end, + }) + end + end + return e(ScrollingFrame, { size = UDim2.new(1, 0, 1, 0), contentSize = self.contentSize, @@ -84,6 +121,8 @@ function PermissionsPage:render() end, }), + Sources = Roact.createFragment(sources), + Padding = e("UIPadding", { PaddingLeft = UDim.new(0, 20), PaddingRight = UDim.new(0, 20), diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 15dd98ed6..235e365d6 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -519,11 +519,26 @@ function App:render() }), Permissions = createPageElement(AppStatus.Permissions, { + headlessAPI = self.headlessAPI, + onBack = function() self:setState({ appStatus = AppStatus.Settings, }) end, + + onEdit = function(source, meta, apiMap) + local apiList = {} + for api in apiMap do + table.insert(apiList, api) + end + self:requestPermission( + source, + meta.Name .. if meta.Creator then " by " .. meta.Creator else "", + apiList, + apiMap + ) + end, }), Error = createPageElement(AppStatus.Error, { diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 78ab84fed..a219ec785 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -26,6 +26,7 @@ local Assets = { Back = "rbxassetid://6017213752", Reset = "rbxassetid://10142422327", Expand = "rbxassetid://12045401097", + Settings = "rbxassetid://12046309515", }, Checkbox = { Active = "rbxassetid://6016251644", diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index cde2ad508..0268ab9e8 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -95,6 +95,43 @@ function API.new(app) return "Command Bar" end + function Rojo:_getMetaFromSource(source) + local localPlugin = string.match(source, "user_(.+)") + if localPlugin then + return { + Type = "Local", + Name = localPlugin, + } + end + + local cloudId = string.match(source, "cloud_(%d+)") + if cloudId then + local info = cloudIdInfoCache[cloudId] + if info then + return { + Type = "Cloud", + Name = info.Name, + Creator = info.Creator.Name, + } + else + local success, newInfo = pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) + if success then + cloudIdInfoCache[cloudId] = newInfo + return { + Type = "Cloud", + Name = newInfo.Name, + Creator = newInfo.Creator.Name, + } + end + end + end + + return { + Type = "Studio", + Name = "Command Bar", + } + end + function Rojo:_getCallerType() local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1] From de33cf0e55904a0ad343b61befb12c5a76cea814 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 4 Jan 2023 18:51:55 -0500 Subject: [PATCH 17/57] Genericize popup state --- plugin/src/App/init.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 235e365d6..846dcf900 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -67,7 +67,7 @@ function App:init() guiEnabled = false, notifications = {}, toolbarIcon = Assets.Images.PluginButton, - permissionPopups = {}, + popups = {}, }) end @@ -212,7 +212,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini local responseEvent = Instance.new("BindableEvent") self:setState(function(state) - state.permissionPopups[source] = { + state.popups[source] = { responseEvent = responseEvent, initialState = initialState, name = name, @@ -234,7 +234,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini responseEvent:Destroy() self:setState(function(state) - state.permissionPopups[source] = nil + state.popups[source] = nil return state end) @@ -403,9 +403,9 @@ function App:render() return e(Page, props) end - local permissionPopups = {} - for id, popup in self.state.permissionPopups do - permissionPopups["Rojo_"..id] = e(StudioPluginGui, { + local popups = {} + for id, popup in self.state.popups do + popups["Rojo_"..id] = e(StudioPluginGui, { id = id, title = popup.name .. " Permissions", active = true, @@ -423,7 +423,7 @@ function App:render() popup.responseEvent:Destroy() self:setState(function(state) - state[id] = nil + state.popups[id] = nil return state end) end, @@ -446,7 +446,7 @@ function App:render() }, { e(Theme.StudioProvider, nil, { e(Tooltip.Provider, nil, { - permissionPopups = Roact.createFragment(permissionPopups), + popups = Roact.createFragment(popups), gui = e(StudioPluginGui, { id = pluginName, From d158ccb2eb7c829bfafd331d073dfef11bff90b5 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 4 Jan 2023 18:59:53 -0500 Subject: [PATCH 18/57] Simplify popup handling --- plugin/src/App/init.lua | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 846dcf900..3d4c0c161 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -213,10 +213,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini self:setState(function(state) state.popups[source] = { - responseEvent = responseEvent, - initialState = initialState, name = name, - apis = apis, content = e(PermissionPopup, { responseEvent = responseEvent, initialState = initialState, @@ -226,6 +223,10 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini apiDescriptions = self.headlessAPI._apiDescriptions, transparency = Roact.createBinding(0), }), + onClose = function() + responseEvent:Fire(initialState) + responseEvent:Destroy() + end, } return state end) @@ -418,15 +419,7 @@ function App:render() zIndexBehavior = Enum.ZIndexBehavior.Sibling, - onClose = function() - popup.responseEvent:Fire(popup.initialState) - popup.responseEvent:Destroy() - - self:setState(function(state) - state.popups[id] = nil - return state - end) - end, + onClose = popup.onClose, }, { Content = popup.content, From 6fddcc5ceb289112726dee138d7c7d677a7010fb Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 4 Jan 2023 19:13:31 -0500 Subject: [PATCH 19/57] Generic popup titles --- plugin/src/App/init.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 3d4c0c161..edcf2b072 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -212,7 +212,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini local responseEvent = Instance.new("BindableEvent") self:setState(function(state) - state.popups[source] = { + state.popups[source .. " Permissions"] = { name = name, content = e(PermissionPopup, { responseEvent = responseEvent, @@ -225,7 +225,6 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini }), onClose = function() responseEvent:Fire(initialState) - responseEvent:Destroy() end, } return state @@ -235,7 +234,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini responseEvent:Destroy() self:setState(function(state) - state.popups[source] = nil + state.popups[source .. " Permissions"] = nil return state end) @@ -408,7 +407,7 @@ function App:render() for id, popup in self.state.popups do popups["Rojo_"..id] = e(StudioPluginGui, { id = id, - title = popup.name .. " Permissions", + title = popup.name, active = true, initDockState = Enum.InitialDockState.Top, From cf5e13f5a08eaee4a77bd17731ceaa830dfe266b Mon Sep 17 00:00:00 2001 From: boatbomber Date: Wed, 4 Jan 2023 19:37:37 -0500 Subject: [PATCH 20/57] Add api conflict popup --- plugin/src/App/ConflictAPIPopup.lua | 121 ++++++++++++++++++++++++++++ plugin/src/App/init.lua | 52 ++++++++++-- 2 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 plugin/src/App/ConflictAPIPopup.lua diff --git a/plugin/src/App/ConflictAPIPopup.lua b/plugin/src/App/ConflictAPIPopup.lua new file mode 100644 index 000000000..86f93d0b4 --- /dev/null +++ b/plugin/src/App/ConflictAPIPopup.lua @@ -0,0 +1,121 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Config = require(Plugin.Config) +local Version = require(Plugin.Version) +local Theme = require(Plugin.App.Theme) + +local TextButton = require(Plugin.App.Components.TextButton) + +local e = Roact.createElement + +local ConflictAPIPopup = Roact.Component:extend("ConflictAPIPopup") + +function ConflictAPIPopup:render() + return Theme.with(function(theme) + theme = theme.Settings + + return e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }, { + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + PaddingTop = UDim.new(0, 15), + PaddingBottom = UDim.new(0, 15), + }), + + Details = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 1, 0), + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 15), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + }), + + Info = e("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Text = "There is already a Rojo API exposed by a Rojo plugin. Do you want to overwrite it with this one?", + Font = Enum.Font.GothamMedium, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + TextTransparency = self.props.transparency, + LayoutOrder = 1, + }), + + Existing = e("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Text = string.format("Existing: Version %s, Protocol %d", Version.display(self.props.existingAPI.Version), self.props.existingAPI.ProtocolVersion), + Font = Enum.Font.Gotham, + TextSize = 15, + TextColor3 = theme.Setting.DescriptionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + LayoutOrder = 2, + }), + + Incoming = e("TextLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + Text = string.format("Incoming: Version %s, Protocol %d", Version.display(Config.version), Config.protocolVersion), + Font = Enum.Font.Gotham, + TextSize = 15, + TextColor3 = theme.Setting.DescriptionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + LayoutOrder = 3, + }), + }), + + Actions = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 34), + Position = UDim2.fromScale(0, 1), + AnchorPoint = Vector2.new(0, 1), + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + HorizontalAlignment = Enum.HorizontalAlignment.Right, + }), + + Keep = e(TextButton, { + text = "Keep", + style = "Bordered", + transparency = self.props.transparency, + layoutOrder = 1, + onClick = function() + self.props.onDeny() + end, + }), + + Overwrite = e(TextButton, { + text = "Overwrite", + style = "Solid", + transparency = self.props.transparency, + layoutOrder = 2, + onClick = function() + self.props.onAccept() + end, + }), + }), + }) + end) +end + +return ConflictAPIPopup diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index edcf2b072..97010ac18 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -24,6 +24,7 @@ local Theme = require(script.Theme) local Page = require(script.Page) local Notifications = require(script.Notifications) local PermissionPopup = require(script.PermissionPopup) +local ConflictAPIPopup = require(script.ConflictAPIPopup) local Tooltip = require(script.Components.Tooltip) local StudioPluginAction = require(script.Components.Studio.StudioPluginAction) local StudioToolbar = require(script.Components.Studio.StudioToolbar) @@ -52,11 +53,6 @@ function App:init() self.host, self.setHost = Roact.createBinding(priorHost or "") self.port, self.setPort = Roact.createBinding(priorPort or "") - self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self) - - -- selene: allow(global_usage) - _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar - self.patchInfo, self.setPatchInfo = Roact.createBinding({ changes = 0, timestamp = os.time(), @@ -69,6 +65,50 @@ function App:init() toolbarIcon = Assets.Images.PluginButton, popups = {}, }) + + self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self) + + -- selene: allow(global_usage) + local existingAPI = _G.Rojo + if existingAPI then + local responseEvent = Instance.new("BindableEvent") + responseEvent.Event:Once(function(accepted) + if accepted then + -- selene: allow(global_usage) + _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar + end + + responseEvent:Destroy() + self:setState(function(state) + state.popups["apiReplacement"] = nil + return state + end) + end) + + self:setState(function(state) + state.popups["apiReplacement"] = { + name = "Headless API Conflict", + dockState = Enum.InitialDockState.Float, + onClose = function() + responseEvent:Fire(false) + end, + content = e(ConflictAPIPopup, { + existingAPI = existingAPI, + onAccept = function() + responseEvent:Fire(true) + end, + onDeny = function() + responseEvent:Fire(false) + end, + transparency = Roact.createBinding(0), + }), + } + return state + end) + else + -- selene: allow(global_usage) + _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar + end end function App:addNotification(text: string, timeout: number?) @@ -410,7 +450,7 @@ function App:render() title = popup.name, active = true, - initDockState = Enum.InitialDockState.Top, + initDockState = popup.dockState or Enum.InitialDockState.Top, initEnabled = true, overridePreviousState = true, floatingSize = Vector2.new(400, 300), From 3b1aca2958508e0fd0bda7c21fcbcc7a03d934fc Mon Sep 17 00:00:00 2001 From: boatbomber Date: Thu, 5 Jan 2023 10:58:26 -0500 Subject: [PATCH 21/57] Clearer button UX --- plugin/src/App/StatusPages/Permissions/Listing.lua | 2 +- plugin/src/App/StatusPages/Settings/init.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/src/App/StatusPages/Permissions/Listing.lua b/plugin/src/App/StatusPages/Permissions/Listing.lua index 4f2e10725..717f1cefb 100644 --- a/plugin/src/App/StatusPages/Permissions/Listing.lua +++ b/plugin/src/App/StatusPages/Permissions/Listing.lua @@ -72,7 +72,7 @@ function Listing:render() }, { Icon = e("ImageLabel", { Image = Assets.Images.Icons.Settings, - ImageColor3 = theme.Inactive.IconColor, + ImageColor3 = theme.Active.IconColor, ImageTransparency = self.props.transparency, Size = UDim2.new(0, 16, 0, 16), diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index 60ff7d87a..f39aba0e2 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -115,14 +115,14 @@ function SettingsPage:render() Button = Theme.with(function(theme) theme = theme.Checkbox return e(SlicedImage, { - slice = Assets.Slices.RoundedBorder, - color = theme.Inactive.BorderColor, + slice = Assets.Slices.RoundedBackground, + color = theme.Active.BackgroundColor, transparency = self.props.transparency, size = UDim2.new(1, 0, 1, 0), }, { Icon = e("ImageLabel", { Image = Assets.Images.Icons.Expand, - ImageColor3 = theme.Inactive.IconColor, + ImageColor3 = theme.Active.IconColor, ImageTransparency = self.props.transparency, Size = UDim2.new(0, 16, 0, 16), From e24657ca4d7e315ea2280c8b06a037e9acf016a8 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Thu, 5 Jan 2023 11:07:03 -0500 Subject: [PATCH 22/57] Better light theme support --- .../App/StatusPages/Permissions/Listing.lua | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/plugin/src/App/StatusPages/Permissions/Listing.lua b/plugin/src/App/StatusPages/Permissions/Listing.lua index 717f1cefb..780b1b91a 100644 --- a/plugin/src/App/StatusPages/Permissions/Listing.lua +++ b/plugin/src/App/StatusPages/Permissions/Listing.lua @@ -37,8 +37,6 @@ end function Listing:render() return Theme.with(function(theme) - theme = theme.Settings - return e("Frame", { Size = self.contentSize:map(function(value) return UDim2.new(1, 0, 0, 20 + value.Y + 20) @@ -62,27 +60,24 @@ function Listing:render() self.props.onClick() end, }, { - Button = Theme.with(function(theme) - theme = theme.Checkbox - return e(SlicedImage, { - slice = Assets.Slices.RoundedBorder, - color = theme.Inactive.BorderColor, - transparency = self.props.transparency, - size = UDim2.new(1, 0, 1, 0), - }, { - Icon = e("ImageLabel", { - Image = Assets.Images.Icons.Settings, - ImageColor3 = theme.Active.IconColor, - ImageTransparency = self.props.transparency, - - Size = UDim2.new(0, 16, 0, 16), - Position = UDim2.new(0.5, 0, 0.5, 0), - AnchorPoint = Vector2.new(0.5, 0.5), - - BackgroundTransparency = 1, - }), - }) - end), + Button = e(SlicedImage, { + slice = Assets.Slices.RoundedBorder, + color = theme.Checkbox.Inactive.BorderColor, + transparency = self.props.transparency, + size = UDim2.new(1, 0, 1, 0), + }, { + Icon = e("ImageLabel", { + Image = Assets.Images.Icons.Settings, + ImageColor3 = theme.Notification.InfoColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }), }), Text = e("Frame", { @@ -93,7 +88,7 @@ function Listing:render() Text = self.props.name, Font = Enum.Font.GothamBold, TextSize = 17, - TextColor3 = theme.Setting.NameColor, + TextColor3 = theme.Settings.Setting.NameColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = self.props.transparency, @@ -108,7 +103,7 @@ function Listing:render() Font = Enum.Font.Gotham, LineHeight = 1.2, TextSize = 14, - TextColor3 = theme.Setting.DescriptionColor, + TextColor3 = theme.Settings.Setting.DescriptionColor, TextXAlignment = Enum.TextXAlignment.Left, TextTransparency = self.props.transparency, TextWrapped = true, @@ -143,7 +138,7 @@ function Listing:render() }), Divider = e("Frame", { - BackgroundColor3 = theme.DividerColor, + BackgroundColor3 = theme.Settings.DividerColor, BackgroundTransparency = self.props.transparency, Size = UDim2.new(1, 0, 0, 1), BorderSizePixel = 0, From 9cf195691c125a6bf36ee55297c83a265a372e5e Mon Sep 17 00:00:00 2001 From: boatbomber Date: Thu, 5 Jan 2023 11:11:36 -0500 Subject: [PATCH 23/57] Don't scroll the navbar --- .../src/App/StatusPages/Permissions/init.lua | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index 30a0156f3..140e8ac48 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -24,6 +24,11 @@ local function Navbar(props) LayoutOrder = props.layoutOrder, BackgroundTransparency = 1, }, { + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + }), + Back = e(IconButton, { icon = Assets.Images.Icons.Back, iconSize = 24, @@ -101,31 +106,36 @@ function PermissionsPage:render() end end - return e(ScrollingFrame, { - size = UDim2.new(1, 0, 1, 0), - contentSize = self.contentSize, - transparency = self.props.transparency, + return e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + BackgroundTransparency = 1, }, { Navbar = e(Navbar, { onBack = self.props.onBack, transparency = self.props.transparency, - layoutOrder = 0, }), - Layout = e("UIListLayout", { - FillDirection = Enum.FillDirection.Vertical, - SortOrder = Enum.SortOrder.LayoutOrder, + PluginSources = e(ScrollingFrame, { + size = UDim2.new(1, 0, 1, -47), + position = UDim2.new(0, 0, 0, 47), + contentSize = self.contentSize, + transparency = self.props.transparency, + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, - [Roact.Change.AbsoluteContentSize] = function(object) - self.setContentSize(object.AbsoluteContentSize) - end, - }), + [Roact.Change.AbsoluteContentSize] = function(object) + self.setContentSize(object.AbsoluteContentSize) + end, + }), - Sources = Roact.createFragment(sources), + Padding = e("UIPadding", { + PaddingLeft = UDim.new(0, 20), + PaddingRight = UDim.new(0, 20), + }), - Padding = e("UIPadding", { - PaddingLeft = UDim.new(0, 20), - PaddingRight = UDim.new(0, 20), + Sources = Roact.createFragment(sources), }), }) end) From d83543e3cc6929fa2f92c53efdae54e43595fe88 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Thu, 5 Jan 2023 11:28:37 -0500 Subject: [PATCH 24/57] Handle falsy API values --- plugin/src/HeadlessAPI.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 0268ab9e8..db3c552c9 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -201,10 +201,10 @@ function API.new(app) -- Sanitize request local sanitizedApis = {} for _, api in apis do - if Rojo[api] then + if Rojo[api] ~= nil then table.insert(sanitizedApis, api) else - warn(string.format("Rojo.%s is not a valid API", api)) + warn(string.format("Rojo.%s is not a valid API", tostring(api))) end end assert(#sanitizedApis > 0, "Rojo:RequestAccess expects an array of valid API names") @@ -329,6 +329,12 @@ function API.new(app) return nil end + -- Existence check + if Rojo[key] == nil then + warn(string.format("Rojo.%s is not a valid API", tostring(key))) + return nil + end + -- Permissions check local granted = Rojo:_permissionCheck(key) if not granted then From 5cf9a7de42720a671a5b3d24203141b298ac3aba Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 9 Jan 2023 11:24:51 -0500 Subject: [PATCH 25/57] Handle nil APIs --- plugin/src/HeadlessAPI.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index db3c552c9..bb339a7c3 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -40,7 +40,10 @@ function API.new(app) Rojo._apiDescriptions.ProjectName = "The name of the project that the plugin is connected to" Rojo.Version = table.clone(Config.version) + Rojo._apiDescriptions.Version = "The version of the plugin" + Rojo.ProtocolVersion = Config.protocolVersion + Rojo._apiDescriptions.ProtocolVersion = "The protocol version that the plugin is using" function Rojo:_updateProperty(property: string, value: any?) local oldValue = Rojo[property] @@ -201,7 +204,7 @@ function API.new(app) -- Sanitize request local sanitizedApis = {} for _, api in apis do - if Rojo[api] ~= nil then + if Rojo._apiDescriptions[api] ~= nil then table.insert(sanitizedApis, api) else warn(string.format("Rojo.%s is not a valid API", tostring(api))) @@ -330,7 +333,7 @@ function API.new(app) end -- Existence check - if Rojo[key] == nil then + if Rojo._apiDescriptions[key] == nil then warn(string.format("Rojo.%s is not a valid API", tostring(key))) return nil end From 8a8ee80043321fd22d3fa0d4c11352f1e06583cb Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 9 Jan 2023 11:39:11 -0500 Subject: [PATCH 26/57] Move api descs to top --- plugin/src/HeadlessAPI.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index bb339a7c3..e53f07dde 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -27,23 +27,23 @@ function API.new(app) Rojo._changedEvent = Instance.new("BindableEvent") Rojo._apiDescriptions = {} - Rojo.Changed = Rojo._changedEvent.Event Rojo._apiDescriptions.Changed = "An event that fires when a headless API property changes" + Rojo.Changed = Rojo._changedEvent.Event - Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false Rojo._apiDescriptions.Connected = "Whether or not the plugin is connected to a Rojo server" + Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false - Rojo.Address = nil Rojo._apiDescriptions.Address = "The address (host:port) that the plugin is connected to" + Rojo.Address = nil - Rojo.ProjectName = nil Rojo._apiDescriptions.ProjectName = "The name of the project that the plugin is connected to" + Rojo.ProjectName = nil - Rojo.Version = table.clone(Config.version) Rojo._apiDescriptions.Version = "The version of the plugin" + Rojo.Version = table.clone(Config.version) - Rojo.ProtocolVersion = Config.protocolVersion Rojo._apiDescriptions.ProtocolVersion = "The protocol version that the plugin is using" + Rojo.ProtocolVersion = Config.protocolVersion function Rojo:_updateProperty(property: string, value: any?) local oldValue = Rojo[property] @@ -187,6 +187,7 @@ function API.new(app) return false end + Rojo._apiDescriptions.RequestAccess = "Used to gain access to Rojo API members" function Rojo:RequestAccess(apis: {string}): {[string]: boolean} assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names") From b68b89bf78204b76e6bcfde0904f7fe487298103 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 4 Jun 2023 10:36:33 -0700 Subject: [PATCH 27/57] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a7b948a..c4339ae6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Rojo Changelog ## Unreleased Changes +* Added headless API for Studio companion plugins. ([#631]) * Significantly improved performance of `rojo sourcemap`. ([#668]) +[#631]: https://github.com/rojo-rbx/rojo/pull/631 [#668]: https://github.com/rojo-rbx/rojo/pull/668 ## [7.3.0] - April 22, 2023 From a795e23c3c938a7ee1a9272840528367b671e029 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 13:46:31 -0700 Subject: [PATCH 28/57] Fix typo in log --- plugin/src/App/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 48cb2b9bd..4e5e939d1 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -311,7 +311,7 @@ function App:releaseSyncLock() return end - Log.trace("Could not relase sync lock because it is owned by {}", lock.Value) + Log.trace("Could not release sync lock because it is owned by {}", lock.Value) end function App:requestPermission(source: string, name: string, apis: {string}, initialState: {[string]: boolean?}): {[string]: boolean} From 7d1ee6eb535260705ccee349001b2b8c8ab74dc3 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 13:53:49 -0700 Subject: [PATCH 29/57] Support actions on third party notifs --- plugin/src/HeadlessAPI.lua | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index e53f07dde..a23270e82 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -302,15 +302,37 @@ function API.new(app) end Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" - function Rojo:Notify(msg: string, timeout: number?) + function Rojo:Notify(msg: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?) assert(type(msg) == "string", "Message must be type `string`") assert(type(timeout) == "number" or timeout == nil, "Timeout must be type `number?`") + assert((actions == nil) or (type(actions) == "table"), "Actions must be table or nil") if Rojo:_checkRateLimit("Notify") then return end - app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout) + local sanitizedActions = nil + if actions then + sanitizedActions = {} + for id, action in actions do + assert(type(id) == "string", "Action key must be string") + assert(type(action) == "table", "Action must be table") + assert(type(action.text) == "string", "Action.text must be string") + assert(type(action.style) == "string", "Action.style must be string") + assert(action.style == "Solid" or action.style == "Bordered", "Action.style must be 'Solid' or 'Bordered'") + assert(type(action.layoutOrder) == "number", "Action.layoutOrder must be number") + assert(type(action.onClick) == "function", "Action.onClick must be function") + + sanitizedActions[id] = { + text = action.text, + style = action.style, + layoutOrder = action.layoutOrder, + onClick = action.onClick, + } + end + end + + app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout, sanitizedActions) return end From 2f78d943702e4fb75ef3c2cafbae0e3cadc8ef13 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 13:56:48 -0700 Subject: [PATCH 30/57] Undo old notif changes --- plugin/src/App/Notifications.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index 04240f8b1..cb521331b 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -176,7 +176,7 @@ function Notification:render() TextWrapped = true, Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), - Position = UDim2.fromOffset(38, 0), + Position = UDim2.fromOffset(35, 0), LayoutOrder = 1, BackgroundTransparency = 1, @@ -199,8 +199,8 @@ function Notification:render() }), Padding = e("UIPadding", { - PaddingLeft = UDim.new(0, 12), - PaddingRight = UDim.new(0, 12), + PaddingLeft = UDim.new(0, 17), + PaddingRight = UDim.new(0, 15), }), }) }) From dd30b1686b32d3ff94e6fe16387e1db671b45211 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 14:34:15 -0700 Subject: [PATCH 31/57] Improve sanitization error messages --- plugin/src/HeadlessAPI.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index a23270e82..acba86c34 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -315,13 +315,14 @@ function API.new(app) if actions then sanitizedActions = {} for id, action in actions do - assert(type(id) == "string", "Action key must be string") - assert(type(action) == "table", "Action must be table") - assert(type(action.text) == "string", "Action.text must be string") - assert(type(action.style) == "string", "Action.style must be string") - assert(action.style == "Solid" or action.style == "Bordered", "Action.style must be 'Solid' or 'Bordered'") - assert(type(action.layoutOrder) == "number", "Action.layoutOrder must be number") - assert(type(action.onClick) == "function", "Action.onClick must be function") + assert(type(id) == "string", "Actions key must be string") + local actionId = "Actions." .. id + assert(type(action) == "table", actionId .. " must be table") + assert(type(action.text) == "string", actionId .. ".text must be string") + assert(type(action.style) == "string", actionId .. ".style must be string") + assert(action.style == "Solid" or action.style == "Bordered", actionId .. ".style must be 'Solid' or 'Bordered'") + assert(type(action.layoutOrder) == "number", actionId .. ".layoutOrder must be number") + assert(type(action.onClick) == "function", actionId .. ".onClick must be function") sanitizedActions[id] = { text = action.text, From 5c0e1d0ec4dfd3be9d44e9cea22ed0b98d2f4c0b Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 14:34:38 -0700 Subject: [PATCH 32/57] Adjust the UI for third party notifications to include source --- plugin/src/App/Notifications.lua | 45 ++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index cb521331b..4a891e29b 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -126,14 +126,24 @@ function Notification:render() buttonsX += (count - 1) * 5 end - local paddingY, logoSize = 20, 32 + local paddingX, paddingY, logoSize = 24, 20, 32 + local sourceY = if self.props.source then logoSize + 5 else 0 + local sourceX = if self.props.source then TextService:GetTextSize( + self.props.source, + 15, + Enum.Font.GothamMedium, + Vector2.new(350, 15) + ).X + logoSize + 5 else 0 local actionsY = if self.props.actions then 35 else 0 - local contentX = math.max(textBounds.X, buttonsX) + local contentX = + math.max(textBounds.X, buttonsX, sourceX) + + (if self.props.thirdParty then 0 else logoSize + 3) + + 2 local size = self.binding:map(function(value) return UDim2.fromOffset( - (35 + 40 + contentX) * value, - 5 + actionsY + paddingY + math.max(logoSize, textBounds.Y) + (paddingX + contentX) * value, + 5 + actionsY + sourceY + paddingY + math.max(logoSize, textBounds.Y) ) end) @@ -154,8 +164,8 @@ function Notification:render() size = UDim2.new(1, 0, 1, 0), }, { Contents = e("Frame", { - Size = UDim2.new(0, 35 + contentX, 1, -paddingY), - Position = UDim2.new(0, 0, 0, paddingY / 2), + Size = UDim2.new(1, 0, 1, 0), + Position = UDim2.new(0, 0, 0, 0), BackgroundTransparency = 1 }, { Logo = e("ImageLabel", { @@ -166,6 +176,19 @@ function Notification:render() Position = UDim2.new(0, 0, 0, 0), AnchorPoint = Vector2.new(0, 0), }), + Source = if self.props.source then e("TextLabel", { + Text = self.props.source, + Font = Enum.Font.GothamMedium, + TextSize = 15, + TextColor3 = theme.Notification.InfoColor, + TextTransparency = transparency, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = Enum.TextTruncate.AtEnd, + + Size = UDim2.new(1, -logoSize - 5, 0, logoSize), + Position = UDim2.fromOffset(logoSize + 5, 0), + BackgroundTransparency = 1, + }) else nil, Message = e("TextLabel", { Text = self.props.text, Font = Enum.Font.GothamMedium, @@ -176,9 +199,7 @@ function Notification:render() TextWrapped = true, Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), - Position = UDim2.fromOffset(35, 0), - - LayoutOrder = 1, + Position = if self.props.thirdParty then UDim2.fromOffset(0, logoSize + 5) else UDim2.fromOffset(logoSize + 3, 0), BackgroundTransparency = 1, }), Actions = if self.props.actions then e("Frame", { @@ -199,8 +220,10 @@ function Notification:render() }), Padding = e("UIPadding", { - PaddingLeft = UDim.new(0, 17), - PaddingRight = UDim.new(0, 15), + PaddingLeft = UDim.new(0, paddingX / 2), + PaddingRight = UDim.new(0, paddingX / 2), + PaddingTop = UDim.new(0, paddingY / 2), + PaddingBottom = UDim.new(0, paddingY / 2), }), }) }) From 91cba1b0e972cc6a31d818ea66e58f7d58a710b5 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 15:09:11 -0700 Subject: [PATCH 33/57] Return dismiss function from Notify --- plugin/src/HeadlessAPI.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index acba86c34..83e735ea3 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -302,13 +302,13 @@ function API.new(app) end Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" - function Rojo:Notify(msg: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?) + function Rojo:Notify(msg: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?): () -> () assert(type(msg) == "string", "Message must be type `string`") assert(type(timeout) == "number" or timeout == nil, "Timeout must be type `number?`") assert((actions == nil) or (type(actions) == "table"), "Actions must be table or nil") if Rojo:_checkRateLimit("Notify") then - return + return function() end end local sanitizedActions = nil @@ -333,8 +333,7 @@ function API.new(app) end end - app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout, sanitizedActions) - return + return app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout, sanitizedActions) end Rojo._apiDescriptions.GetHostAndPort = "Gets the host and port that Rojo is set to" From dcfdf7fabd24d4a8bb0b07e5d84b8761297bf94b Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 4 Jul 2023 19:21:32 -0700 Subject: [PATCH 34/57] Prevent misuse of RequestAccess --- plugin/src/App/init.lua | 10 +++++++++- plugin/src/HeadlessAPI.lua | 25 ++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 4e5e939d1..6cb47d75d 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -314,9 +314,16 @@ function App:releaseSyncLock() Log.trace("Could not release sync lock because it is owned by {}", lock.Value) end -function App:requestPermission(source: string, name: string, apis: {string}, initialState: {[string]: boolean?}): {[string]: boolean} +function App:requestPermission(plugin: Plugin, source: string, name: string, apis: {string}, initialState: {[string]: boolean?}): {[string]: boolean} local responseEvent = Instance.new("BindableEvent") + Log.info("The third-party plugin '{}' is requesting permission to use the API!", name) + + local unloadProtection = plugin.Unloading:Connect(function() + Log.warn("Cancelling API permission request for '{}' because the third-party plugin has been removed.", name) + responseEvent:Fire(initialState) + end) + self:setState(function(state) state.popups[source .. " Permissions"] = { name = name, @@ -338,6 +345,7 @@ function App:requestPermission(source: string, name: string, apis: {string}, ini local response = responseEvent.Event:Wait() responseEvent:Destroy() + unloadProtection:Disconnect() self:setState(function(state) state.popups[source .. " Permissions"] = nil diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 83e735ea3..1221be7e1 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -23,7 +23,9 @@ function API.new(app) local Rojo = {} Rojo._rateLimit = {} + Rojo._sourceToPlugin = {} Rojo._permissions = Settings:get("apiPermissions") or {} + Rojo._activePermissionRequests = {} Rojo._changedEvent = Instance.new("BindableEvent") Rojo._apiDescriptions = {} @@ -55,7 +57,7 @@ function API.new(app) local traceback = string.split(debug.traceback(), "\n") local topLevel = traceback[#traceback - 1] - local localPlugin = string.match(topLevel, "(user_.-)%.") + local localPlugin = string.match(topLevel, "user_.-%.%w+") if localPlugin then return localPlugin end @@ -188,15 +190,26 @@ function API.new(app) end Rojo._apiDescriptions.RequestAccess = "Used to gain access to Rojo API members" - function Rojo:RequestAccess(apis: {string}): {[string]: boolean} - assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names") + function Rojo:RequestAccess(plugin: Plugin, apis: {string}): {[string]: boolean} + assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names as the second argument") + assert(typeof(plugin) == "Instance" and plugin:IsA("Plugin"), "Rojo:RequestAccess expects a Plugin as the first argument") + + local source, name = Rojo:_getCallerSource(), Rojo:_getCallerName() + Rojo._sourceToPlugin[source] = plugin if Rojo:_checkRateLimit("RequestAccess") then -- Because this opens a popup, we dont want to let users get spammed by it return {} end - local source, name = Rojo:_getCallerSource(), Rojo:_getCallerName() + if Rojo._activePermissionRequests[source] then + -- If a request is already active, exit + error( + "Rojo:RequestAccess cannot be called in multiple threads at once. Please call it once and wait for the response before calling it again.", + 2 + ) + end + Rojo._activePermissionRequests[source] = true if Rojo._permissions[source] == nil then Rojo._permissions[source] = {} @@ -226,10 +239,11 @@ function API.new(app) for _, api in sanitizedApis do response[api] = true end + Rojo._activePermissionRequests[source] = nil return response end - local response = app:requestPermission(source, name, sanitizedApis, Rojo._permissions[source]) + local response = app:requestPermission(plugin, source, name, sanitizedApis, Rojo._permissions[source]) for api, granted in response do Log.warn(string.format( @@ -240,6 +254,7 @@ function API.new(app) end Settings:set("apiPermissions", Rojo._permissions) + Rojo._activePermissionRequests[source] = nil return response end From a0b99317bcb8aababd2235ecdbdcb80fb16d24d4 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sat, 8 Jul 2023 15:39:54 -0700 Subject: [PATCH 35/57] Fix editing permissions to use new request api --- plugin/src/App/StatusPages/Permissions/init.lua | 2 +- plugin/src/App/init.lua | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index 140e8ac48..5d10736f0 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -100,7 +100,7 @@ function PermissionsPage:render() ), onClick = function() - self.props.onEdit(source, meta, self.props.headlessAPI._permissions[source] or {}) + self.props.onEdit(self.props.headlessAPI._sourceToPlugin[source], source, meta, self.props.headlessAPI._permissions[source] or {}) end, }) end diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 6cb47d75d..bb1ce6b3c 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -319,10 +319,10 @@ function App:requestPermission(plugin: Plugin, source: string, name: string, api Log.info("The third-party plugin '{}' is requesting permission to use the API!", name) - local unloadProtection = plugin.Unloading:Connect(function() + local unloadProtection = if plugin then plugin.Unloading:Connect(function() Log.warn("Cancelling API permission request for '{}' because the third-party plugin has been removed.", name) responseEvent:Fire(initialState) - end) + end) else nil self:setState(function(state) state.popups[source .. " Permissions"] = { @@ -345,7 +345,9 @@ function App:requestPermission(plugin: Plugin, source: string, name: string, api local response = responseEvent.Event:Wait() responseEvent:Destroy() - unloadProtection:Disconnect() + if unloadProtection then + unloadProtection:Disconnect() + end self:setState(function(state) state.popups[source .. " Permissions"] = nil @@ -687,12 +689,13 @@ function App:render() }) end, - onEdit = function(source, meta, apiMap) + onEdit = function(plugin, source, meta, apiMap) local apiList = {} for api in apiMap do table.insert(apiList, api) end self:requestPermission( + plugin, source, meta.Name .. if meta.Creator then " by " .. meta.Creator else "", apiList, From b401b6db2f611d5f0280f56ae11661f82f7337fa Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 20 Aug 2023 22:00:38 -0700 Subject: [PATCH 36/57] Use new setting input prop --- plugin/src/App/StatusPages/Settings/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index bcc573196..14988d519 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -158,7 +158,7 @@ function SettingsPage:render() description = "Manage permissions for third party plugins", transparency = self.props.transparency, layoutOrder = 6, - customInput = e("TextButton", { + input = e("TextButton", { Text = "", BackgroundTransparency = 1, Size = UDim2.fromOffset(28, 28), From 955b143cedc0a42b121a65d89bc4887279c55e83 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 4 Sep 2023 08:22:58 -0700 Subject: [PATCH 37/57] Change to a game.Rojo module instead of _G --- plugin/src/App/init.lua | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 41a57bdd8..5774733ca 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -138,14 +138,14 @@ function App:init() self.headlessAPI, self.readOnlyHeadlessAPI = HeadlessAPI.new(self) - -- selene: allow(global_usage) - local existingAPI = _G.Rojo - if existingAPI then + local existingAPIModule = game:FindFirstChild("Rojo") + if existingAPIModule and existingAPIModule:IsA("ModuleScript") then + local existingAPI = require(existingAPIModule :: ModuleScript) + local responseEvent = Instance.new("BindableEvent") responseEvent.Event:Once(function(accepted) if accepted then - -- selene: allow(global_usage) - _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar + existingAPI.API = self.readOnlyHeadlessAPI end responseEvent:Destroy() @@ -163,7 +163,7 @@ function App:init() responseEvent:Fire(false) end, content = e(ConflictAPIPopup, { - existingAPI = existingAPI, + existingAPI = existingAPI.API, onAccept = function() responseEvent:Fire(true) end, @@ -176,8 +176,15 @@ function App:init() return state end) else - -- selene: allow(global_usage) - _G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar + local ExposedAPIModule = Instance.new("ModuleScript") + ExposedAPIModule.Name = "Rojo" + ExposedAPIModule.Archivable = false + ExposedAPIModule.Source = "return { API = nil }" + + local ExposedAPI = require(ExposedAPIModule) + ExposedAPI.API = self.readOnlyHeadlessAPI + + ExposedAPIModule.Parent = game end if From d1d70521a30d6ae39bd4fab52a1f54e0e70b9d73 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 4 Sep 2023 08:46:14 -0700 Subject: [PATCH 38/57] Properly handle changing and removing permissions --- .../src/App/StatusPages/Permissions/init.lua | 22 +++++- plugin/src/App/init.lua | 6 +- plugin/src/HeadlessAPI.lua | 68 +++++++++++++------ 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index 5d10736f0..2a553d9f8 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -64,6 +64,20 @@ local PermissionsPage = Roact.Component:extend("PermissionsPage") function PermissionsPage:init() self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + + self:setState({ + permissions = self.props.headlessAPI._permissions, + }) + + self.changedListener = self.props.headlessAPI._permissionsChanged:Connect(function() + self:setState({ + permissions = self.props.headlessAPI._permissions, + }) + end) +end + +function PermissionsPage:willUnmount() + self.changedListener:Disconnect() end function PermissionsPage:render() @@ -71,7 +85,7 @@ function PermissionsPage:render() theme = theme.Settings local sources = {} - if next(self.props.headlessAPI._permissions) == nil then + if next(self.state.permissions) == nil then sources.noSources = e("TextLabel", { Text = "No third-party plugins have been granted permissions.", Font = Enum.Font.Gotham, @@ -86,7 +100,11 @@ function PermissionsPage:render() BackgroundTransparency = 1, }) else - for source in self.props.headlessAPI._permissions do + for source, permissions in self.state.permissions do + if next(permissions) == nil then + continue + end + local meta = self.props.headlessAPI:_getMetaFromSource(source) sources[source] = e(Listing, { layoutOrder = string.byte(source), diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 5774733ca..e5635de40 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -851,17 +851,19 @@ function App:render() end, onEdit = function(plugin, source, meta, apiMap) + local name = meta.Name .. if meta.Creator then " by " .. meta.Creator else "" local apiList = {} for api in apiMap do table.insert(apiList, api) end - self:requestPermission( + local response = self:requestPermission( plugin, source, - meta.Name .. if meta.Creator then " by " .. meta.Creator else "", + name, apiList, apiMap ) + self.headlessAPI:_setPermissions(source, name, response) end, }), diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 1221be7e1..bf6617394 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -152,17 +152,6 @@ function API.new(app) return "CommandBar" end - function Rojo:_permissionCheck(key: string): boolean - if apiPermissionAllowlist[key] then return true end - - local source = Rojo:_getCallerSource() - if Rojo._permissions[source] == nil then - Rojo._permissions[source] = {} - end - - return not not Rojo._permissions[source][key] - end - local BUCKET, LIMIT = 10, 15 function Rojo:_checkRateLimit(api: string): boolean local source = Rojo:_getCallerSource() @@ -189,6 +178,54 @@ function API.new(app) return false end + Rojo._permissionsChangedEvent = Instance.new("BindableEvent") + Rojo._permissionsChanged = Rojo._permissionsChangedEvent.Event + + function Rojo:_permissionCheck(key: string): boolean + if apiPermissionAllowlist[key] then return true end + + local source = Rojo:_getCallerSource() + if Rojo._permissions[source] == nil then + Rojo._permissions[source] = {} + end + + return not not Rojo._permissions[source][key] + end + + function Rojo:_setPermissions(source, name, permissions) + -- Ensure permissions exist for this source + if Rojo._permissions[source] == nil then + Rojo._permissions[source] = {} + end + + -- Set permissions + for api, granted in permissions do + Log.warn(string.format( + "%s Rojo.%s for '%s'", + granted and "Granting permission to" or "Denying permission to", api, name + )) + Rojo._permissions[source][api] = granted + end + + -- Clear out source if no permissions are granted + local hasAnyPermissions = false + for _, granted in Rojo._permissions[source] do + if granted then + hasAnyPermissions = true + break + end + end + if not hasAnyPermissions then + Rojo._permissions[source] = nil + end + + -- Update stored permissions + Settings:set("apiPermissions", Rojo._permissions) + + -- Share changes + Rojo._permissionsChangedEvent:Fire(source, Rojo._permissions[source]) + end + Rojo._apiDescriptions.RequestAccess = "Used to gain access to Rojo API members" function Rojo:RequestAccess(plugin: Plugin, apis: {string}): {[string]: boolean} assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names as the second argument") @@ -245,14 +282,7 @@ function API.new(app) local response = app:requestPermission(plugin, source, name, sanitizedApis, Rojo._permissions[source]) - for api, granted in response do - Log.warn(string.format( - "%s Rojo.%s for '%s'", - granted and "Granting permission to" or "Denying permission to", api, name - )) - Rojo._permissions[source][api] = granted - end - Settings:set("apiPermissions", Rojo._permissions) + Rojo:_setPermissions(source, name, response) Rojo._activePermissionRequests[source] = nil return response From 588424707dc697f42cd1b2e45abe61369bd602ac Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 18 Sep 2023 17:56:31 -0400 Subject: [PATCH 39/57] Use latest plugingui component --- plugin/src/App/init.lua | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 8f3437f19..9b0210adc 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -714,6 +714,7 @@ function App:render() id = id, title = popup.name, active = true, + isEphemeral = true, initDockState = popup.dockState or Enum.InitialDockState.Top, initEnabled = true, @@ -724,18 +725,7 @@ function App:render() zIndexBehavior = Enum.ZIndexBehavior.Sibling, onClose = popup.onClose, - }, { - Content = popup.content, - - Background = Theme.with(function(theme) - return e("Frame", { - Size = UDim2.new(1, 0, 1, 0), - BackgroundColor3 = theme.BackgroundColor, - ZIndex = 0, - BorderSizePixel = 0, - }) - end), - }) + }, popup.content) end return e(StudioPluginContext.Provider, { From b8e5fda48226025ad53c2341a49b2517efe19c1f Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 18 Sep 2023 20:34:50 -0400 Subject: [PATCH 40/57] Stylua formatting --- plugin/src/App/Components/Toggle.lua | 5 +- plugin/src/App/ConflictAPIPopup.lua | 12 +++- plugin/src/App/Notifications.lua | 50 ++++++------- plugin/src/App/PermissionPopup.lua | 7 +- .../App/StatusPages/Permissions/Listing.lua | 7 +- .../src/App/StatusPages/Permissions/init.lua | 11 ++- plugin/src/App/init.lua | 44 +++++++----- plugin/src/HeadlessAPI.lua | 71 ++++++++++++------- 8 files changed, 132 insertions(+), 75 deletions(-) diff --git a/plugin/src/App/Components/Toggle.lua b/plugin/src/App/Components/Toggle.lua index 924fe075f..967c3ddb4 100644 --- a/plugin/src/App/Components/Toggle.lua +++ b/plugin/src/App/Components/Toggle.lua @@ -85,7 +85,10 @@ function Toggle:render() Size = self.containerSize:map(function(value) local offset = self.props.options ~= nil and 120 or 40 local textBounds = getTextBounds( - self.props.description, 14, Enum.Font.Gotham, 1.2, + self.props.description, + 14, + Enum.Font.Gotham, + 1.2, Vector2.new(value.X - offset, math.huge) ) return UDim2.new(1, -offset, 0, textBounds.Y) diff --git a/plugin/src/App/ConflictAPIPopup.lua b/plugin/src/App/ConflictAPIPopup.lua index 86f93d0b4..2d2c45f64 100644 --- a/plugin/src/App/ConflictAPIPopup.lua +++ b/plugin/src/App/ConflictAPIPopup.lua @@ -58,7 +58,11 @@ function ConflictAPIPopup:render() BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, - Text = string.format("Existing: Version %s, Protocol %d", Version.display(self.props.existingAPI.Version), self.props.existingAPI.ProtocolVersion), + Text = string.format( + "Existing: Version %s, Protocol %d", + Version.display(self.props.existingAPI.Version), + self.props.existingAPI.ProtocolVersion + ), Font = Enum.Font.Gotham, TextSize = 15, TextColor3 = theme.Setting.DescriptionColor, @@ -71,7 +75,11 @@ function ConflictAPIPopup:render() BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, - Text = string.format("Incoming: Version %s, Protocol %d", Version.display(Config.version), Config.protocolVersion), + Text = string.format( + "Incoming: Version %s, Protocol %d", + Version.display(Config.version), + Config.protocolVersion + ), Font = Enum.Font.Gotham, TextSize = 15, TextColor3 = theme.Setting.DescriptionColor, diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index aeabc609e..ecda8208d 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -121,17 +121,11 @@ function Notification:render() local paddingX, paddingY, logoSize = 24, 20, 32 local sourceY = if self.props.source then logoSize + 5 else 0 - local sourceX = if self.props.source then TextService:GetTextSize( - self.props.source, - 15, - Enum.Font.GothamMedium, - Vector2.new(350, 15) - ).X + logoSize + 5 else 0 + local sourceX = if self.props.source + then TextService:GetTextSize(self.props.source, 15, Enum.Font.GothamMedium, Vector2.new(350, 15)).X + logoSize + 5 + else 0 local actionsY = if self.props.actions then 35 else 0 - local contentX = - math.max(textBounds.X, buttonsX, sourceX) - + (if self.props.thirdParty then 0 else logoSize + 3) - + 2 + local contentX = math.max(textBounds.X, buttonsX, sourceX) + (if self.props.thirdParty then 0 else logoSize + 3) + 2 local size = self.binding:map(function(value) return UDim2.fromOffset( @@ -163,25 +157,29 @@ function Notification:render() }, { Logo = e("ImageLabel", { ImageTransparency = transparency, - Image = if self.props.thirdParty then Assets.Images.ThirdPartyPlugin else Assets.Images.PluginButton, + Image = if self.props.thirdParty + then Assets.Images.ThirdPartyPlugin + else Assets.Images.PluginButton, BackgroundTransparency = 1, Size = UDim2.new(0, logoSize, 0, logoSize), Position = UDim2.new(0, 0, 0, 0), AnchorPoint = Vector2.new(0, 0), }), - Source = if self.props.source then e("TextLabel", { - Text = self.props.source, - Font = Enum.Font.GothamMedium, - TextSize = 15, - TextColor3 = theme.Notification.InfoColor, - TextTransparency = transparency, - TextXAlignment = Enum.TextXAlignment.Left, - TextTruncate = Enum.TextTruncate.AtEnd, - - Size = UDim2.new(1, -logoSize - 5, 0, logoSize), - Position = UDim2.fromOffset(logoSize + 5, 0), - BackgroundTransparency = 1, - }) else nil, + Source = if self.props.source + then e("TextLabel", { + Text = self.props.source, + Font = Enum.Font.GothamMedium, + TextSize = 15, + TextColor3 = theme.Notification.InfoColor, + TextTransparency = transparency, + TextXAlignment = Enum.TextXAlignment.Left, + TextTruncate = Enum.TextTruncate.AtEnd, + + Size = UDim2.new(1, -logoSize - 5, 0, logoSize), + Position = UDim2.fromOffset(logoSize + 5, 0), + BackgroundTransparency = 1, + }) + else nil, Message = e("TextLabel", { Text = self.props.text, Font = Enum.Font.GothamMedium, @@ -192,7 +190,9 @@ function Notification:render() TextWrapped = true, Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), - Position = if self.props.thirdParty then UDim2.fromOffset(0, logoSize + 5) else UDim2.fromOffset(logoSize + 3, 0), + Position = if self.props.thirdParty + then UDim2.fromOffset(0, logoSize + 5) + else UDim2.fromOffset(logoSize + 3, 0), BackgroundTransparency = 1, }), Actions = if self.props.actions diff --git a/plugin/src/App/PermissionPopup.lua b/plugin/src/App/PermissionPopup.lua index 72947daab..814c7035e 100644 --- a/plugin/src/App/PermissionPopup.lua +++ b/plugin/src/App/PermissionPopup.lua @@ -71,7 +71,10 @@ function PermissionPopup:render() BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, - Text = string.format("A third-party plugin, %s, is asking to use the following parts of the Rojo API. Please grant/deny access.", self.props.name or "[Unknown]"), + Text = string.format( + "A third-party plugin, %s, is asking to use the following parts of the Rojo API. Please grant/deny access.", + self.props.name or "[Unknown]" + ), Font = Enum.Font.GothamMedium, TextSize = 17, TextColor3 = theme.Setting.NameColor, @@ -97,7 +100,7 @@ function PermissionPopup:render() ScrollingFrame = e(ScrollingFrame, { size = self.infoSize:map(function(infoSize) - return UDim2.new(1, 0, 1, -infoSize.Y-44) + return UDim2.new(1, 0, 1, -infoSize.Y - 44) end), layoutOrder = 2, contentSize = self.contentSize, diff --git a/plugin/src/App/StatusPages/Permissions/Listing.lua b/plugin/src/App/StatusPages/Permissions/Listing.lua index 780b1b91a..c22ce4fef 100644 --- a/plugin/src/App/StatusPages/Permissions/Listing.lua +++ b/plugin/src/App/StatusPages/Permissions/Listing.lua @@ -110,10 +110,13 @@ function Listing:render() Size = self.containerSize:map(function(value) local textBounds = getTextBounds( - self.props.description, 14, Enum.Font.Gotham, 1.2, + self.props.description, + 14, + Enum.Font.Gotham, + 1.2, Vector2.new(value.X - 40, math.huge) ) - return UDim2.new(1, - 40, 0, textBounds.Y) + return UDim2.new(1, -40, 0, textBounds.Y) end), LayoutOrder = 2, diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index 2a553d9f8..7ef2ffdab 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -41,7 +41,7 @@ local function Navbar(props) onClick = props.onBack, }, { Tip = e(Tooltip.Trigger, { - text = "Back" + text = "Back", }), }), @@ -55,7 +55,7 @@ local function Navbar(props) Size = UDim2.new(1, 0, 1, 0), BackgroundTransparency = 1, - }) + }), }) end) end @@ -118,7 +118,12 @@ function PermissionsPage:render() ), onClick = function() - self.props.onEdit(self.props.headlessAPI._sourceToPlugin[source], source, meta, self.props.headlessAPI._permissions[source] or {}) + self.props.onEdit( + self.props.headlessAPI._sourceToPlugin[source], + source, + meta, + self.props.headlessAPI._permissions[source] or {} + ) end, }) end diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 7ce1cc6d3..1b2b4e6fc 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -249,7 +249,14 @@ function App:addNotification( end end -function App:addThirdPartyNotification(source: string, text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?) +function App:addThirdPartyNotification( + source: string, + text: string, + timeout: number?, + actions: { + [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () }, + }? +) if not Settings:get("showNotifications") then return end @@ -409,18 +416,29 @@ function App:releaseSyncLock() Log.trace("Could not release sync lock because it is owned by {}", lock.Value) end -function App:requestPermission(plugin: Plugin, source: string, name: string, apis: {string}, initialState: {[string]: boolean?}): {[string]: boolean} +function App:requestPermission( + plugin: Plugin, + source: string, + name: string, + apis: { string }, + initialState: { [string]: boolean? } +): { [string]: boolean } local responseEvent = Instance.new("BindableEvent") Log.info("The third-party plugin '{}' is requesting permission to use the API!", name) - local unloadProtection = if plugin then plugin.Unloading:Connect(function() - Log.warn("Cancelling API permission request for '{}' because the third-party plugin has been removed.", name) - responseEvent:Fire(initialState) - end) else nil + local unloadProtection = if plugin + then plugin.Unloading:Connect(function() + Log.warn( + "Cancelling API permission request for '{}' because the third-party plugin has been removed.", + name + ) + responseEvent:Fire(initialState) + end) + else nil self:setState(function(state) - state.popups[source .. " Permissions"] = { + state.popups[source .. " Permissions"] = { name = name, content = e(PermissionPopup, { responseEvent = responseEvent, @@ -445,7 +463,7 @@ function App:requestPermission(plugin: Plugin, source: string, name: string, api end self:setState(function(state) - state.popups[source .. " Permissions"] = nil + state.popups[source .. " Permissions"] = nil return state end) @@ -716,7 +734,7 @@ function App:render() local popups = {} for id, popup in self.state.popups do - popups["Rojo_"..id] = e(StudioPluginGui, { + popups["Rojo_" .. id] = e(StudioPluginGui, { id = id, title = popup.name, active = true, @@ -854,13 +872,7 @@ function App:render() for api in apiMap do table.insert(apiList, api) end - local response = self:requestPermission( - plugin, - source, - name, - apiList, - apiMap - ) + local response = self:requestPermission(plugin, source, name, apiList, apiMap) self.headlessAPI:_setPermissions(source, name, response) end, }), diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index bf6617394..6a4cdbb5a 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -17,7 +17,7 @@ local apiPermissionAllowlist = { RequestAccess = true, } -local API = {} +local API = {} function API.new(app) local Rojo = {} @@ -85,7 +85,8 @@ function API.new(app) if info then return info.Name .. " by " .. info.Creator.Name else - local success, newInfo = pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) + local success, newInfo = + pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) if success then cloudIdInfoCache[cloudId] = newInfo return newInfo.Name .. " by " .. newInfo.Creator.Name @@ -119,7 +120,8 @@ function API.new(app) Creator = info.Creator.Name, } else - local success, newInfo = pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) + local success, newInfo = + pcall(MarketplaceService.GetProductInfo, MarketplaceService, tonumber(cloudId), Enum.InfoType.Asset) if success then cloudIdInfoCache[cloudId] = newInfo return { @@ -160,14 +162,11 @@ function API.new(app) Rojo._rateLimit[source] = { [api] = 0, } - elseif Rojo._rateLimit[source][api] == nil then Rojo._rateLimit[source][api] = 0 - elseif Rojo._rateLimit[source][api] >= LIMIT then -- No more than LIMIT requests per BUCKET seconds return true - end Rojo._rateLimit[source][api] += 1 @@ -182,7 +181,9 @@ function API.new(app) Rojo._permissionsChanged = Rojo._permissionsChangedEvent.Event function Rojo:_permissionCheck(key: string): boolean - if apiPermissionAllowlist[key] then return true end + if apiPermissionAllowlist[key] then + return true + end local source = Rojo:_getCallerSource() if Rojo._permissions[source] == nil then @@ -200,10 +201,14 @@ function API.new(app) -- Set permissions for api, granted in permissions do - Log.warn(string.format( - "%s Rojo.%s for '%s'", - granted and "Granting permission to" or "Denying permission to", api, name - )) + Log.warn( + string.format( + "%s Rojo.%s for '%s'", + granted and "Granting permission to" or "Denying permission to", + api, + name + ) + ) Rojo._permissions[source][api] = granted end @@ -227,9 +232,12 @@ function API.new(app) end Rojo._apiDescriptions.RequestAccess = "Used to gain access to Rojo API members" - function Rojo:RequestAccess(plugin: Plugin, apis: {string}): {[string]: boolean} + function Rojo:RequestAccess(plugin: Plugin, apis: { string }): { [string]: boolean } assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names as the second argument") - assert(typeof(plugin) == "Instance" and plugin:IsA("Plugin"), "Rojo:RequestAccess expects a Plugin as the first argument") + assert( + typeof(plugin) == "Instance" and plugin:IsA("Plugin"), + "Rojo:RequestAccess expects a Plugin as the first argument" + ) local source, name = Rojo:_getCallerSource(), Rojo:_getCallerName() Rojo._sourceToPlugin[source] = plugin @@ -291,7 +299,7 @@ function API.new(app) Rojo._apiDescriptions.Test = "Prints the given arguments to the console" function Rojo:Test(...) local args = table.pack(...) - for i=1, args.n do + for i = 1, args.n do local v = args[i] local t = type(v) if t == "string" then @@ -301,10 +309,14 @@ function API.new(app) end end - print(string.format( - "Rojo:Test(%s) called from '%s' (%s)", - table.concat(args, ", "), Rojo:_getCallerName(), Rojo:_getCallerSource() - )) + print( + string.format( + "Rojo:Test(%s) called from '%s' (%s)", + table.concat(args, ", "), + Rojo:_getCallerName(), + Rojo:_getCallerSource() + ) + ) end Rojo._apiDescriptions.ConnectAsync = "Connects to a Rojo server" @@ -347,7 +359,11 @@ function API.new(app) end Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" - function Rojo:Notify(msg: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?): () -> () + function Rojo:Notify( + msg: string, + timeout: number?, + actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }? + ): () -> () assert(type(msg) == "string", "Message must be type `string`") assert(type(timeout) == "number" or timeout == nil, "Timeout must be type `number?`") assert((actions == nil) or (type(actions) == "table"), "Actions must be table or nil") @@ -365,7 +381,10 @@ function API.new(app) assert(type(action) == "table", actionId .. " must be table") assert(type(action.text) == "string", actionId .. ".text must be string") assert(type(action.style) == "string", actionId .. ".style must be string") - assert(action.style == "Solid" or action.style == "Bordered", actionId .. ".style must be 'Solid' or 'Bordered'") + assert( + action.style == "Solid" or action.style == "Bordered", + actionId .. ".style must be 'Solid' or 'Bordered'" + ) assert(type(action.layoutOrder) == "number", actionId .. ".layoutOrder must be number") assert(type(action.onClick) == "function", actionId .. ".onClick must be function") @@ -409,10 +428,14 @@ function API.new(app) -- Permissions check local granted = Rojo:_permissionCheck(key) if not granted then - error(string.format( - "Attempted to read Rojo.%s, but the plugin does not have permission to do so.\nPlease first use Rojo:RequestAccess({ \"%s\" }) to gain access to this API.", - key, key - ), 2) + error( + string.format( + 'Attempted to read Rojo.%s, but the plugin does not have permission to do so.\nPlease first use Rojo:RequestAccess({ "%s" }) to gain access to this API.', + key, + key + ), + 2 + ) end return Rojo[key] From e7cf73320b581d8f584f5820f0b529459f410fd5 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 18 Sep 2023 20:44:45 -0400 Subject: [PATCH 41/57] Remove unneeded frame --- plugin/src/App/Notifications.lua | 102 +++++++++++++++---------------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/plugin/src/App/Notifications.lua b/plugin/src/App/Notifications.lua index ecda8208d..e934cc244 100644 --- a/plugin/src/App/Notifications.lua +++ b/plugin/src/App/Notifications.lua @@ -150,69 +150,63 @@ function Notification:render() transparency = transparency, size = UDim2.new(1, 0, 1, 0), }, { - Contents = e("Frame", { - Size = UDim2.new(0, 35 + contentX, 1, -paddingY), - Position = UDim2.new(0, 0, 0, paddingY / 2), + Logo = e("ImageLabel", { + ImageTransparency = transparency, + Image = if self.props.thirdParty + then Assets.Images.ThirdPartyPlugin + else Assets.Images.PluginButton, BackgroundTransparency = 1, - }, { - Logo = e("ImageLabel", { - ImageTransparency = transparency, - Image = if self.props.thirdParty - then Assets.Images.ThirdPartyPlugin - else Assets.Images.PluginButton, - BackgroundTransparency = 1, - Size = UDim2.new(0, logoSize, 0, logoSize), - Position = UDim2.new(0, 0, 0, 0), - AnchorPoint = Vector2.new(0, 0), - }), - Source = if self.props.source - then e("TextLabel", { - Text = self.props.source, - Font = Enum.Font.GothamMedium, - TextSize = 15, - TextColor3 = theme.Notification.InfoColor, - TextTransparency = transparency, - TextXAlignment = Enum.TextXAlignment.Left, - TextTruncate = Enum.TextTruncate.AtEnd, - - Size = UDim2.new(1, -logoSize - 5, 0, logoSize), - Position = UDim2.fromOffset(logoSize + 5, 0), - BackgroundTransparency = 1, - }) - else nil, - Message = e("TextLabel", { - Text = self.props.text, + Size = UDim2.new(0, logoSize, 0, logoSize), + Position = UDim2.new(0, 0, 0, 0), + AnchorPoint = Vector2.new(0, 0), + }), + Source = if self.props.source + then e("TextLabel", { + Text = self.props.source, Font = Enum.Font.GothamMedium, TextSize = 15, TextColor3 = theme.Notification.InfoColor, TextTransparency = transparency, TextXAlignment = Enum.TextXAlignment.Left, - TextWrapped = true, + TextTruncate = Enum.TextTruncate.AtEnd, - Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), - Position = if self.props.thirdParty - then UDim2.fromOffset(0, logoSize + 5) - else UDim2.fromOffset(logoSize + 3, 0), + Size = UDim2.new(1, -logoSize - 5, 0, logoSize), + Position = UDim2.fromOffset(logoSize + 5, 0), BackgroundTransparency = 1, - }), - Actions = if self.props.actions - then e("Frame", { - Size = UDim2.new(1, -40, 0, 35), - Position = UDim2.new(1, 0, 1, 0), - AnchorPoint = Vector2.new(1, 1), - BackgroundTransparency = 1, - }, { - Layout = e("UIListLayout", { - FillDirection = Enum.FillDirection.Horizontal, - HorizontalAlignment = Enum.HorizontalAlignment.Right, - VerticalAlignment = Enum.VerticalAlignment.Center, - SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 5), - }), - Buttons = Roact.createFragment(actionButtons), - }) - else nil, + }) + else nil, + Message = e("TextLabel", { + Text = self.props.text, + Font = Enum.Font.GothamMedium, + TextSize = 15, + TextColor3 = theme.Notification.InfoColor, + TextTransparency = transparency, + TextXAlignment = Enum.TextXAlignment.Left, + TextWrapped = true, + + Size = UDim2.new(0, textBounds.X, 0, textBounds.Y), + Position = if self.props.thirdParty + then UDim2.fromOffset(0, logoSize + 5) + else UDim2.fromOffset(logoSize + 3, 0), + BackgroundTransparency = 1, }), + Actions = if self.props.actions + then e("Frame", { + Size = UDim2.new(1, -40, 0, 35), + Position = UDim2.new(1, 0, 1, 0), + AnchorPoint = Vector2.new(1, 1), + BackgroundTransparency = 1, + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Right, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 5), + }), + Buttons = Roact.createFragment(actionButtons), + }) + else nil, Padding = e("UIPadding", { PaddingLeft = UDim.new(0, paddingX / 2), From 09bcef8e8eae299d5c0aeeb98c464b6c83013fcb Mon Sep 17 00:00:00 2001 From: boatbomber Date: Thu, 1 Feb 2024 00:25:57 -0800 Subject: [PATCH 42/57] Remove unused vars --- plugin/src/App/StatusPages/Permissions/Listing.lua | 4 ---- plugin/src/App/StatusPages/Permissions/init.lua | 1 - 2 files changed, 5 deletions(-) diff --git a/plugin/src/App/StatusPages/Permissions/Listing.lua b/plugin/src/App/StatusPages/Permissions/Listing.lua index c22ce4fef..ed3f7b354 100644 --- a/plugin/src/App/StatusPages/Permissions/Listing.lua +++ b/plugin/src/App/StatusPages/Permissions/Listing.lua @@ -6,13 +6,9 @@ local Packages = Rojo.Packages local Roact = require(Packages.Roact) -local Settings = require(Plugin.Settings) local Assets = require(Plugin.Assets) local Theme = require(Plugin.App.Theme) -local Checkbox = require(Plugin.App.Components.Checkbox) -local Dropdown = require(Plugin.App.Components.Dropdown) -local IconButton = require(Plugin.App.Components.IconButton) local SlicedImage = require(Plugin.App.Components.SlicedImage) local e = Roact.createElement diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index 7ef2ffdab..3762e4749 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -3,7 +3,6 @@ local Plugin = Rojo.Plugin local Packages = Rojo.Packages local Roact = require(Packages.Roact) -local Log = require(Packages.Log) local Assets = require(Plugin.Assets) local Theme = require(Plugin.App.Theme) From 5609205fc31f4702a775725508b4f018ed16575b Mon Sep 17 00:00:00 2001 From: boatbomber Date: Thu, 1 Feb 2024 00:26:31 -0800 Subject: [PATCH 43/57] Fix shadowed and unused theme --- plugin/src/App/StatusPages/Settings/init.lua | 41 +++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index f3f3dad14..ee5e72d58 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -83,8 +83,6 @@ function SettingsPage:render() end return Theme.with(function(theme) - theme = theme.Settings - return e(ScrollingFrame, { size = UDim2.new(1, 0, 1, 0), contentSize = self.contentSize, @@ -187,27 +185,24 @@ function SettingsPage:render() self.props.onNavigatePermissions() end, }, { - Button = Theme.with(function(theme) - theme = theme.Checkbox - return e(SlicedImage, { - slice = Assets.Slices.RoundedBackground, - color = theme.Active.BackgroundColor, - transparency = self.props.transparency, - size = UDim2.new(1, 0, 1, 0), - }, { - Icon = e("ImageLabel", { - Image = Assets.Images.Icons.Expand, - ImageColor3 = theme.Active.IconColor, - ImageTransparency = self.props.transparency, - - Size = UDim2.new(0, 16, 0, 16), - Position = UDim2.new(0.5, 0, 0.5, 0), - AnchorPoint = Vector2.new(0.5, 0.5), - - BackgroundTransparency = 1, - }), - }) - end), + Button = e(SlicedImage, { + slice = Assets.Slices.RoundedBackground, + color = theme.Checkbox.Active.BackgroundColor, + transparency = self.props.transparency, + size = UDim2.new(1, 0, 1, 0), + }, { + Icon = e("ImageLabel", { + Image = Assets.Images.Icons.Expand, + ImageColor3 = theme.Checkbox.Active.IconColor, + ImageTransparency = self.props.transparency, + + Size = UDim2.new(0, 16, 0, 16), + Position = UDim2.new(0.5, 0, 0.5, 0), + AnchorPoint = Vector2.new(0.5, 0.5), + + BackgroundTransparency = 1, + }), + }), }), }), From 997a0406d95e8d7b6db9b266f53d449f25ff38bf Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 12 Feb 2024 21:06:31 -0800 Subject: [PATCH 44/57] Fix changelog pr link --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2e7b768..a4b281f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Rojo Changelog ## Unreleased Changes -* Added headless API for Studio companion plugins. ([#631]) +* Added headless API for Studio companion plugins. ([#639]) * Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Updated Theme to use Studio colors ([#838]) * Added experimental setting for Auto Connect in playtests ([#840]) @@ -52,7 +52,7 @@ **All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced! -[#631]: https://github.com/rojo-rbx/rojo/pull/631 +[#639]: https://github.com/rojo-rbx/rojo/pull/639 [#813]: https://github.com/rojo-rbx/rojo/pull/813 [#834]: https://github.com/rojo-rbx/rojo/pull/834 [#838]: https://github.com/rojo-rbx/rojo/pull/838 From b98af57ec5c3589780bbe6b7e0bd652d56995832 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 12 Feb 2024 22:19:00 -0800 Subject: [PATCH 45/57] Validate settings type before passing them to set --- plugin/src/HeadlessAPI.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 6a4cdbb5a..b91741091 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -350,6 +350,7 @@ function API.new(app) Rojo._apiDescriptions.SetSetting = "Sets a Rojo setting" function Rojo:SetSetting(setting: string, value: any) assert(type(setting) == "string", "Setting must be type `string`") + assert(type(value) == type(Settings:get(setting)), "Value must be the same type as the setting") if Rojo:_checkRateLimit("SetSetting") then return From 57b37f1a4a7a0ee1cb424ed5fc6dbbb93b460b3e Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 12 Feb 2024 22:20:31 -0800 Subject: [PATCH 46/57] Strip duplicate APIs in request --- plugin/src/HeadlessAPI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index b91741091..cf1f1a05d 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -263,7 +263,7 @@ function API.new(app) -- Sanitize request local sanitizedApis = {} for _, api in apis do - if Rojo._apiDescriptions[api] ~= nil then + if Rojo._apiDescriptions[api] ~= nil and table.find(sanitizedApis, api) == nil then table.insert(sanitizedApis, api) else warn(string.format("Rojo.%s is not a valid API", tostring(api))) From 1cd215359df7cf5c5f9e4d7074c4f5ab81c470be Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 12 Feb 2024 22:22:54 -0800 Subject: [PATCH 47/57] Move permissions above experimental section --- plugin/src/App/StatusPages/Settings/init.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua index 399b536ce..664742ac0 100644 --- a/plugin/src/App/StatusPages/Settings/init.lua +++ b/plugin/src/App/StatusPages/Settings/init.lua @@ -161,15 +161,6 @@ function SettingsPage:render() layoutOrder = layoutIncrement(), }), - AutoConnectPlaytestServer = e(Setting, { - id = "autoConnectPlaytestServer", - name = "Auto Connect Playtest Server", - description = "Automatically connect game server to Rojo when playtesting while connected in Edit", - experimental = true, - transparency = self.props.transparency, - layoutOrder = layoutIncrement(), - }), - Permissions = e(Setting, { name = "Third Party Permissions", description = "Manage permissions for third party plugins", @@ -207,6 +198,15 @@ function SettingsPage:render() }), }), + AutoConnectPlaytestServer = e(Setting, { + id = "autoConnectPlaytestServer", + name = "Auto Connect Playtest Server", + description = "Automatically connect game server to Rojo when playtesting while connected in Edit", + experimental = true, + transparency = self.props.transparency, + layoutOrder = layoutIncrement(), + }), + OpenScriptsExternally = e(Setting, { id = "openScriptsExternally", name = "Open Scripts Externally", From ab75e2b5498e0493702f82ce0444af20291498ff Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 12 Feb 2024 22:28:30 -0800 Subject: [PATCH 48/57] Switch to userdata with locked metatable --- plugin/src/HeadlessAPI.lua | 61 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index cf1f1a05d..4ef540e1e 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -413,39 +413,40 @@ function API.new(app) return ApiContext.new(baseUrl) end - local ReadOnly = setmetatable({}, { - __index = function(_, key) - -- Don't expose private members - if string.find(key, "^_") then - return nil - end + local ReadOnly = newproxy(true) + local Metatable = getmetatable(ReadOnly) + Metatable.__index = function(_, key) + -- Don't expose private members + if string.find(key, "^_") then + return nil + end - -- Existence check - if Rojo._apiDescriptions[key] == nil then - warn(string.format("Rojo.%s is not a valid API", tostring(key))) - return nil - end + -- Existence check + if Rojo._apiDescriptions[key] == nil then + warn(string.format("Rojo.%s is not a valid API", tostring(key))) + return nil + end - -- Permissions check - local granted = Rojo:_permissionCheck(key) - if not granted then - error( - string.format( - 'Attempted to read Rojo.%s, but the plugin does not have permission to do so.\nPlease first use Rojo:RequestAccess({ "%s" }) to gain access to this API.', - key, - key - ), - 2 - ) - end + -- Permissions check + local granted = Rojo:_permissionCheck(key) + if not granted then + error( + string.format( + 'Attempted to read Rojo.%s, but the plugin does not have permission to do so.\nPlease first use Rojo:RequestAccess({ "%s" }) to gain access to this API.', + key, + key + ), + 2 + ) + end - return Rojo[key] - end, - __newindex = function(_, key, value) - error(string.format("Attempted to set Rojo.%s to %q but it's a read-only value", key, value), 2) - return - end, - }) + return Rojo[key] + end + Metatable.__newindex = function(_, key, value) + error(string.format("Attempted to set Rojo.%s to %q but it's a read-only value", key, value), 2) + return + end + Metatable.__metatable = "The metatable of the Rojo API is locked" return Rojo, ReadOnly end From 0de0610bdad8725b5aaf0bcb50cfa05d11f6f5ef Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 12 Feb 2024 22:39:52 -0800 Subject: [PATCH 49/57] Spawn action click callbacks in notifs --- plugin/src/HeadlessAPI.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 4ef540e1e..1ba2ee234 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -393,7 +393,9 @@ function API.new(app) text = action.text, style = action.style, layoutOrder = action.layoutOrder, - onClick = action.onClick, + onClick = function() + task.spawn(action.onClick) + end, } end end From 9444c84d73bcb6a0f0b851ad7542dc06cefed83a Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Feb 2024 01:00:20 -0800 Subject: [PATCH 50/57] Improve UX/DX for requesting permissions --- plugin/src/App/Components/Toggle.lua | 137 -------- plugin/src/App/PermissionPopup.lua | 296 +++++++++++++++--- .../src/App/StatusPages/Permissions/init.lua | 58 ++-- plugin/src/App/init.lua | 17 +- plugin/src/Assets.lua | 1 + plugin/src/HeadlessAPI.lua | 159 ++++++---- 6 files changed, 387 insertions(+), 281 deletions(-) delete mode 100644 plugin/src/App/Components/Toggle.lua diff --git a/plugin/src/App/Components/Toggle.lua b/plugin/src/App/Components/Toggle.lua deleted file mode 100644 index 967c3ddb4..000000000 --- a/plugin/src/App/Components/Toggle.lua +++ /dev/null @@ -1,137 +0,0 @@ -local TextService = game:GetService("TextService") - -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin -local Packages = Rojo.Packages - -local Roact = require(Packages.Roact) - -local Theme = require(Plugin.App.Theme) - -local Checkbox = require(Plugin.App.Components.Checkbox) - -local e = Roact.createElement - -local DIVIDER_FADE_SIZE = 0.1 - -local function getTextBounds(text, textSize, font, lineHeight, bounds) - local textBounds = TextService:GetTextSize(text, textSize, font, bounds) - - local lineCount = textBounds.Y / textSize - local lineHeightAbsolute = textSize * lineHeight - - return Vector2.new(textBounds.X, lineHeightAbsolute * lineCount - (lineHeightAbsolute - textSize)) -end - -local Toggle = Roact.Component:extend("Toggle") - -function Toggle:init() - self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) - self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) -end - -function Toggle:render() - return Theme.with(function(theme) - theme = theme.Settings - - return e("Frame", { - Size = self.contentSize:map(function(value) - return UDim2.new(1, 0, 0, 20 + value.Y + 20) - end), - LayoutOrder = self.props.layoutOrder, - ZIndex = -self.props.layoutOrder, - BackgroundTransparency = 1, - - [Roact.Change.AbsoluteSize] = function(object) - self.setContainerSize(object.AbsoluteSize) - end, - }, { - Input = e(Checkbox, { - active = self.props.active, - transparency = self.props.transparency, - position = UDim2.new(1, 0, 0.5, 0), - anchorPoint = Vector2.new(1, 0.5), - onClick = self.props.onClick, - }), - - Text = e("Frame", { - Size = UDim2.new(1, 0, 1, 0), - BackgroundTransparency = 1, - }, { - Name = e("TextLabel", { - Text = self.props.name, - Font = Enum.Font.GothamBold, - TextSize = 17, - TextColor3 = theme.Setting.NameColor, - TextXAlignment = Enum.TextXAlignment.Left, - TextTransparency = self.props.transparency, - - Size = UDim2.new(1, 0, 0, 17), - - LayoutOrder = 1, - BackgroundTransparency = 1, - }), - - Description = e("TextLabel", { - Text = self.props.description, - Font = Enum.Font.Gotham, - LineHeight = 1.2, - TextSize = 14, - TextColor3 = theme.Setting.DescriptionColor, - TextXAlignment = Enum.TextXAlignment.Left, - TextTransparency = self.props.transparency, - TextWrapped = true, - - Size = self.containerSize:map(function(value) - local offset = self.props.options ~= nil and 120 or 40 - local textBounds = getTextBounds( - self.props.description, - 14, - Enum.Font.Gotham, - 1.2, - Vector2.new(value.X - offset, math.huge) - ) - return UDim2.new(1, -offset, 0, textBounds.Y) - end), - - LayoutOrder = 2, - BackgroundTransparency = 1, - }), - - Layout = e("UIListLayout", { - VerticalAlignment = Enum.VerticalAlignment.Center, - FillDirection = Enum.FillDirection.Vertical, - SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 6), - - [Roact.Change.AbsoluteContentSize] = function(object) - self.setContentSize(object.AbsoluteContentSize) - end, - }), - - Padding = e("UIPadding", { - PaddingTop = UDim.new(0, 20), - PaddingBottom = UDim.new(0, 20), - }), - }), - - Divider = e("Frame", { - BackgroundColor3 = theme.DividerColor, - BackgroundTransparency = self.props.transparency, - Size = UDim2.new(1, 0, 0, 1), - BorderSizePixel = 0, - }, { - Gradient = e("UIGradient", { - Transparency = NumberSequence.new({ - NumberSequenceKeypoint.new(0, 1), - NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0), - NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0), - NumberSequenceKeypoint.new(1, 1), - }), - }), - }), - }) - end) -end - -return Toggle diff --git a/plugin/src/App/PermissionPopup.lua b/plugin/src/App/PermissionPopup.lua index 814c7035e..d63cf5d5c 100644 --- a/plugin/src/App/PermissionPopup.lua +++ b/plugin/src/App/PermissionPopup.lua @@ -4,51 +4,139 @@ local Packages = Rojo.Packages local Roact = require(Packages.Roact) +local Assets = require(Plugin.Assets) local Theme = require(Plugin.App.Theme) -local Toggle = require(Plugin.App.Components.Toggle) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) local TextButton = require(Plugin.App.Components.TextButton) local e = Roact.createElement +local DIVIDER_FADE_SIZE = 0.1 + local PermissionPopup = Roact.Component:extend("PermissionPopup") function PermissionPopup:init() self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) self.infoSize, self.setInfoSize = Roact.createBinding(Vector2.new(0, 0)) - - local response = {} - for _, api in self.props.apis do - response[api] = if self.props.initialState[api] == nil then true else self.props.initialState[api] - end - - self:setState({ - response = response, - }) end function PermissionPopup:render() return Theme.with(function(theme) theme = theme.Settings - local apiToggles = {} + local thumbnail = Assets.Images.ThirdPartyPlugin + local thumbnailId = string.match(self.props.source, "cloud_(%d+)") + if thumbnailId then + thumbnail = string.format("rbxthumb://type=Asset&id=%s&w=150&h=150", thumbnailId) + end + + local apiRequests = { + Event = {}, + Property = {}, + Method = {}, + } for index, api in self.props.apis do - apiToggles[api] = e(Toggle, { - active = self.state.response[api], - name = api, - description = self.props.apiDescriptions[api], - transparency = self.props.transparency, - layoutOrder = index, - onClick = function() - self:setState(function(state) - state.response[api] = not state.response[api] - return state - end) - end, + local apiDesc = self.props.apiDescriptions[api] + + apiRequests[apiDesc.Type][api] = e("Frame", { + LayoutOrder = index, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 17), + AutomaticSize = Enum.AutomaticSize.Y, + }, { + Divider = e("Frame", { + BackgroundColor3 = theme.DividerColor, + BackgroundTransparency = self.props.transparency, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 0, -2), + BorderSizePixel = 0, + }, { + Gradient = e("UIGradient", { + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1), + NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1, 1), + }), + }), + }), + Name = e("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 0, 0, 0), + Size = UDim2.new(0, 140, 0, 17), + TextWrapped = true, + AutomaticSize = Enum.AutomaticSize.Y, + Text = api, + Font = Enum.Font.Gotham, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + }), + Desc = e("TextLabel", { + BackgroundTransparency = 1, + Position = UDim2.new(0, 145, 0, 0), + Size = UDim2.new(1, -145, 0, 17), + TextWrapped = true, + AutomaticSize = Enum.AutomaticSize.Y, + Text = apiDesc.Description, + Font = Enum.Font.Gotham, + TextSize = 15, + TextColor3 = theme.Setting.DescriptionColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + }), }) end + -- Add labels to explain the api types + if next(apiRequests.Event) then + apiRequests.Event["_apiTypeInfo"] = e("TextLabel", { + LayoutOrder = -1, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 18), + Text = string.format("%s will be able to listen to these events:", self.props.name), + TextWrapped = true, + AutomaticSize = Enum.AutomaticSize.Y, + Font = Enum.Font.GothamMedium, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + }, e("UIPadding", { PaddingBottom = UDim.new(0, 8) })) + end + if next(apiRequests.Property) then + apiRequests.Property["_apiTypeInfo"] = e("TextLabel", { + LayoutOrder = -1, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 18), + Text = string.format("%s will be able to read these properties:", self.props.name), + TextWrapped = true, + AutomaticSize = Enum.AutomaticSize.Y, + Font = Enum.Font.GothamMedium, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + }, e("UIPadding", { PaddingBottom = UDim.new(0, 8) })) + end + if next(apiRequests.Method) then + apiRequests.Method["_apiTypeInfo"] = e("TextLabel", { + LayoutOrder = -1, + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 18), + Text = string.format("%s will be able to call these methods:", self.props.name), + TextWrapped = true, + AutomaticSize = Enum.AutomaticSize.Y, + Font = Enum.Font.GothamMedium, + TextSize = 17, + TextColor3 = theme.Setting.NameColor, + TextXAlignment = Enum.TextXAlignment.Left, + TextTransparency = self.props.transparency, + }, e("UIPadding", { PaddingBottom = UDim.new(0, 8) })) + end + return e("Frame", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0), @@ -56,8 +144,8 @@ function PermissionPopup:render() Layout = e("UIListLayout", { FillDirection = Enum.FillDirection.Vertical, SortOrder = Enum.SortOrder.LayoutOrder, - Padding = UDim.new(0, 5), - HorizontalAlignment = Enum.HorizontalAlignment.Right, + Padding = UDim.new(0, 15), + HorizontalAlignment = Enum.HorizontalAlignment.Center, }), Padding = e("UIPadding", { @@ -67,59 +155,171 @@ function PermissionPopup:render() PaddingBottom = UDim.new(0, 15), }), + Icons = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 32), + LayoutOrder = 1, + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 5), + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + }), + + ThirdPartyIcon = e("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 32, 0, 32), + Image = thumbnail, + LayoutOrder = 1, + }), + + TransactIcon = e("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 24, 0, 24), + Image = Assets.Images.Icons.Transact, + ImageColor3 = theme.Setting.DescriptionColor, + LayoutOrder = 2, + }), + + RojoIcon = e("ImageLabel", { + BackgroundTransparency = 1, + Size = UDim2.new(0, 32, 0, 32), + Image = Assets.Images.PluginButton, + LayoutOrder = 3, + }), + }), + Info = e("TextLabel", { BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, - Text = string.format( - "A third-party plugin, %s, is asking to use the following parts of the Rojo API. Please grant/deny access.", - self.props.name or "[Unknown]" - ), - Font = Enum.Font.GothamMedium, + Text = string.format("%s is asking to use the Rojo API", self.props.name or "[Unknown]"), + Font = Enum.Font.GothamBold, TextSize = 17, TextColor3 = theme.Setting.NameColor, - TextXAlignment = Enum.TextXAlignment.Left, + TextXAlignment = Enum.TextXAlignment.Center, TextWrapped = true, TextTransparency = self.props.transparency, - LayoutOrder = 1, + LayoutOrder = 2, [Roact.Change.AbsoluteSize] = function(rbx) self.setInfoSize(rbx.AbsoluteSize) end, }), - Submit = e(TextButton, { - text = "Submit", - style = "Solid", - transparency = self.props.transparency, - layoutOrder = 3, - onClick = function() - self.props.responseEvent:Fire(self.state.response) - end, + Divider = e("Frame", { + LayoutOrder = 3, + BackgroundColor3 = theme.DividerColor, + BackgroundTransparency = self.props.transparency, + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 0, -2), + BorderSizePixel = 0, + }, { + Gradient = e("UIGradient", { + Transparency = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 1), + NumberSequenceKeypoint.new(DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1 - DIVIDER_FADE_SIZE, 0), + NumberSequenceKeypoint.new(1, 1), + }), + }), }), ScrollingFrame = e(ScrollingFrame, { size = self.infoSize:map(function(infoSize) - return UDim2.new(1, 0, 1, -infoSize.Y - 44) + return UDim2.new(0.9, 0, 1, -infoSize.Y - 140) end), - layoutOrder = 2, + layoutOrder = 9, contentSize = self.contentSize, transparency = self.props.transparency, }, { - APIToggles = Roact.createFragment(apiToggles), - - Padding = e("UIPadding", { - PaddingRight = UDim.new(0, 15), - }), - Layout = e("UIListLayout", { FillDirection = Enum.FillDirection.Vertical, SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 18), [Roact.Change.AbsoluteContentSize] = function(object) self.setContentSize(object.AbsoluteContentSize) end, }), + + PropertyRequests = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + LayoutOrder = 1, + }, { + APIs = Roact.createFragment(apiRequests.Property), + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 4), + }), + }), + + EventRequests = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + LayoutOrder = 2, + }, { + APIs = Roact.createFragment(apiRequests.Event), + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 4), + }), + }), + + MethodRequests = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 0), + AutomaticSize = Enum.AutomaticSize.Y, + LayoutOrder = 3, + }, { + APIs = Roact.createFragment(apiRequests.Method), + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Vertical, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 4), + }), + }), + }), + + Actions = e("Frame", { + BackgroundTransparency = 1, + Size = UDim2.new(1, 0, 0, 34), + LayoutOrder = 10, + }, { + Layout = e("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal, + HorizontalAlignment = Enum.HorizontalAlignment.Center, + VerticalAlignment = Enum.VerticalAlignment.Center, + SortOrder = Enum.SortOrder.LayoutOrder, + Padding = UDim.new(0, 10), + }), + + Deny = e(TextButton, { + text = "Deny", + style = "Bordered", + transparency = self.props.transparency, + layoutOrder = 1, + onClick = function() + self.props.responseEvent:Fire(false) + end, + }), + + Allow = e(TextButton, { + text = "Allow", + style = "Solid", + transparency = self.props.transparency, + layoutOrder = 2, + onClick = function() + self.props.responseEvent:Fire(true) + end, + }), }), }) end) diff --git a/plugin/src/App/StatusPages/Permissions/init.lua b/plugin/src/App/StatusPages/Permissions/init.lua index 3762e4749..c895e401a 100644 --- a/plugin/src/App/StatusPages/Permissions/init.lua +++ b/plugin/src/App/StatusPages/Permissions/init.lua @@ -84,7 +84,35 @@ function PermissionsPage:render() theme = theme.Settings local sources = {} - if next(self.state.permissions) == nil then + for source, permissions in self.state.permissions do + if next(permissions) == nil then + continue + end + + local meta = self.props.headlessAPI:_getMetaFromSource(source) + sources[source] = e(Listing, { + layoutOrder = string.byte(source), + transparency = self.props.transparency, + + name = meta.Name, + description = string.format( + "%s plugin%s", + meta.Type, + if meta.Creator then " by " .. meta.Creator else "" + ), + + onClick = function() + self.props.onEdit( + self.props.headlessAPI._sourceToPlugin[source], + source, + meta, + self.props.headlessAPI._permissions[source] or {} + ) + end, + }) + end + + if next(sources) == nil then sources.noSources = e("TextLabel", { Text = "No third-party plugins have been granted permissions.", Font = Enum.Font.Gotham, @@ -98,34 +126,6 @@ function PermissionsPage:render() BackgroundTransparency = 1, }) - else - for source, permissions in self.state.permissions do - if next(permissions) == nil then - continue - end - - local meta = self.props.headlessAPI:_getMetaFromSource(source) - sources[source] = e(Listing, { - layoutOrder = string.byte(source), - transparency = self.props.transparency, - - name = meta.Name, - description = string.format( - "%s plugin%s", - meta.Type, - if meta.Creator then " by " .. meta.Creator else "" - ), - - onClick = function() - self.props.onEdit( - self.props.headlessAPI._sourceToPlugin[source], - source, - meta, - self.props.headlessAPI._permissions[source] or {} - ) - end, - }) - end end return e("Frame", { diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 88ef3f318..a040a5c4a 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -459,7 +459,7 @@ function App:requestPermission( source: string, name: string, apis: { string }, - initialState: { [string]: boolean? } + initialState: boolean? ): { [string]: boolean } local responseEvent = Instance.new("BindableEvent") @@ -471,7 +471,7 @@ function App:requestPermission( "Cancelling API permission request for '{}' because the third-party plugin has been removed.", name ) - responseEvent:Fire(initialState) + responseEvent:Fire(initialState or false) end) else nil @@ -480,7 +480,6 @@ function App:requestPermission( name = name, content = e(PermissionPopup, { responseEvent = responseEvent, - initialState = initialState, source = source, name = name, apis = apis, @@ -488,7 +487,7 @@ function App:requestPermission( transparency = Roact.createBinding(0), }), onClose = function() - responseEvent:Fire(initialState) + responseEvent:Fire(initialState or false) end, } return state @@ -833,7 +832,7 @@ function App:render() initEnabled = true, overridePreviousState = true, floatingSize = Vector2.new(400, 300), - minimumSize = Vector2.new(390, 240), + minimumSize = Vector2.new(390, 240) or popup.minimumSize, zIndexBehavior = Enum.ZIndexBehavior.Sibling, @@ -961,8 +960,12 @@ function App:render() for api in apiMap do table.insert(apiList, api) end - local response = self:requestPermission(plugin, source, name, apiList, apiMap) - self.headlessAPI:_setPermissions(source, name, response) + local granted = self:requestPermission(plugin, source, name, apiList, true) + if granted then + self.headlessAPI:_setPermissions(source, name, apiList) + else + self.headlessAPI:_removePermissions(source, name) + end end, }), diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index 247eec4b2..a96a8d70c 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -27,6 +27,7 @@ local Assets = { Reset = "rbxassetid://10142422327", Expand = "rbxassetid://12045401097", Settings = "rbxassetid://12046309515", + Transact = "rbxassetid://16350762910", }, Diff = { Add = "rbxassetid://10434145835", diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index 1ba2ee234..adc9cee02 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -29,22 +29,40 @@ function API.new(app) Rojo._changedEvent = Instance.new("BindableEvent") Rojo._apiDescriptions = {} - Rojo._apiDescriptions.Changed = "An event that fires when a headless API property changes" + Rojo._apiDescriptions.Changed = { + Type = "Event", + Description = "An event that fires when a headless API property changes", + } Rojo.Changed = Rojo._changedEvent.Event - Rojo._apiDescriptions.Connected = "Whether or not the plugin is connected to a Rojo server" + Rojo._apiDescriptions.Connected = { + Type = "Property", + Description = "Whether or not the plugin is connected to a Rojo server", + } Rojo.Connected = if app.serveSession then app.serveSession:getStatus() == "Connected" else false - Rojo._apiDescriptions.Address = "The address (host:port) that the plugin is connected to" + Rojo._apiDescriptions.Address = { + Type = "Property", + Description = "The address (host:port) that the plugin is connected to", + } Rojo.Address = nil - Rojo._apiDescriptions.ProjectName = "The name of the project that the plugin is connected to" + Rojo._apiDescriptions.ProjectName = { + Type = "Property", + Description = "The name of the project that the plugin is connected to", + } Rojo.ProjectName = nil - Rojo._apiDescriptions.Version = "The version of the plugin" + Rojo._apiDescriptions.Version = { + Type = "Property", + Description = "The version of the plugin", + } Rojo.Version = table.clone(Config.version) - Rojo._apiDescriptions.ProtocolVersion = "The protocol version that the plugin is using" + Rojo._apiDescriptions.ProtocolVersion = { + Type = "Property", + Description = "The protocol version that the plugin is using", + } Rojo.ProtocolVersion = Config.protocolVersion function Rojo:_updateProperty(property: string, value: any?) @@ -187,52 +205,49 @@ function API.new(app) local source = Rojo:_getCallerSource() if Rojo._permissions[source] == nil then - Rojo._permissions[source] = {} + return false end return not not Rojo._permissions[source][key] end function Rojo:_setPermissions(source, name, permissions) - -- Ensure permissions exist for this source - if Rojo._permissions[source] == nil then - Rojo._permissions[source] = {} + if next(permissions) == nil then + Rojo:_removePermissions(source, name) + return end -- Set permissions - for api, granted in permissions do - Log.warn( - string.format( - "%s Rojo.%s for '%s'", - granted and "Granting permission to" or "Denying permission to", - api, - name - ) - ) - Rojo._permissions[source][api] = granted + local sourcePermissions = {} + for _, api in permissions do + Log.info(string.format("Granting '%s' access to Rojo.%s", name, api)) + sourcePermissions[api] = true end - -- Clear out source if no permissions are granted - local hasAnyPermissions = false - for _, granted in Rojo._permissions[source] do - if granted then - hasAnyPermissions = true - break - end - end - if not hasAnyPermissions then - Rojo._permissions[source] = nil - end + -- Update stored permissions + Rojo._permissions[source] = sourcePermissions + Settings:set("apiPermissions", Rojo._permissions) + + -- Share changes + Rojo._permissionsChangedEvent:Fire(source, sourcePermissions) + end + + function Rojo:_removePermissions(source, name) + Rojo._permissions[source] = nil + Log.info(string.format("Denying access to Rojo APIs for '%s'", name)) -- Update stored permissions Settings:set("apiPermissions", Rojo._permissions) -- Share changes - Rojo._permissionsChangedEvent:Fire(source, Rojo._permissions[source]) + Rojo._permissionsChangedEvent:Fire(source, nil) end - Rojo._apiDescriptions.RequestAccess = "Used to gain access to Rojo API members" - function Rojo:RequestAccess(plugin: Plugin, apis: { string }): { [string]: boolean } + Rojo._apiDescriptions.RequestAccess = { + Type = "Method", + Description = "Used to gain access to Rojo API members", + } + function Rojo:RequestAccess(plugin: Plugin, apis: { string }): boolean assert(type(apis) == "table", "Rojo:RequestAccess expects an array of valid API names as the second argument") assert( typeof(plugin) == "Instance" and plugin:IsA("Plugin"), @@ -244,7 +259,7 @@ function API.new(app) if Rojo:_checkRateLimit("RequestAccess") then -- Because this opens a popup, we dont want to let users get spammed by it - return {} + return false end if Rojo._activePermissionRequests[source] then @@ -256,10 +271,6 @@ function API.new(app) end Rojo._activePermissionRequests[source] = true - if Rojo._permissions[source] == nil then - Rojo._permissions[source] = {} - end - -- Sanitize request local sanitizedApis = {} for _, api in apis do @@ -270,33 +281,40 @@ function API.new(app) end end assert(#sanitizedApis > 0, "Rojo:RequestAccess expects an array of valid API names") + table.sort(sanitizedApis) local alreadyAllowed = true - for _, api in sanitizedApis do - if not Rojo._permissions[source][api] then - alreadyAllowed = false - break + if Rojo._permissions[source] == nil then + alreadyAllowed = false + else + for _, api in sanitizedApis do + if not Rojo._permissions[source][api] then + alreadyAllowed = false + break + end end end if alreadyAllowed then - local response = {} - for _, api in sanitizedApis do - response[api] = true - end Rojo._activePermissionRequests[source] = nil - return response + return true end - local response = app:requestPermission(plugin, source, name, sanitizedApis, Rojo._permissions[source]) - - Rojo:_setPermissions(source, name, response) + local granted = app:requestPermission(plugin, source, name, sanitizedApis, false) + if granted then + Rojo:_setPermissions(source, name, sanitizedApis) + else + Rojo:_removePermissions(source, name) + end Rojo._activePermissionRequests[source] = nil - return response + return granted end - Rojo._apiDescriptions.Test = "Prints the given arguments to the console" + Rojo._apiDescriptions.Test = { + Type = "Method", + Description = "Prints the given arguments to the console", + } function Rojo:Test(...) local args = table.pack(...) for i = 1, args.n do @@ -319,7 +337,10 @@ function API.new(app) ) end - Rojo._apiDescriptions.ConnectAsync = "Connects to a Rojo server" + Rojo._apiDescriptions.ConnectAsync = { + Type = "Method", + Description = "Connects to a Rojo server", + } function Rojo:ConnectAsync(host: string?, port: string?) assert(type(host) == "string" or host == nil, "Host must be type `string?`") assert(type(port) == "string" or port == nil, "Port must be type `string?`") @@ -331,7 +352,10 @@ function API.new(app) app:startSession(host, port) end - Rojo._apiDescriptions.DisconnectAsync = "Disconnects from the Rojo server" + Rojo._apiDescriptions.DisconnectAsync = { + Type = "Method", + Description = "Disconnects from the Rojo server", + } function Rojo:DisconnectAsync() if Rojo:_checkRateLimit("DisconnectAsync") then return @@ -340,14 +364,20 @@ function API.new(app) app:endSession() end - Rojo._apiDescriptions.GetSetting = "Gets a Rojo setting" + Rojo._apiDescriptions.GetSetting = { + Type = "Method", + Description = "Gets a Rojo setting", + } function Rojo:GetSetting(setting: string): any assert(type(setting) == "string", "Setting must be type `string`") return Settings:get(setting) end - Rojo._apiDescriptions.SetSetting = "Sets a Rojo setting" + Rojo._apiDescriptions.SetSetting = { + Type = "Method", + Description = "Sets a Rojo setting", + } function Rojo:SetSetting(setting: string, value: any) assert(type(setting) == "string", "Setting must be type `string`") assert(type(value) == type(Settings:get(setting)), "Value must be the same type as the setting") @@ -359,7 +389,10 @@ function API.new(app) return Settings:set(setting, value) end - Rojo._apiDescriptions.Notify = "Shows a notification in the Rojo UI" + Rojo._apiDescriptions.Notify = { + Type = "Method", + Description = "Shows a notification in the Rojo UI", + } function Rojo:Notify( msg: string, timeout: number?, @@ -403,12 +436,18 @@ function API.new(app) return app:addThirdPartyNotification(Rojo:_getCallerName(), msg, timeout, sanitizedActions) end - Rojo._apiDescriptions.GetHostAndPort = "Gets the host and port that Rojo is set to" + Rojo._apiDescriptions.GetHostAndPort = { + Type = "Method", + Description = "Gets the host and port that Rojo is set to", + } function Rojo:GetHostAndPort(): (string, string) return app:getHostAndPort() end - Rojo._apiDescriptions.CreateApiContext = "Creates a new API context" + Rojo._apiDescriptions.CreateApiContext = { + Type = "Method", + Description = "Creates a new API context", + } function Rojo:CreateApiContext(baseUrl: string) assert(type(baseUrl) == "string", "Base URL must be type `string`") From a38147973ab4e55d381465684612d32b759a6e31 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Feb 2024 01:11:20 -0800 Subject: [PATCH 51/57] Fix type signature --- plugin/src/HeadlessAPI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index adc9cee02..dddcb772d 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -396,7 +396,7 @@ function API.new(app) function Rojo:Notify( msg: string, timeout: number?, - actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }? + actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: () -> () } }? ): () -> () assert(type(msg) == "string", "Message must be type `string`") assert(type(timeout) == "number" or timeout == nil, "Timeout must be type `number?`") From fd2e309c2a3df47692f958c9901985dcc3d1e24a Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Feb 2024 01:16:38 -0800 Subject: [PATCH 52/57] Note that Test is for development --- plugin/src/HeadlessAPI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index dddcb772d..c47cae804 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -313,7 +313,7 @@ function API.new(app) Rojo._apiDescriptions.Test = { Type = "Method", - Description = "Prints the given arguments to the console", + Description = "Prints the given arguments to the console. Useful during development for testing purposes.", } function Rojo:Test(...) local args = table.pack(...) From d1078a1dfa355ab9c2e094a037b50eea03e5cca1 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Feb 2024 01:26:28 -0800 Subject: [PATCH 53/57] Clearer text for users --- plugin/src/HeadlessAPI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index c47cae804..eb0838cda 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -31,7 +31,7 @@ function API.new(app) Rojo._apiDescriptions.Changed = { Type = "Event", - Description = "An event that fires when a headless API property changes", + Description = "An event that fires when a Rojo API property changes", } Rojo.Changed = Rojo._changedEvent.Event From a7e15a025903ce0c8b8e7648fa2a73b06eac5788 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Feb 2024 18:31:55 -0800 Subject: [PATCH 54/57] Remove setsetting --- plugin/src/HeadlessAPI.lua | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/plugin/src/HeadlessAPI.lua b/plugin/src/HeadlessAPI.lua index eb0838cda..9bdad4964 100644 --- a/plugin/src/HeadlessAPI.lua +++ b/plugin/src/HeadlessAPI.lua @@ -374,21 +374,6 @@ function API.new(app) return Settings:get(setting) end - Rojo._apiDescriptions.SetSetting = { - Type = "Method", - Description = "Sets a Rojo setting", - } - function Rojo:SetSetting(setting: string, value: any) - assert(type(setting) == "string", "Setting must be type `string`") - assert(type(value) == type(Settings:get(setting)), "Value must be the same type as the setting") - - if Rojo:_checkRateLimit("SetSetting") then - return - end - - return Settings:set(setting, value) - end - Rojo._apiDescriptions.Notify = { Type = "Method", Description = "Shows a notification in the Rojo UI", From 1af32ad2c79ecfc89a60eba39727624e80cd7a16 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 13 Feb 2024 18:35:46 -0800 Subject: [PATCH 55/57] Ensure permissions are always listed in the same order --- plugin/src/App/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index a040a5c4a..b1637a883 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -960,6 +960,8 @@ function App:render() for api in apiMap do table.insert(apiList, api) end + table.sort(apiList) + local granted = self:requestPermission(plugin, source, name, apiList, true) if granted then self.headlessAPI:_setPermissions(source, name, apiList) From 6f9fd5ca9503a56e8f61c079c5bf0e3f1f0fbf34 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 5 Nov 2024 18:40:14 -0800 Subject: [PATCH 56/57] add images to assets --- assets/images/icons/settings.png | Bin 0 -> 237 bytes assets/images/icons/transact.png | Bin 0 -> 284 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/images/icons/settings.png create mode 100644 assets/images/icons/transact.png diff --git a/assets/images/icons/settings.png b/assets/images/icons/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..7cfac3ac494ffba9fb466da296a3ab3d77a574fa GIT binary patch literal 237 zcmVC3&m=t_3!=yE!;4mc{8ot00000NkvXXu0mjfp-Ej} literal 0 HcmV?d00001 diff --git a/assets/images/icons/transact.png b/assets/images/icons/transact.png new file mode 100644 index 0000000000000000000000000000000000000000..be49074cb9f6307fc34217264c7c06b2a7c5fb50 GIT binary patch literal 284 zcmV+%0ptFOP)r+!R{s+Mu80XZSnrgnU5hqC0ipBos<%7u{YL4rauOAv?Xh z2lu|Y_q{i8k5FGxGr?M!#!UpNq(YNp)%H?e={aQp4A>|!JAcO)`H^W1EsvsG;cY~! z&=DxoLromx8i|!_ebrK~_!L?(DMLB{=3UdX&?}yRg2ZgCz|tYm!Xf^eHvs*V^wNwU z&vtRS;?jFqNtb4J7-9KryMEK=%dJ}Gn}ni*`&Tj9Q|J$tm!bXV@TgBoX}v+aX|rce i!GE*64;y}zTfqUIJh35$*N Date: Tue, 5 Nov 2024 19:29:59 -0800 Subject: [PATCH 57/57] Move some image assets into icons dir --- assets/images/{ => icons}/syncsuccess.png | Bin assets/images/{ => icons}/syncwarning.png | Bin assets/{ => images/icons}/thirdParty.png | Bin 3 files changed, 0 insertions(+), 0 deletions(-) rename assets/images/{ => icons}/syncsuccess.png (100%) rename assets/images/{ => icons}/syncwarning.png (100%) rename assets/{ => images/icons}/thirdParty.png (100%) diff --git a/assets/images/syncsuccess.png b/assets/images/icons/syncsuccess.png similarity index 100% rename from assets/images/syncsuccess.png rename to assets/images/icons/syncsuccess.png diff --git a/assets/images/syncwarning.png b/assets/images/icons/syncwarning.png similarity index 100% rename from assets/images/syncwarning.png rename to assets/images/icons/syncwarning.png diff --git a/assets/thirdParty.png b/assets/images/icons/thirdParty.png similarity index 100% rename from assets/thirdParty.png rename to assets/images/icons/thirdParty.png