From 401812931a5122e36f936369decec5fb5ed11b36 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 23 Jul 2020 22:59:48 +0200 Subject: [PATCH 1/5] Add BigBlueButton widget to integration manager This adds the widget and the configuration for it to the integration manager, so that the user can add a BBB widget. The code that will actually run inside of the widget is not yet here. A few CSS things are updated as well to make them more generic, as we reused a few things that were previously jitsi only. --- .../20200630165247-AddBigBlueButtonWidget.ts | 23 ++++++++ web/app/app.module.ts | 2 + web/app/app.routing.ts | 6 ++ .../bigbluebutton.widget.component.html | 11 ++++ .../bigbluebutton.widget.component.scss | 0 .../bigbluebutton.widget.component.ts | 53 ++++++++++++++++++ web/app/home/home.component.html | 4 ++ web/app/shared/models/widget.ts | 1 + .../shared/registry/integrations.registry.ts | 4 ++ .../jitsi/jitsi.component.scss | 4 +- web/public/img/avatars/bigbluebutton.png | Bin 0 -> 13303 bytes web/style/themes/dark.scss | 2 +- web/style/themes/light.scss | 4 +- 13 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts create mode 100644 web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html create mode 100644 web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.scss create mode 100644 web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts create mode 100644 web/public/img/avatars/bigbluebutton.png diff --git a/src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts b/src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts new file mode 100644 index 0000000..27c00ba --- /dev/null +++ b/src/db/migrations/20200630165247-AddBigBlueButtonWidget.ts @@ -0,0 +1,23 @@ +import { QueryInterface } from "sequelize"; + +export default { + up: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkInsert("dimension_widgets", [ + { + type: "bigbluebutton", + name: "BigBlueButton", + avatarUrl: "/img/avatars/bigbluebutton.png", + isEnabled: true, + isPublic: true, + description: "Embed a BigBlueButton conference", + } + ])); + }, + down: (queryInterface: QueryInterface) => { + return Promise.resolve() + .then(() => queryInterface.bulkDelete("dimension_widgets", { + type: "bigbluebutton", + })); + } +} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index 6f5496e..e217674 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -118,6 +118,7 @@ import { CKEditorModule } from "@ckeditor/ckeditor5-angular"; import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component"; import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component"; import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component"; +import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; @NgModule({ imports: [ @@ -148,6 +149,7 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo VideoWidgetWrapperComponent, JitsiWidgetWrapperComponent, GCalWidgetWrapperComponent, + BigBlueButtonConfigComponent, RiotHomeComponent, IboxComponent, ConfigScreenWidgetComponent, diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 1086ea8..1e36590 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -2,6 +2,7 @@ import { RouterModule, Routes } from "@angular/router"; import { HomeComponent } from "./home/home.component"; import { RiotComponent } from "./riot/riot.component"; import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component"; +import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component"; import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component"; import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component"; @@ -180,6 +181,11 @@ const routes: Routes = [ component: CustomWidgetConfigComponent, data: {breadcrumb: "Custom Widgets", name: "Custom Widgets"}, }, + { + path: "bigbluebutton", + component: BigBlueButtonConfigComponent, + data: {breadcrumb: "BigBlueButton Widgets", name: "BigBlueButton Widgets"}, + }, { path: "etherpad", component: EtherpadWidgetConfigComponent, diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html new file mode 100644 index 0000000..a0ab995 --- /dev/null +++ b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.html @@ -0,0 +1,11 @@ + + + + + diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.scss b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts new file mode 100644 index 0000000..ac92217 --- /dev/null +++ b/web/app/configs/widget/bigbluebutton/bigbluebutton.widget.component.ts @@ -0,0 +1,53 @@ +import { WidgetComponent, DISABLE_AUTOMATIC_WRAPPING } from "../widget.component"; +import { WIDGET_BIGBLUEBUTTON, EditableWidget } from "../../../shared/models/widget"; +import { Component } from "@angular/core"; +import { FE_BigBlueButtonWidget } from "../../../shared/models/integration"; +import { SessionStorage } from "../../../shared/SessionStorage"; +import * as url from "url"; + +@Component({ + templateUrl: "bigbluebutton.widget.component.html", + styleUrls: ["bigbluebutton.widget.component.scss"], +}) + +// Configuration of BigBlueButton widgets +export class BigBlueButtonConfigComponent extends WidgetComponent { + private bigBlueButtonWidget: FE_BigBlueButtonWidget = SessionStorage.editIntegration; + + constructor() { + super(WIDGET_BIGBLUEBUTTON, "BigBlueButton Conference", DISABLE_AUTOMATIC_WRAPPING); + } + + protected OnWidgetsDiscovered(widgets: EditableWidget[]) { + for (const widget of widgets) { + widget.data.conferenceUrl = this.templateUrl(widget.url, widget.data); + } + } + + protected OnNewWidgetPrepared(widget: EditableWidget): void { + widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl; + } + + protected OnWidgetBeforeAdd(widget: EditableWidget) { + this.setWidgetOptions(widget); + } + + protected OnWidgetBeforeEdit(widget: EditableWidget) { + this.setWidgetOptions(widget); + } + + private setWidgetOptions(widget: EditableWidget) { + widget.dimension.newData.url = widget.dimension.newData.conferenceUrl; + + let widgetQueryString = url.format({ + query: { + "conferenceUrl": "$conferenceUrl", + "displayName": "$matrix_display_name", + "avatarUrl": "$matrix_avatar_url", + "userId": "$matrix_user_id", + }, + }); + widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k)); + widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString; + } +} diff --git a/web/app/home/home.component.html b/web/app/home/home.component.html index 20c63ea..72a58f1 100644 --- a/web/app/home/home.component.html +++ b/web/app/home/home.component.html @@ -69,6 +69,10 @@ Google Calendar +
+ + BigBlueButton +
Custom Widget diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index 37d853a..0400c80 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -1,6 +1,7 @@ import { WidgetsResponse } from "./server-client-responses"; export const WIDGET_CUSTOM = ["m.custom", "customwidget", "dimension-customwidget"]; +export const WIDGET_BIGBLUEBUTTON = ["m.bigbluebutton", "bigbluebutton", "dimension-bigbluebutton"]; export const WIDGET_ETHERPAD = ["m.etherpad", "etherpad", "dimension-etherpad"]; export const WIDGET_GOOGLE_DOCS = ["m.googledoc", "googledocs", "dimension-googledocs"]; export const WIDGET_GOOGLE_CALENDAR = ["m.googlecalendar", "googlecalendar", "dimension-googlecalendar"]; diff --git a/web/app/shared/registry/integrations.registry.ts b/web/app/shared/registry/integrations.registry.ts index 3d902b9..673836b 100644 --- a/web/app/shared/registry/integrations.registry.ts +++ b/web/app/shared/registry/integrations.registry.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { WIDGET_CUSTOM, + WIDGET_BIGBLUEBUTTON, WIDGET_ETHERPAD, WIDGET_GOOGLE_CALENDAR, WIDGET_GOOGLE_DOCS, @@ -35,6 +36,9 @@ export class IntegrationsRegistry { "custom": { types: WIDGET_CUSTOM, }, + "bigbluebutton": { + types: WIDGET_BIGBLUEBUTTON, + }, "youtube": { types: WIDGET_YOUTUBE }, diff --git a/web/app/widget-wrappers/jitsi/jitsi.component.scss b/web/app/widget-wrappers/jitsi/jitsi.component.scss index 553cb6c..38484f6 100644 --- a/web/app/widget-wrappers/jitsi/jitsi.component.scss +++ b/web/app/widget-wrappers/jitsi/jitsi.component.scss @@ -16,7 +16,7 @@ position: absolute; height: 100%; width: 100%; - background-color: themed(jitsiWelcomeBgColor); + background-color: themed(widgetWelcomeBgColor); } .join-conference-boat { @@ -30,4 +30,4 @@ width: 90%; text-align: center; } -} \ No newline at end of file +} diff --git a/web/public/img/avatars/bigbluebutton.png b/web/public/img/avatars/bigbluebutton.png new file mode 100644 index 0000000000000000000000000000000000000000..43f8aa5a79a037f38b201e46cff274f1b991d6c8 GIT binary patch literal 13303 zcmVNy)ph25@AbYfQg>@dOG|7*fY=enHaLmx@e;!f zhVC|yi9r(a5D)Xs1UnOx#P-CK%r}nnMH1MVjOlLh287syt zmU>^_{O8JM@L5(P`eSJ z)AJZKVCZ^6`(&!XUU767pY+SP@-J~C-!{qgvmNW&2J;If-ye$FAkp#nshX+S+ z_$Hjwfm0rBU)Qp25|2OSr=BtZx2?}N9D*1g zHADCe4B$6>MsOUn6@RSA&g?1~aHJ+koR5qQAHXT!!bu(N>sy|ka zO<7411I-`~zc=zgf^Tfu=!5fYUi6_zVq!*@FPOpFS7>!?-V! z9(-qEac&1@5>li9QevnF$2^3VzS6P2b@P;=mP11)9N_C(zM3elnsGfk;cn>H3xu&E z3q_Yw2No9DxxV$$Nh6nK11ALF z>u&j4qO7$3(*X4gus&yxTEEikF$N6(uC1vsXDyp;7M{@n5sVIT;#XMB;?w~wI+npv z09&TX03cvu0L&1}5iw0--T`RA&jgrNgOd^fjde7EJe-Cr1weJjTL0Xz*v5Io;QT+P z(<6V{vHny0v&1Ry!=@eJn^w2aPbE`dfl=~Fx=ZSCF;M8iiJ3Jpl+HPOu4!J}WQq%+ zyI7Z6L(4MTid$mphz2+Ulm&eZ)~dk~*1P(6VXhVHq$Z-$LJuASSpF^QfQ_|8fk~%| zAfi(i7-x!mt2n`7rC>2ed|y2MlXvKrxoCqH68Cq%y0vAZ8L`fRZxP&TVYYk zoPFk8bM9I5*eV`&I+ei`aD|O5p2ZR-CObojty^a-iTHL5WIfHsSHMWGmz@D;2MyYe zX@?9pxsI0}exQUU2LmgNW{HHSEi5*vLJEJgpXK48XQQA=8u4!2*_R=e{sy{vS^_20 z25{S&&O7i>e+B(h0sk;8j`8$lG+lDhVpzmg)(Up|RdlTX8|^q}-G;AbMG9n1X* zF(;yvW3A;F*CWm6tDL_KK^Mp=Tp{G~U$- ztYs@Ym0>lW0X431W%ainECN!jk)?GypXWrjQyuzT9vnD6Sx{)I>gr5!DYBXpQzc12 z1{gqwUVamC?oW1X_|)@L1@e>u+`8ue2C(-2RI2cI;!H%HN-S%Yp5^b< zL6HE1x0F3F@j;A$rGb$YHnuW@8QSuWH_QU@OyGGxI4ZK$zqL^oFg_SK9JIlJuLj6f zHPz1kvTUY-j=m#1V=tuM4G`DAmO^ zDFHmQsUak?z44V!cOgd50+4?+S&%0UaO>L6nP}u;#F1Bx4x>l{cVts~HaC0W`HRf? z%jfg&9H<~RkZyLaPkd}R!c+xNpbw+0v~35*69!U5cZTBAY$Q3|8nVtEz+_$fXqaNK zv$&%=IRqyA(Aq$g>$u;rj|O^0b)}gzceW{jYbbizgFHHV2q6Fajty=9IvJ2B1#sKi z&RPKXAd6*yEB-Bw?gXO#m#$n4|8}0WUPD&0bz3ByXhD{Mgy>fVpb9Mp0gix$)PsBABGNjY{p&CBcv7zfLm_=W)Tdk&VqvC>xYL?<>LH2I}(k#;?kw& z%%yX@zGX0#ZySn4MFIvHmTySxW{S!nVEo`GA}kz+f(c^}k{H{W0lW z+34n|6za;$mYJo^vyg=eyFikm$%rMR1IWrA8X9zNmBel-NI{8(OmRGB{McEn;cG;7 zt~Q`z^s!3~dP7VY?JZ@+QUOebM|Fg2EW=Agx{dCQnzHgzvvk=aQwj^4C7?3A5Z&Ae ziT9C?^=*4|1=+>Yay#w3i#~BX45}}3>5B8|Bv*PVf){7Os3J?5Zq(5Km36EwRtYVV zh^;_6u;=L8=D^-p%>KRGO!v_)WMYcoKG3MPq06zYU7ISjpRn)jjhiv7-&p@vj5#0O z3dC`kotEYLH+?-}Fvrwgn{vFwr)|LkN<2|?aICw>RM%9&7!7nXsjh;;;@RlKGgn-E z-9ww6dl>nv?6;f&MoG<8&=3Cyy0?V%sEjmnBUYY|0LD4?QDr+g$F}a zD)6x`Y9&6h)~UK3Jy61|4O`{b$h4cNwE*Nn)7#T)YU^q_2qfx``nDidv=mHEY})wn z6WPNH|2mh0A)4nPkNZ6UJcp7Z0xs#mbIzJ;E?lwD8%~lm_la|7K{MEn8KQ?tJkOs4 zw*g_x0t5r<>t~pA&OO(hb=H|?|Gw>J+nX=p0V}}fau2d3oKU=uCYEcTi40tCCe}R z_^&r@eB||PamyOuwmUjgxX8LxLE-x-kfFhyKeygoaT(Q{Q;BI{AqP6wS*Zjp1Fc0V z00O}C=GK}S_2r@auB@yyD^{$4j2|+uyz(?2usj5^NCt^9d%Y_|%unkp?oZ+(+;4?d@YPq#ABiFetcEgvvUvIPaqC9s!e&W&?8807LKI z4Ca0wIsx}*Z8Peen(8uh^{TVDe8vVbVxmSN!zaQP>I{h9`SWT3a7D&F)2ME4UW&mp zYF_#EQxIKM02+E3=Xl)~s_ck2$m0FhxFAPVe2=AL!){U_`Xko23rUVu#}$2G>D%MS zdf0_3FE3L*@+by)VGaD_`718@#KX|XQad~*)>#3J%+NwyxD%f$(2sQp-8an_S%ug# z#h3})AU7HY(Th_xs)acurm5!XjQO=@=8VcQZ*a(Y zd{Y`gB;ve24O9e&AeDt~4Is%mTVIx$=#vIk0@+NGRSi~0+cx>HGP)$(98DKa#5@}) z8Z`uR4UDeBLJDD8_lXQv5N#O{*a3iFoTx2Zx`Zw?6---F--v>S1M?d;eGY9oq-AA|iYtZ?_ z=u`?CaG`*{j#$@A5phxX(V%uygI~J~uF=4N^n= zJk~HGg$TpfQI+GK^uDR;5P=s)oV}mOylh*kPja|YWBXn&&RBZ zU_`W3vS1!Y(cKfZKQ6#6Ydgxp-g{7NOa>OWMSn|BFLfbuy0no6tUFY*Qdq422FDVs zEVH2$!1+QiT^$G)WpK9Yck2wJ@$& zG`o6aVW(xLuowV(M(FQOJhK~NziSs_+j1BZH>0!~S7QjHaQ_R@2n4}i#V{3o~A~`>OJN|&S{_)Wx-R9sy zlvisOgLs%pfx0*I;hX`6F#dEwLD3>Hti*~t8|C6jPUStZAy|yw$wZp7Zk|CCnVkl`*&H7Z?1{wRrkP39T4mW1Y$b60yL+#x? zdpV>j|8|(b6H4E8^BtXb>SJV!WiZ%VkK=B$L2o-k3A{NltQrw*8p1RV4X%hl_D$VL z=kMe?QAe?!lYQ}jvlY>D|7V=B2m$&8u6GO|6Z4c9c9uBoVq8WP4 ziyPRVqSoUudzM2#OU zmc#Cia;@b27=w%kkQiZ;b$@bsMt!UpT>JJNVE4)A0IYvZEWW*M?fn^XjSL1O-b@u? zw^{lJh38ET)q!CZ=>+fV%J1WdJ1e%8tKyiHD}yQHGJDQ!48A@qC6t(!c(=GZ-i_0) zQN}$eBg$FQfh%VN7=fLYvCSWFrkP>M_-Oe$qG!Z zVD{2h0G+#N!3>wSQ`Vy}He{u0i#2M!3bU2ylpsWwZjye0rp8A2g9Dyj-aul2lu@Ky zAUQ@1rl|W9V!H$SMCsaz;1?DjJkSLLGswd;8IuLb?wQ*r1ekO$7+(-nU!={<`bxx{ zOAJkBw-GC?N0lg6BG-yvtpgp{q~F8}^@f_Qr^xLw?HP|iFZ;APXMM98|jj@Zo~HPU~J7z zC(<9+?SxJ1bo#?6&TbeP4$^^@l_h3IEd@0s%xLvd{0w>PqJ-uUv2K(kKoIB;uNv1b!{6C;NYH zkAS9o9zyJPc!au*B38i?64`(=RmTv>J&0@_J9gYuRZ&Wjc`VKUyKDd3KY#tFU-_t2 zuk!1yfAT{!bhPV()IU0E)qubTEsH=)8w9mc1hB4xwBG|C)Mal7Y|bE2N7_kX%v|nJ ziDKFF@&TCqabDjFZC#0?`nj_Pka?k5UlRo!Y)H{=E(Gi6>xW_B5?{1WQM(?vDZ3Ch z+_CeZInb3hM~)@Tk>fN|kb)7^Z@S&7f-|!n$u=E6bQC#bB>31MCJDom_o`Xl-OV_Q zr@(lChq}8K(@yIexpW}r5j7%(N21MVi}chC0g%KngXTbVX|(WMU6p%vKoc-@bNK>n z({PxV3)|qP8?Ho7ncVm3-FkYCoBjLunQd=vGcUgIl6m#Dy=MFFQM0d$Dj+S;Cu$>r zXU}Ou%@%n_w(9y&Rq4#6}jDdOyEm`ENHSj|P$~a3)D3MRmA_s&pFDAE}S^cQ0 z8XQZ5nxkNHtu2rlz)vr3~w5B4;A;e*F0dI@gak?0YZq+~+UR z;q}nH)bl~qN-SX;q6`8VDzG04_XU=-ge`3_L>5euzxj!19<}%%Rd8;41?9shcq=I> zF)Pl$(A;syXCOg7U{n)Nn5>RmalSrIbf}?k#dGs^xrI^MR(^#VT@OoqI>?ux1l2kcr~6A zy-_Xmhd%rfGjIL^^PTT}$CSKs%)IdqC3+@aP;D~ziuJ@yH>y6!9i_!-EOun-8_X3m z^YG7oj=^a{d!g39MJUH9#teKLPY#$CO)zLSMx#lgWO~TOa#ppECgo>E^8>ja+OlxX2GA2m^NhIihTRp6R_pXn#x+P;X=3mLyDSHO zU--5;a+FpRJbBwi7hMD^`%<&)3`zvpz{+%NWHH%g0 z3$mGXa9n6oGd7w`kJd8D{!mOfRKw-OHh#-jjM2Ezm6T*`{NP_+GN1m;H_YpAygMCz znpK-bl54K{fLXSr-Za&dWj#&$81$ss5JVE$IN*{_&=LY?V{BoLkui;-qPpS&7SlSX z8!CIvC@XbQESFshO$3vSQ(US}rR?$tSXWxu`zXf!Z-iCdcH4UM&CXvSe;Q<7vO;Hy z4ywaa4VPYanVB~m1?yCNJPj0nFHqHE-hUrNG*s>sVcz@U5~Pkho<7*u2y zlNkybV7hoM5Xa#fsHm6@&dFO(VIcZzs76Y#uI1swJ?1aI@=xZbKUilT`|(SrCnCzG z2*+`oS&7o31#_!REp#yfJhdR(K|_QZ#31%D47Q^vjGJyKNTd_z;~%h~$r7-U5RPj) z@bnA>8*3(g;*tgt|JM7pv3{>R*T<%V&?Q^J-r>ou+gU{O^=JwHa0ICUgO~>Hi<{mt zcmCIQ)4Zg~eCT)1H&N^W#+k;pEn1OOmVE5^9D3Mm--+) ztCVMc8dHl{bYe05ri4u7=3Ag_i?g7^Ss#@s2(1z2zzYokIo3_e8fVRld}GmeD2N*p zA5@@GO(L6s-ulK~^V*ij%-?PJ5sLO_bNMeV>2w~ZywVJf61PM#ULz11Wu>SCuf$^G z`E10b#Y>vajGE^Vv$hP*bFxGD@-DzbC=xk_Y1v!q4Ng38z>@envjD+9l&%F2MEeS0 zWQ8JF!a4zvGeVk?V*u>t>2N}U2aKc+!&xO}aGtIN#iF%n8VdwEh8AHXOPjV=U)zoF z=1xv3k-uMv$^ly9sT4~pN+R7lVF@g)P-L1HHJLkZyVfjSGAn1C8yXr>Zd`)O7%DBw zYC6~$*B}{ByI555F(S1FQ3$C4W|f;xG#21bxmFAm-K7Wi#dPe$3Zn(oFgrcV{Q!u} zzokr51YBEFY(@QK7@R8`WH1oe^pTA^r2AOEIohp_6(=LG@72w_%nL7XH|<|vjfrxh zqKOFspE+|Tq(~8}Pf+GNK+FG6cuLFx8eEGpr$u>yQ##WclJb1crfQ_&TudpqV55_) z-^#{|N)<{d(igU?k{c;-ld6BNUUY#EU-#q!9zn^fEtbn=>PkLclg&9C;={xBi&s_I3lJj;(6D3^q#sFiAG5jH#<*$d7 z&CvLz`J8x`L_q+uPAWKts+slnIH{Cy;Y?z(ukHvYOizZv*?>1S$0Q(PQ_Nx=HOi)K z(!-RHutfpcoZ;n45N!A5CDH(i>7XsbOC2o52PM$KR8t$?WHr@bF)t;WBiW*y){qKW z&<<=~J1YM`M=sqp*<)$eeJ={$a@|NePJK_*!bd*o2S~r>&S1IqZ0M_WEuF@%k)bb6jj~lm z{}iyrVvcQ_mL^9DrQUEH=|s}YVR5VtrQqEOcG2v>2n((1rd%oSL%RuEda#h5&o`2F zqEVOHIVm$h0Mlz$ zgIjTtAryo-_b9$XBdUA#vVCDg%7N`H=#b&%^!e$cgR{mfmjk1$)#8_N7Ln{gcUH5% zgbQjEvW^cn7wkAjztYrkwFe>^)qx4WaHiI`fn}^kY-mkJWrS%V~=;M#)v(?vw* zg5CJXav(xc(2_Q+TNRT%M2@)vJQJIOIECDREKX-%lRUDI(nj#?m59dq;kBT`09(;( z#des*qO$|{C#b;+_GMVzPGn~jjhed7J=rZqL^9t_L?nmk7ze&Ju53BKechiWd|-T~ zM-u=T%kl85d|X(LbvWrzN;Zs9KsV6>n;9b7uS6G!Yn!yNp}^lNa5;pi#I1nqehQo# zZ6!N+EP@JpE50dCMWI#=unRvQfE8_C8bzfv>~YlW*ybq@5x3-hwjx(Tmt*T`8hKV~ zN^njY#547=SAq709$RtVaToqbZ$z;)Ea?PW(iUK>a7rwunT24WEMyWF ziPAckS8$c+jEj$EC)vW`((KN$wn^?e)@)nPJkCx>3BQ^y>(;n)6)QbUNysq^xBvhX zok>JNRK&SfqWal4?nF90uREBYol=-p6?VyCJudM51xRO9s?nB6?87;Gf>ITVZNj#O zWfrV&6NG|(P+ zEs}%VV^NG%nRE;7$NyV2~}bqs?sD50M$&Z2X9RlV6w z=K9~i)+}qDoy~29j_Eph!0g#K2vt|eg?HX$a6krm-M~r?#SJW;%xvo^(IPhnGZ)KE zF=EgZj1q!j_9tedC4M0{-~qmW-Hjud_WvXF^)+$=jfe`W285+HFo3s`xy;X0uvYg# z3hnRNbJ!d{(qroD#(r-@T+dR);`;#dm8(`^(QG?NCJU@wM*ya4R8`gF46K~6ckf7aW09<^X#X4`sT7Fp+G@O|Mo9y*p|GssuM#84UuOW!KP1efIN)vs zJ!)yl5Wr-i5y(Bg^!9Pv_+-R!TrZ^%OWnDv%WT>DE?$e58^F?DQBkp}tDF9*D7m6c zeYoZIx6A>|OK0{<6iY>{WFQH+wtt1T!dhan?SC?PXxdFRW?O__Xvqey`C{@!1=5k2ok zOd998A}$PG$_IdO2mR@=lj(RL-B}ae_;s(N6 zVX_+-Za|RlJEPXFaK}8y7tAlCZ$IzY(DKfh0P~J?`Uym}-t@6){6vON)2s$|(Wa}k zhOV)1|50=QgTMTZ!I#+k!1tcUYIw!A=-mZ5DBloreLV%qjcyM@l@LkQ~6hzFdIgQiGV(Nt6{y}rk|9IFOI(+=J#`xLiUNrZ1 zyZ~IVT9I8+(|V|0*Q)rlcYiF976bQTscL^Bay4L8yE3E)eQ1)I&Ihc&e_dO~w2Ro4 z6%nMSd6gX`*QFt1acyl4zxq@MIHnqfBKUQ$Y~E@9^e?{)%RTpLB2#@o`Lwrn>o)WG zFMZz}I!enLk|Cr*ltfAv$yZF?4Su*6tP)nRTyqjb4U4_@}sg z9ee4_g!wzHTluaS2jUvdncK((Ueg6K$Cu%n{qV;xn?L*F_fGnfNVx*M`L%84KYi}o z=B-_Why(&$W}XtsY{)o3JxeufMfV23G4pXw)68m~q%Qe&PCA$b<@3MVzP=?^b>MVi{7QbWo2cip^=vLn9c>hEXWW4^K<62f6~Fm*XgkR@~O?{_PZW1Ti@=1 zu5@8j#xlGHsE1TF=b=nd(O2m)D&YUMV?ok7Bv@b$s5`e)qf=F2bs z#AaA$*MY^g`fcCk$Uh#X*eb2U&G)Y?z9~Y2+9dJz#+&=hQ_s9%&cHh(=sm@!;zlzL z@B8M@%{_np7~^98P3pR% zV?*n!c>(p zqsq*}M~9H{Hdv_1;YV?eFOBye;R&gS_c-8f9_b&T!E1syklNt(J6Kg@&+fhKCr<~9 zH4>EQ7=Xm6tF17XU%bS8E^kH2DG-SVz| zEr45;+ljP5+UmNij2bJFv`YU9%N_Xip5k(ps zjp0pb7eI?VfxDsA(_D#+)xld{dlQRc9y=i*)3wRC8^D4iR4Q75MN<~cn}OHE*P9u2 z6}*%%{hqlIom@4A?nJhBqzlMxi5Nh1Jv?JSiV?bx^)aw-Z9m9=dwT~t9~%PJ0TXyG zO5BR$Dps>LwER7Y7`O5=h4AB6+-tUMeuGy%%{A3G(anvwUyM9{y)9>uoLJN+F@EBPIJN{5(6T3PzvxCc z7_1L#hXU0=@w+0()uHjA7#OTpB&<|}N9$jm*QG-u;B$cjj$b&d&NShb3K`&z3_lIM zd~N&Rw@kPoXY30N<2n{1_-_F2djawX1Y}|py<`52h4ajtZ)~4deH^_fR~m|%B<%=r z&$vz4FQ(q!I2(C@M91U^2bo@`=VjY`Wdxm-aJZ#PoL{FG23In z&2U4a;43;ySkRy2*sZ{~Wi&b(r9Fs&D{p*>gmH-D94bAkttvC~n<#_l zB!@d6{SU!4`rjd%UzoT*>=9W5Oa}wx&A7sy7~ue$=AT+)i$n^AF&mp2SVvB0ygK&5 ztW2{)R7Zx~Pu}8*>OCzQeZz`@{F?<%@XE~D%5~kLG9dhbA(Whe7hq$qOH#oaRYhje zoO&ApbK`-Y&05VP-%KP5He?Gf9hdX6YnwJc{Oa&U0uvSw7ZNR|+w0Hayy!PiRIF-QUF!-lRGIp69eYv7j#!1HgOCQA2;sE@s_3`940 z&>ZPms@Pn%U?$#fPGzu8Q{YRg0Fw|#hMt4h+uFXNC3B^=aS+dm!NDL;+%r5f_)scE zj<+oSVIwSOEShg-&a6KHOpak~$XE%)o+F`9%svL)tRE1LwLWcU_or#$GB1)XC~hrg zCoNo)E)?n8Wy4A9TcvdQx^95DMiK8wSHL)zAK)ZyVDf#`V8$nc;O)mCJa&-(UT@9@ivr`|`7Go(DlY|jKEG4O&y zhLjv5j1xw>6Is`eWg)v`JwqLsp`MJktN*R8g`ZJ&j_gpOX$H9ESWS=m&Z<3|NWgXk zE6IMObA9Wtaso&mC7+Yz9Jo#kaWju1Cc0d^PYg0{aL4vtX5YI9>@0~;$LD!fos0JE zjRBGm0}5LlOEvhyWMQ1cO1nQ+ZOQ;rZTR2Ze$_@uVu-62b6wWC5X;T;XCMqo%niWh zRBgiaBUF5A$NIK!<=JB)4?C|mTGq6miSk;2oLWjRiImZ20Zp6TyY}H@m)4&Y$Si)v zSfR!Q4c){|P} zE)PEl8|O19z_>3jV5Wk>ipeW*Q{Tf{O-tx))B>bf*#{3EGVi>-i>Gho9t_dCR<0P) zX}XjBE@5^=WXl*(th6dr7n&26N{mO_6y{HOT{Sq|btU!Jh{)UgSv6SdUwb3OU=I5Z zz+%1&;Qsw&z*WP2vdwU$>nz;ZcTqTh4!@q(CM`X4=~bw2zqONJi<-4w_99tCV@S~{ zeXGHAY!eWqkA;Euj)L*+NUXCzu_8Gd3<>0LMDJ^f5~vRgEM|+q{h0?JmjI?sTgy)8 z_9E7BC&cnMCXb3TL8hWHi0fvE>uI2;IcYlO1FEE&>n&*1Gl`vqPc8@mL!-hZ4kkqn`5 zV=&c(2RhtMuy#8Ps#Y*Jqi}9AVCNj@nCN1gHa>EE`O4q_K7cI;n=gS*L43`=`qYXf zSdNHa*VR|^A}_steY|9o#FlOQ(N1>2X(TctR&^{u~- zX0-yJ_iG7a+vU0`2%;*mb}=GTR7=GRg7f7>Z%|P~F$FQ6!K!rsp=150vJOE`goEsl zk!{(YI`MIMtLY!$Vg46%OHGhVwjc}Olxp0)Yp?0FZfT|~MV)Tf1EHy^`}Q3~ zH6?{yo!j6=wgN76=Ze`OYj=W(lpR6gj`QG;-KLf>yc{DArRgLyG25>Jz!G~cuz~cp z13Sq!2MnnvFdY7K`?@yN`AqM2LI9RWfGhPo=)W%~lZ7koo2tXQS@bY1+jJD7`{2PN z5Z%Y^d--r8x`#oxwvjN-u`vWx)Q}pBwOZ`!u84x`=(uZ6gvXXTamCg`V73EvKEwo=t2`2nDrs#|vKCt<)y+J6gJ z)|SG2AgUsTKBd~InyOMRU#0LO9q6-_uGtWWZ=wUW3RS?gV=2KvAVULRhys* z60_uvQOz|iWI`pWmgaBk3s}-61uxQKwd+-~# zoZMHDpGp7+jP7hk&t4B?Zv^?47jTq^M6uPqdW-LDeT){M&QM|s%9G2nDt;XiyV?>C zgN#8Uj2;=;jTRn9!}lRX`)?22du#YrpeK<-rwoImebBbLy%ZG{?*Wgl$G-Q1Axk8# zjgVf}U1rG_)s(^Mxuh`JxTAHb&?r))-Du_K`04xj_haqrTk&quQ*S$E0oGjwJv{?- zxePM)eGr;gLANbOSC=4j#fBM)-tj=pz1v0Sljub{k6%A9j2ZNa-N5!q0R2-KcTab2 zXx)-~+b8yn(*j`KCGe$75ptZ5pz^r@^;*d7v+)q~(eVxF_%f Date: Thu, 23 Jul 2020 23:03:32 +0200 Subject: [PATCH 2/5] Add API backend This commit adds the join API endpoint that will be used by the widget to transform a greenlight URL to a BigBlueButton meeting URL. The full flow is defined within the code itself, but it roughly boils down to taking a greenlight URL that the user pastes it, sending it to Dimension, Dimension making some API calls to greenlight to "join" the meeting and retrieving a join link, before passing that back down to the client to load. Unfortunately, while BigBlueButton's server has a nice API, it's useless to us if all we have is a greenlight link, so we need to do this hacky route instead. --- .../DimensionBigBlueButtonService.ts | 207 ++++++++++++++++++ src/models/Widget.ts | 7 + src/models/WidgetResponses.ts | 4 + web/app/app.module.ts | 2 + 4 files changed, 220 insertions(+) create mode 100644 src/api/dimension/DimensionBigBlueButtonService.ts create mode 100644 src/models/Widget.ts create mode 100644 src/models/WidgetResponses.ts diff --git a/src/api/dimension/DimensionBigBlueButtonService.ts b/src/api/dimension/DimensionBigBlueButtonService.ts new file mode 100644 index 0000000..0a4f141 --- /dev/null +++ b/src/api/dimension/DimensionBigBlueButtonService.ts @@ -0,0 +1,207 @@ +import { GET, Path, QueryParam } from "typescript-rest"; +import * as request from "request"; +import { LogService } from "matrix-js-snippets"; +import { URL } from "url"; +import { BigBlueButtonJoinRequest } from "../../models/Widget"; +import { BigBlueButtonJoinResponse } from "../../models/WidgetResponses"; +import { AutoWired } from "typescript-ioc/es6"; +import { ApiError } from "../ApiError"; + +/** + * API for the BigBlueButton widget. + */ +@Path("/api/v1/dimension/bigbluebutton") +@AutoWired +export class DimensionBigBlueButtonService { + + /** + * A regex used for extracting the authenticity token from the HTML of a + * greenlight server response + */ + private authenticityTokenRegexp = new RegExp(`name="authenticity_token" value="([^"]+)".*`); + + // join handles the request from a client to join a BigBlueButton meeting + // + // The client is expected to send a link created by greenlight, the nice UI + // that's recommended to be installed on top of BBB, which is itself a BBB + // client. + // + // This greenlight link is nice, but greenlight unfortunately doesn't have any + // API, and no simple way for us to translate a link from it into a BBB meeting + // URL. It's intended to be loaded by browsers. You enter your preferred name, + // click submit, you potentially wait for the meeting to start, and then you + // finally get the link to join the meeting, and you load that. + // + // As there's no other way to do it, we just reverse-engineer it and pretend + // to be a browser below. We can't do this from the client side as widgets + // run in iframes and browsers can't inspect the content of an iframe if + // it's running on a separate domain. + // + // So the client gets a greenlight URL pasted into it. The flow is then: + // + // + // +---------+ +-----------+ +-------------+ +-----+ + // | Client | | Dimension | | Greenlight | | BBB | + // +---------+ +-----------+ +-------------+ +-----+ + // | | | | + // | | | | + // | | | | + // | | | | + // | /bigbluebutton/join&greenlightUrl=https://.../abc-def-123&fullName=bob | | | + // |---------------------------------------------------------------------------->| | | + // | | | | + // | | GET https://.../abc-def-123 | | + // | |-------------------------------------------------------------------------------------->| | + // | | | | + // | | Have some HTML | | + // | |<--------------------------------------------------------------------------------------| | + // | | | | + // | | Extract authenticity_token from HTML | | + // | |------------------------------------- | | + // | | | | | + // | |<------------------------------------ | | + // | | | | + // | | Extract cookies from HTTP response | | + // | |----------------------------------- | | + // | | | | | + // | |<---------------------------------- | | + // | | | | + // | | POST https://.../abc-def-123&authenticity_token=...&abc-def-123[join_name]=bob | | + // | |-------------------------------------------------------------------------------------->| | + // |===============================================================================================If the meeting has not started yet================================================| + // | | | | + // | | HTML https://.../abc-def-123 Meeting not started | | + // | |<--------------------------------------------------------------------------------------| | + // | | | | + // | 400 MEETING_NOT_STARTED_YET | | | + // |<----------------------------------------------------------------------------| | | + // | | | | + // | | | | + // | Wait a bit and restart the process | | | + // |------------------------------------- | | | + // | | | | | + // |<------------------------------------ | | | + // | | | | + // |=================================================================================================================================================================================| + // | | | | + // | | 302 Location: https://bbb.example.com/join?... | | + // | |<--------------------------------------------------------------------------------------| | + // | | | | + // | | Extract value of Location header | | + // | |--------------------------------- | | + // | | | | | + // | |<-------------------------------- | | + // | | | | + // | https://bbb.example.com/join?... | | | + // |<----------------------------------------------------------------------------| | | + // | | | | + // | GET https://bbb.example.com/join?... | | | + // |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->| + // | | | | + // | | Send back meeting page HTML | | + // |<--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + // + @GET + @Path("join") + public async join( + joinRequest: BigBlueButtonJoinRequest, + @QueryParam("greenlightUrl") greenlightURL: string, + @QueryParam("fullName") fullName: string, + ): Promise { + // Parse the greenlight url and retrieve the path + const greenlightMeetingID = new URL(greenlightURL).pathname; + + LogService.info("BigBlueButton", "URL from client: " + greenlightURL); + LogService.info("BigBlueButton", "MeetingID: " + greenlightMeetingID); + LogService.info("BigBlueButton", "Name given from client: " + fullName); + LogService.info("BigBlueButton", joinRequest); + + // Query the URL the user has given us + let response = await this.doRequest("GET", greenlightURL); + if (!response || !response.body) { + throw new Error("Invalid response from Greenlight server while joining meeting"); + } + + // Attempt to extract the authenticity token + const matches = response.body.match(this.authenticityTokenRegexp); + if (matches.length < 2) { + throw new Error("Unable to find authenticity token for given 'greenlightUrl' parameter"); + } + const authenticityToken = matches[1]; + + // Give the authenticity token and desired name to greenlight, getting the + // join URL in return. Greenlight will send the URL back as a Location: + // header. We want to extract and return the contents of this header, rather + // than following it ourselves + + // Add authenticity token and full name to the query parameters + let queryParams = {authenticity_token: authenticityToken}; + queryParams[`${greenlightMeetingID}[join_name]`] = fullName; + + // Request the updated URL + response = await this.doRequest("POST", greenlightURL, queryParams, "{}", false); + if (!response || !response.body) { + throw new Error("Invalid response from Greenlight server while joining meeting"); + } + + if (!("location" in response.response.headers)) { + // We didn't get a meeting URL back. This could either happen due to an issue with the parameters + // sent to the server... or the meeting simply hasn't started yet. + + // Assume it hasn't started yet. Send a custom error code back to the client informing them to try + // again in a bit + return new ApiError( + 400, + {error: "Unable to find meeting URL in greenlight response"}, + "WAITING_FOR_MEETING_START", + ); + } + + // Return the join URL for the client to load + const joinUrl = response.response.headers["location"]; + LogService.info("BigBlueButton", "Sending back join URL: " + joinUrl) + return {url: joinUrl}; + } + + private async doRequest( + method: string, + url: string, + qs?: any, + body?: any, + followRedirect: boolean = true, + ): Promise { + // Query a URL, expecting an HTML response in return + return new Promise((resolve, reject) => { + request({ + method: method, + url: url, + qs: qs, + body: body, + followRedirect: followRedirect, + jar: true, // remember cookies between requests + json: false, // expect html + }, (err, res, _body) => { + try { + if (err) { + LogService.error("BigBlueButtonWidget", "Error calling " + url); + LogService.error("BigBlueButtonWidget", err); + reject(err); + } else if (!res) { + LogService.error("BigBlueButtonWidget", "There is no response for " + url); + reject(new Error("No response provided - is the service online?")); + } else if (res.statusCode !== 200 && res.statusCode !== 302) { + LogService.error("BigBlueButtonWidget", "Got status code " + res.statusCode + " when calling " + url); + LogService.error("BigBlueButtonWidget", res.body); + reject({body: res.body, status: res.statusCode}); + } else { + resolve({body: res.body, response: res}); + } + } catch (e) { + LogService.error("BigBlueButtonWidget", e); + reject(e); + } + }); + }); + } + +} diff --git a/src/models/Widget.ts b/src/models/Widget.ts new file mode 100644 index 0000000..3a26ef2 --- /dev/null +++ b/src/models/Widget.ts @@ -0,0 +1,7 @@ +export interface BigBlueButtonJoinRequest { + // A URL supplied by greenlight, BigBlueButton's nice UI project that is itself + // a BigBlueButton client + greenlightUrl: string; + // The name the user wishes to join the meeting with + fullName: string; +} diff --git a/src/models/WidgetResponses.ts b/src/models/WidgetResponses.ts new file mode 100644 index 0000000..854ea31 --- /dev/null +++ b/src/models/WidgetResponses.ts @@ -0,0 +1,4 @@ +export interface BigBlueButtonJoinResponse { + // The meeting URL the client should load to join the meeting + url: string; +} diff --git a/web/app/app.module.ts b/web/app/app.module.ts index e217674..c7a8def 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -119,6 +119,7 @@ import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.comp import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component"; import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component"; import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; +import { BigBlueButtonApiService } from "./shared/services/integrations/bigbluebutton-api.service"; @NgModule({ imports: [ @@ -236,6 +237,7 @@ import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/big AdminStickersApiService, MediaService, StickerApiService, + BigBlueButtonApiService, AdminTelegramApiService, TelegramApiService, AdminWebhooksApiService, From e3f27156e05de60e4ea19fc64ab93f83a1699c74 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 23 Jul 2020 23:10:27 +0200 Subject: [PATCH 3/5] Add the client-side widget code Here is where the actual code that runs in the widget's iframe is. This includes the HTML/CSS stuff, the definitions for API request/responses, some routing and the javascript which makes requests to the new /join api endpoint. --- web/app/app.module.ts | 2 + web/app/app.routing.ts | 2 + web/app/shared/models/integration.ts | 13 +- .../integrations/bigbluebutton-api.service.ts | 16 ++ .../bigbluebutton.component.html | 25 +++ .../bigbluebutton.component.scss | 32 ++++ .../bigbluebutton/bigbluebutton.component.ts | 152 ++++++++++++++++++ 7 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 web/app/shared/services/integrations/bigbluebutton-api.service.ts create mode 100644 web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html create mode 100644 web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss create mode 100644 web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts diff --git a/web/app/app.module.ts b/web/app/app.module.ts index c7a8def..9d9dbf7 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -119,6 +119,7 @@ import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.comp import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component"; import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component"; import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; +import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component"; import { BigBlueButtonApiService } from "./shared/services/integrations/bigbluebutton-api.service"; @NgModule({ @@ -149,6 +150,7 @@ import { BigBlueButtonApiService } from "./shared/services/integrations/bigblueb FullscreenButtonComponent, VideoWidgetWrapperComponent, JitsiWidgetWrapperComponent, + BigBlueButtonWidgetWrapperComponent, GCalWidgetWrapperComponent, BigBlueButtonConfigComponent, RiotHomeComponent, diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index 1e36590..8bbd11c 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -2,6 +2,7 @@ import { RouterModule, Routes } from "@angular/router"; import { HomeComponent } from "./home/home.component"; import { RiotComponent } from "./riot/riot.component"; import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component"; +import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component"; import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component"; import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component"; import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component"; @@ -292,6 +293,7 @@ const routes: Routes = [ {path: "generic", component: GenericWidgetWrapperComponent}, {path: "video", component: VideoWidgetWrapperComponent}, {path: "jitsi", component: JitsiWidgetWrapperComponent}, + {path: "bigbluebutton", component: BigBlueButtonWidgetWrapperComponent}, {path: "gcal", component: GCalWidgetWrapperComponent}, {path: "stickerpicker", component: StickerPickerWidgetWrapperComponent}, {path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent}, diff --git a/web/app/shared/models/integration.ts b/web/app/shared/models/integration.ts index a841e4d..6d8a444 100644 --- a/web/app/shared/models/integration.ts +++ b/web/app/shared/models/integration.ts @@ -64,6 +64,11 @@ export interface FE_Sticker { }; } +export interface FE_BigBlueButtonJoin { + // The meeting URL the client should load to join the meeting + url: string; +} + export interface FE_StickerConfig { enabled: boolean; stickerBot: string; @@ -88,8 +93,14 @@ export interface FE_JitsiWidget extends FE_Widget { }; } +export interface FE_BigBlueButtonWidget extends FE_Widget { + options: { + conferenceUrl: string; + }; +} + export interface FE_IntegrationRequirement { condition: "publicRoom" | "canSendEventTypes" | "userInRoom"; argument: any; expectedValue: any; -} \ No newline at end of file +} diff --git a/web/app/shared/services/integrations/bigbluebutton-api.service.ts b/web/app/shared/services/integrations/bigbluebutton-api.service.ts new file mode 100644 index 0000000..4a23cfe --- /dev/null +++ b/web/app/shared/services/integrations/bigbluebutton-api.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@angular/core"; +import { AuthedApi } from "../authed-api"; +import { FE_BigBlueButtonJoin } from "../../models/integration" +import { HttpClient } from "@angular/common/http"; +import { ApiError } from "../../../../../src/api/ApiError"; + +@Injectable() +export class BigBlueButtonApiService extends AuthedApi { + constructor(http: HttpClient) { + super(http); + } + + public joinMeeting(url: string, name: string): Promise { + return this.authedGet("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise(); + } +} \ No newline at end of file diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html new file mode 100644 index 0000000..cf07776 --- /dev/null +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html @@ -0,0 +1,25 @@ + + +
+
+
+

+
+ +
+

BigBlueButton Conference

+ +
+
+
+
diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss new file mode 100644 index 0000000..1918e2a --- /dev/null +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.scss @@ -0,0 +1,32 @@ +// component styles are encapsulated and only applied to their components +@import "../../../style/themes/themes"; + +@include themifyComponent() { + #bigBlueButtonContainer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .join-conference-wrapper { + display: table; + position: absolute; + height: 100%; + width: 100%; + background-color: themed(widgetWelcomeBgColor); + } + + .join-conference-boat { + display: table-cell; + vertical-align: middle; + } + + .join-conference-prompt { + margin-left: auto; + margin-right: auto; + width: 90%; + text-align: center; + } +} diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts new file mode 100644 index 0000000..a423e99 --- /dev/null +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.ts @@ -0,0 +1,152 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { WidgetApiService } from "../../shared/services/integrations/widget-api.service"; +import { Subscription } from "rxjs/Subscription"; +import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api"; +import { CapableWidget } from "../capable-widget"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { BigBlueButtonApiService } from "../../shared/services/integrations/bigbluebutton-api.service"; +import { FE_BigBlueButtonJoin } from "../../shared/models/integration"; + +@Component({ + selector: "my-bigbluebutton-widget-wrapper", + templateUrl: "bigbluebutton.component.html", + styleUrls: ["bigbluebutton.component.scss"], +}) +export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy { + + public canEmbed = true; + + /** + * User metadata passed to us by the client + */ + private conferenceUrl: string; + private displayName: string; + private userId: string; + + /** + * The poll period in ms while waiting for a meeting to start + */ + private pollIntervalMillis = 5000; + + /** + * Subscriber for messages from the client via the postMessage API + */ + private bigBlueButtonApiSubscription: Subscription; + + /** + * A status message to display to the user in the widget, typically for loading messages + */ + public statusMessage: string; + + /** + * Whether we are currently in a meeting + */ + private inMeeting: boolean = false; + + /** + * The URL to embed into the iframe + */ + public embedUrl: SafeUrl = null; + + constructor(activatedRoute: ActivatedRoute, + private bigBlueButtonApi: BigBlueButtonApiService, + private widgetApi: WidgetApiService, + private sanitizer: DomSanitizer) { + super(); + this.supportsAlwaysOnScreen = true; + + let params: any = activatedRoute.snapshot.queryParams; + + console.log("BigBlueButton: Given greenlight url: " + params.conferenceUrl); + + this.conferenceUrl = params.conferenceUrl; + this.displayName = params.displayName; + this.userId = params.userId || params.email; // Element uses `email` when placing a conference call + + // Set the widget ID if we have it + ScalarWidgetApi.widgetId = params.widgetId; + } + + public ngOnInit() { + super.ngOnInit(); + } + + public onIframeLoad() { + if (this.inMeeting) { + // The meeting has ended and we've come back full circle + this.inMeeting = false; + this.statusMessage = null; + this.embedUrl = null; + + ScalarWidgetApi.sendSetAlwaysOnScreen(false); + return; + } + + // Have a toggle for whether we're in a meeting. We do this as we don't have a method + // of checking which URL was just loaded in the iframe (due to different origin domains + // and browser security), so we have to guess that it'll always be the second load (the + // first being joining the meeting) + this.inMeeting = true; + + // We've successfully joined the meeting + ScalarWidgetApi.sendSetAlwaysOnScreen(true); + } + + public joinConference(updateStatusMessage: boolean = true) { + if (updateStatusMessage) { + // Inform the user that we're loading their meeting + this.statusMessage = "Joining conference..."; + } + + // Generate a nick to display in the meeting + const joinName = `${this.displayName} (${this.userId})`; + + // Make a request to Dimension requesting the join URL + console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl); + this.bigBlueButtonApi.joinMeeting(this.conferenceUrl, joinName).then((response) => { + if ("errorCode" in response) { + // This is an instance of ApiError + if (response.errorCode == "WAITING_FOR_MEETING_START") { + // The meeting hasn't started yet + this.statusMessage = "Waiting for conference to start..."; + + // Poll until it has + setTimeout(this.joinConference.bind(this), this.pollIntervalMillis, false); + return; + } + + // Otherwise this is a generic error + this.statusMessage = "An error occurred while loading the meeting"; + } + + const joinUrl = (response as FE_BigBlueButtonJoin).url; + + // Check if the given URL is embeddable + this.widgetApi.isEmbeddable(joinUrl).then(result => { + this.canEmbed = result.canEmbed; + this.statusMessage = null; + + // Embed the return meeting URL, joining the meeting + this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(joinUrl); + + // Inform the client that we would like the meeting to remain visible for its duration + ScalarWidgetApi.sendSetAlwaysOnScreen(true); + }).catch(err => { + console.error(err); + this.canEmbed = false; + this.statusMessage = "Unable to embed meeting"; + }); + }); + } + + public ngOnDestroy() { + if (this.bigBlueButtonApiSubscription) this.bigBlueButtonApiSubscription.unsubscribe(); + } + + protected onCapabilitiesSent(): void { + super.onCapabilitiesSent(); + ScalarWidgetApi.sendSetAlwaysOnScreen(false); + } + +} From aed5fde3918c84f7364ae69c3f0444c00966b08b Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 23 Jul 2020 23:48:08 +0200 Subject: [PATCH 4/5] Drop m.bigbluebutton event type --- web/app/shared/models/widget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/shared/models/widget.ts b/web/app/shared/models/widget.ts index 0400c80..795189b 100644 --- a/web/app/shared/models/widget.ts +++ b/web/app/shared/models/widget.ts @@ -1,7 +1,7 @@ import { WidgetsResponse } from "./server-client-responses"; export const WIDGET_CUSTOM = ["m.custom", "customwidget", "dimension-customwidget"]; -export const WIDGET_BIGBLUEBUTTON = ["m.bigbluebutton", "bigbluebutton", "dimension-bigbluebutton"]; +export const WIDGET_BIGBLUEBUTTON = ["bigbluebutton", "dimension-bigbluebutton"]; export const WIDGET_ETHERPAD = ["m.etherpad", "etherpad", "dimension-etherpad"]; export const WIDGET_GOOGLE_DOCS = ["m.googledoc", "googledocs", "dimension-googledocs"]; export const WIDGET_GOOGLE_CALENDAR = ["m.googlecalendar", "googlecalendar", "dimension-googlecalendar"]; From 259650ec7e26f13117ff2558b2c63ad0fdac280d Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 18 Aug 2020 11:30:12 +0200 Subject: [PATCH 5/5] Ensure the iframe can request mic/cam permissions Found while fixing the same issue on Scalar: https://github.com/vector-im/element-web/issues/14901 --- .../widget-wrappers/bigbluebutton/bigbluebutton.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html index cf07776..c42759f 100644 --- a/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html +++ b/web/app/widget-wrappers/bigbluebutton/bigbluebutton.component.html @@ -6,6 +6,7 @@ allowfullscreen width="100%" height="100%" + allow="camera; microphone; encrypted-media; autoplay;" >