├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── django_bigbluebutton.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── requires.txt └── top_level.txt ├── django_bigbluebutton ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── apps.py ├── bbb.py ├── forms.py ├── locale │ └── fa │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200831_1635.py │ ├── 0003_auto_20200831_1644.py │ ├── 0004_meeting_is_running.py │ ├── 0005_auto_20200911_0547.py │ ├── 0006_auto_20200911_0622.py │ ├── 0007_auto_20200925_0959.py │ ├── 0008_auto_20201008_1609.py │ ├── 0009_auto_20201112_1032.py │ ├── 0010_auto_20201126_0923.py │ ├── 0011_meetinglog.py │ ├── 0012_meetinglog_meeting.py │ ├── 0013_auto_20201211_0601.py │ ├── 0014_auto_20201211_0920.py │ ├── 0015_auto_20201214_1423.py │ ├── 0016_auto_20210202_1546.py │ ├── 0017_auto_20210202_1600.py │ └── __init__.py ├── models.py ├── settings.py ├── signals.py ├── tasks.py ├── templates │ └── admin │ │ └── meeting_create_link.html ├── tests.py ├── urls.py └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Build related 2 | *.pyc 3 | *.egg 4 | *.[oa] 5 | pip-log.txt 6 | docs/.build 7 | docs/_build 8 | build/ 9 | log/ 10 | src/ 11 | tmp/ 12 | db/ 13 | 14 | # Backup files 15 | *~.nib 16 | .*.swp 17 | *~ 18 | *.tmp 19 | *.bak 20 | .metadata 21 | Thumbs.db 22 | Desktop.ini 23 | 24 | # Other repositories 25 | .hg 26 | .svn 27 | CVS 28 | dist/ 29 | 30 | # Mac OS X Finder and whatnot 31 | .DS_Store 32 | .idea 33 | django_bigbluebutton.egg-info/* 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.ini *.cfg *.rst 2 | recursive-include django_bigbluebutton *.ico *.png *.css *.gif *.jpg *.txt *.js *.html *.xml 3 | include LICENSE 4 | include README.md 5 | recursive-include django_bigbluebutton/migrations *.py 6 | recursive-include django_bigbluebutton/templates * 7 | recursive-include django_bigbluebutton/locale * 8 | recursive-include django_bigbluebutton/api * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-bigbluebutton 2 | 3 | A Django Application for better interaction of Django projects with BigBlueButton APIs. 4 | 5 | Features: 6 | 7 | - Create a room on BBB and store the meeting info in a model for later access 8 | - Start a meeting having meeting id 9 | - End a meeting having meeting id 10 | - Create join link for a meeting with moderator or normal access 11 | - Get meeting report (activity of users in meeting and time log of them) 12 | - Log each meeting for later access and reportings 13 | - Create and Delete API Hook on BBB. (Will be usefull for example to be notified when user is leaved room from BBB and get noticed in your local service) 14 | 15 | ### Requirements 16 | 17 | To use django-bigbluebutton following packages are needed. If you don't have them installed, they will be installed automatically. 18 | ``` 19 | Django>=2.0 20 | requests>=2.0 21 | ``` 22 | 23 | ### Installation 24 | 25 | **Note:** This package is well tested on `django>=2.0`. But if you are using older versions, It can be 26 | used with minor changes in structure. 27 | 28 | install using pip: 29 | ```bash 30 | pip install django-bigbluebutton 31 | ``` 32 | 33 | ### Configure App 34 | Register app in `settings.py` 35 | 36 | ```python 37 | INSTALLED_APPS = [ 38 | "django_bigbluebutton", 39 | ] 40 | ``` 41 | 42 | Now should define you Big Blue Button Server configs in `settings.py`: 43 | 44 | ```python 45 | BBB_API_URL = 'https://test.com/bigbluebutton/api/' 46 | BBB_SECRET_KEY = 'abcdefgabcdefgabcdefgabcdefgabcdefg' 47 | ``` 48 | 49 | Next run migrate: 50 | ```bash 51 | python manage.py migrate 52 | ``` 53 | 54 | Run tests to check if installation is done: 55 | ```bash 56 | python manage.py test 57 | ``` 58 | 59 | ### Usage 60 | 61 | You can follow `tests.py` file to see how to use this package. 62 | 63 | 64 | ### Admin Integration 65 | 66 | By installing this app in your django project, A admin section will be added named `Meeting`. 67 | Under this section you can see list of open meetings, join meeting, create join link for other 68 | users with moderator or attendee permissions. 69 | 70 | Also to enable update state of each meeting (sync with bigbluebutton) set below variable in 71 | `settings.py`: 72 | 73 | ```python 74 | UPDATE_RUNNING_ON_EACH_CALL = True 75 | ``` 76 | 77 | So whenever you open list of meetings in django admin, it will update state of all meetings in database 78 | with result of `getMeetings` API from bigbluebutton. 79 | -------------------------------------------------------------------------------- /django_bigbluebutton.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: django-bigbluebutton 3 | Version: 0.5.9 4 | Summary: A Django integration APP to connect django projects to Big Blue Button ;) 5 | Home-page: https://github.com/Execut3/django-bigbluebutton 6 | Author: Execut3 7 | Author-email: execut3.binarycodes@gmail.com 8 | License: GPT 9 | Description: # django-bigbluebutton 10 | 11 | A Django Application for better interaction of Django projects with Big Blue Button APIs. 12 | 13 | ### Requirements 14 | 15 | To use this package following needed. if not provided will be installed automatically. 16 | ``` 17 | Django>=2.0 18 | requests>=2.0 19 | ``` 20 | 21 | ### Installation 22 | 23 | **Note:** This package is well tested on `django>=2.0`. But if you are using older versions can be 24 | used with minor changes in structure. 25 | 26 | install using pip: 27 | ```bash 28 | pip install django-bigbluebutton 29 | ``` 30 | 31 | ### Usage 32 | Register app in `settings.py` 33 | 34 | ```python 35 | INSTALLED_APPS = [ 36 | "django_bigbluebutton", 37 | ] 38 | ``` 39 | 40 | Now should define you Big Blue Button Server core configs in `settings.py`: 41 | 42 | ```python 43 | BBB_API_URL = 'https://test.com/bigbluebutton/api/' 44 | BBB_SECRET_KEY = 'abcdefgabcdefgabcdefgabcdefgabcdefg' 45 | ``` 46 | 47 | Next apply migrations: 48 | ```bash 49 | python manage.py migrate 50 | ``` 51 | 52 | And finally run test: 53 | 54 | ```bash 55 | python manage.py test 56 | ``` 57 | 58 | You can follow `tests.py` file to see how to use this package. 59 | 60 | ### Admin Integration 61 | 62 | By installing this app in your django project, A admin section will be added named `Meeting`. 63 | Under this section you can see list of open meetings, join meeting, create join link for other 64 | users with moderator or attendee permissions. 65 | 66 | Also to enable update state of each meeting (sync with bigbluebutton) set below variable in 67 | `settings.py`: 68 | 69 | ```python 70 | UPDATE_RUNNING_ON_EACH_CALL = True 71 | ``` 72 | 73 | So whenever you open list of meetings in django admin, it will update state of all meetings in database 74 | with result of `getMeetings` API from bigbluebutton. 75 | Platform: UNKNOWN 76 | Description-Content-Type: text/markdown 77 | -------------------------------------------------------------------------------- /django_bigbluebutton.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | MANIFEST.in 3 | README.md 4 | setup.py 5 | django_bigbluebutton/__init__.py 6 | django_bigbluebutton/admin.py 7 | django_bigbluebutton/apps.py 8 | django_bigbluebutton/bbb.py 9 | django_bigbluebutton/forms.py 10 | django_bigbluebutton/models.py 11 | django_bigbluebutton/settings.py 12 | django_bigbluebutton/signals.py 13 | django_bigbluebutton/tasks.py 14 | django_bigbluebutton/tests.py 15 | django_bigbluebutton/urls.py 16 | django_bigbluebutton/utils.py 17 | django_bigbluebutton.egg-info/PKG-INFO 18 | django_bigbluebutton.egg-info/SOURCES.txt 19 | django_bigbluebutton.egg-info/dependency_links.txt 20 | django_bigbluebutton.egg-info/requires.txt 21 | django_bigbluebutton.egg-info/top_level.txt 22 | django_bigbluebutton/api/__init__.py 23 | django_bigbluebutton/api/serializers.py 24 | django_bigbluebutton/api/urls.py 25 | django_bigbluebutton/api/views.py 26 | django_bigbluebutton/api/__pycache__/__init__.cpython-36.pyc 27 | django_bigbluebutton/api/__pycache__/serializers.cpython-36.pyc 28 | django_bigbluebutton/api/__pycache__/urls.cpython-36.pyc 29 | django_bigbluebutton/api/__pycache__/views.cpython-36.pyc 30 | django_bigbluebutton/locale/fa/LC_MESSAGES/django.mo 31 | django_bigbluebutton/locale/fa/LC_MESSAGES/django.po 32 | django_bigbluebutton/migrations/0001_initial.py 33 | django_bigbluebutton/migrations/0002_auto_20200831_1635.py 34 | django_bigbluebutton/migrations/0003_auto_20200831_1644.py 35 | django_bigbluebutton/migrations/0004_meeting_is_running.py 36 | django_bigbluebutton/migrations/0005_auto_20200911_0547.py 37 | django_bigbluebutton/migrations/0006_auto_20200911_0622.py 38 | django_bigbluebutton/migrations/0007_auto_20200925_0959.py 39 | django_bigbluebutton/migrations/0008_auto_20201008_1609.py 40 | django_bigbluebutton/migrations/0009_auto_20201112_1032.py 41 | django_bigbluebutton/migrations/0010_auto_20201126_0923.py 42 | django_bigbluebutton/migrations/0011_meetinglog.py 43 | django_bigbluebutton/migrations/0012_meetinglog_meeting.py 44 | django_bigbluebutton/migrations/0013_auto_20201211_0601.py 45 | django_bigbluebutton/migrations/0014_auto_20201211_0920.py 46 | django_bigbluebutton/migrations/0015_auto_20201214_1423.py 47 | django_bigbluebutton/migrations/0016_auto_20210202_1546.py 48 | django_bigbluebutton/migrations/0017_auto_20210202_1600.py 49 | django_bigbluebutton/migrations/__init__.py 50 | django_bigbluebutton/templates/admin/meeting_create_link.html -------------------------------------------------------------------------------- /django_bigbluebutton.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /django_bigbluebutton.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | Django>=2.0 2 | requests>=2.0 3 | djangorestframework>=3.0.0 4 | -------------------------------------------------------------------------------- /django_bigbluebutton.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | django_prefetch_relatedbigbluebutton 2 | -------------------------------------------------------------------------------- /django_bigbluebutton/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'django_bigbluebutton.apps.BigBlueButtonAppConfig' 2 | -------------------------------------------------------------------------------- /django_bigbluebutton/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.http import HttpResponseRedirect 3 | from django.urls import reverse, re_path, path 4 | from django.template.response import TemplateResponse 5 | 6 | from django.utils.html import format_html 7 | 8 | from .forms import MeetingCreateLinkForm 9 | from .settings import UPDATE_RUNNING_ON_EACH_CALL 10 | from .models import Meeting, MeetingLog, MeetingRecord 11 | 12 | 13 | @admin.register(Meeting) 14 | class MeetingAdmin(admin.ModelAdmin): 15 | date_hierarchy = 'updated_at' 16 | search_fields = ['name', 'meeting_id'] 17 | list_display = ( 18 | 'id', 'name', 'meeting_id', 'created_at', 19 | 'is_running', 'meeting_actions' 20 | ) 21 | actions = ["update_running_meetings"] if not UPDATE_RUNNING_ON_EACH_CALL else [] 22 | list_per_page = 30 23 | 24 | def get_queryset(self, request): 25 | qs = super(MeetingAdmin, self).get_queryset(request) 26 | 27 | # If settings.UPDATE_RUNNING_ON_EACH_CALL, then on each call 28 | # For queryset, call getMeetings and update status of meetings 29 | if UPDATE_RUNNING_ON_EACH_CALL: 30 | Meeting.update_running_meetings() 31 | 32 | return qs 33 | 34 | def join_meeting_action(self, request, meeting_id, *args, **kwargs): 35 | """ Will try to call join API of bigbluebutton and 36 | get a join link to meeting with provided meeting_id. 37 | And will redirect to join link. """ 38 | meeting = self.get_object(request, meeting_id) 39 | meeting.start() 40 | if not meeting.is_running: 41 | meeting.is_running = True 42 | meeting.save() 43 | return HttpResponseRedirect(reverse('admin:django_bigbluebutton_meeting_changelist')) 44 | 45 | def end_meeting_action(self, request, meeting_id, *args, **kwargs): 46 | """ Will call end() method from bigbluebutton, 47 | and then will update Meeting obj is_running status from local database""" 48 | meeting = self.get_object(request, meeting_id) 49 | ended = meeting.end() 50 | if ended: 51 | self.message_user(request, 'Success') 52 | else: 53 | self.message_user(request, 'Unable to close meeting', level=40) 54 | return HttpResponseRedirect(reverse('admin:django_bigbluebutton_meeting_changelist')) 55 | 56 | def create_meeting_link_action(self, request, meeting_id, *args, **kwargs): 57 | """ Will create meeting with meeting_id if not exist, 58 | Will join as moderator access. This method is for fast 59 | join to a meeting as admin. 60 | You can get same functionality with join meeting too, 61 | But should change user_type to 'moderator' for that. """ 62 | meeting = self.get_object(request, meeting_id) 63 | context = self.admin_site.each_context(request) 64 | 65 | if request.method != 'POST': 66 | # If method is not post, then just render the form 67 | form = MeetingCreateLinkForm() 68 | else: 69 | form = MeetingCreateLinkForm(request.POST) 70 | if form.is_valid(): 71 | # Now call create_link method and pass meeting 72 | # Object to it. create_link is located in 73 | # MeetingCreateLinkForm class in forms.py 74 | context['link'] = form.create_link(meeting) 75 | else: 76 | print('error not is_valid()') 77 | self.message_user(request, 'Error') 78 | url = reverse( 79 | 'admin:meeting-create-link', 80 | args=[meeting.pk], 81 | current_app=self.admin_site.name, 82 | ) 83 | return HttpResponseRedirect(url) 84 | 85 | context['opts'] = self.model._meta 86 | context['form'] = form 87 | context['meeting'] = meeting 88 | context['title'] = meeting.name 89 | return TemplateResponse( 90 | request, 91 | 'admin/meeting_create_link.html', 92 | context, 93 | ) 94 | 95 | def get_urls(self): 96 | """ All extra URLs are defined here.""" 97 | urls = super().get_urls() 98 | custom_urls = [ 99 | re_path( 100 | r'^(?P.+)/join/$', 101 | self.admin_site.admin_view(self.join_meeting_action), 102 | name='meeting-join', 103 | ), 104 | re_path( 105 | r'^(?P.+)/end/$', 106 | self.admin_site.admin_view(self.end_meeting_action), 107 | name='meeting-end', 108 | ), 109 | re_path( 110 | r'^(?P.+)/create-link/$', 111 | self.admin_site.admin_view(self.create_meeting_link_action), 112 | name='meeting-create-link', 113 | ), 114 | path('refresh-meetings/', self.update_running_meetings), 115 | ] 116 | return custom_urls + urls 117 | 118 | def meeting_actions(self, obj): 119 | """ This actions will be placed in front of each row of table list. 120 | For example here we registered two buttons: 121 | - Join Now: Which will call url 'admin:meeting-join' 122 | - Create Join Link: Which will cal url 'admin:meeting-create-link' 123 | """ 124 | 125 | create_join_link_href = 'Create join link'.format( 128 | reverse('admin:meeting-create-link', args=[obj.pk]) 129 | ) 130 | start_meeting_href = 'Start Now '.format( 132 | reverse('admin:meeting-join', args=[obj.pk]) 133 | ) 134 | end_meeting_href = 'End meeting'.format( 135 | reverse('admin:meeting-end', args=[obj.pk]) 136 | ) 137 | return format_html( 138 | '{} {}'.format( 139 | create_join_link_href, 140 | start_meeting_href if not obj.is_running else end_meeting_href 141 | ) 142 | ) 143 | 144 | meeting_actions.short_description = 'Actions' 145 | meeting_actions.allow_tags = True 146 | 147 | def changelist_view(self, request, extra_context=None): 148 | if 'action' in request.POST and request.POST['action'] == 'update_running_meetings': 149 | if not request.POST.getlist(admin.ACTION_CHECKBOX_NAME): 150 | post = request.POST.copy() 151 | for u in self.model.objects.all(): 152 | post.update({admin.ACTION_CHECKBOX_NAME: str(u.id)}) 153 | request._set_post(post) 154 | return super(MeetingAdmin, self).changelist_view(request, extra_context) 155 | 156 | def update_running_meetings(self, request, *args, **kwargs): 157 | """ Will get list of running meetings from bigbluebutton, 158 | And update states of Meetings objects in database. """ 159 | Meeting.update_running_meetings() 160 | return HttpResponseRedirect(".") 161 | 162 | update_running_meetings.short_description = "Refresh Meetings" 163 | 164 | 165 | admin.site.register(MeetingLog) 166 | admin.site.register(MeetingRecord) 167 | -------------------------------------------------------------------------------- /django_bigbluebutton/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Execut3/django-bigbluebutton/4cb2519a28c1e2024094ef9171ade0b0084b839d/django_bigbluebutton/api/__init__.py -------------------------------------------------------------------------------- /django_bigbluebutton/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from ..models import Meeting, MeetingLog 4 | 5 | 6 | class MeetingSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Meeting 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /django_bigbluebutton/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | 4 | from .views import * 5 | 6 | router = DefaultRouter() 7 | router.register("meeting", MeetingViewSet, "meeting_vs") 8 | 9 | urlpatterns = [ 10 | path('', include(router.urls)), 11 | ] 12 | -------------------------------------------------------------------------------- /django_bigbluebutton/api/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import urllib.parse 5 | 6 | from rest_framework.decorators import action 7 | from rest_framework.response import Response 8 | from rest_framework.viewsets import ModelViewSet 9 | 10 | from ..models import Meeting, MeetingLog 11 | from .serializers import MeetingSerializer 12 | 13 | 14 | class MeetingViewSet(ModelViewSet): 15 | queryset = Meeting.objects.all() 16 | serializer_class = MeetingSerializer 17 | 18 | @action(methods=['post'], detail=True, url_path='callback') 19 | def hook_callback(self, request, **kwargs): 20 | """ Data sample: 21 | event=%5B%7B%22data%22%3A%7B%22type%22%3A%22event%22%2C%22id%22%3A%22meeting-ended%22%2C%22attributes%22%3A%7B%22meeting%22%3A%7B%22internal-meeting-id%22%3A%2200834d44d223918856f4683db2fc2651dd05782e-1607608335199%22%2C%22external-meeting-id%22%3A%22testmeeting-11%22%7D%7D%2C%22event%22%3A%7B%22ts%22%3A1607608642899%7D%7D%7D%5D×tamp=1607608642905&domain=meeting.cpol.co 22 | 23 | first should be url-decode to be like below: 24 | event=[ 25 | { 26 | "data":{ 27 | "type":"event", 28 | "id":"user-emoji-changed", 29 | "attributes":{ 30 | "meeting":{ 31 | "internal-meeting-id":"5c13f0e2e59b3348767e88ee9e7d8ee858689f8c-1607594986040", 32 | "external-meeting-id":"meeting-11" 33 | }, 34 | "user":{ 35 | "internal-user-id":"w_fqxudq3emu8z", 36 | "external-user-id":"w_fqxudq3emu8z" 37 | } 38 | }, 39 | "event":{ 40 | "ts":1607595086800 41 | } 42 | } 43 | } 44 | ]×tamp=1607608642905&domain=meeting.cpol.co 45 | """ 46 | print('here') 47 | event_data = request.data.get('event', '') 48 | current_datetime = datetime.datetime.now() 49 | 50 | try: 51 | tmp = urllib.parse.unquote(event_data) 52 | event_data = json.loads(tmp) 53 | print(event_data) 54 | for e in event_data: 55 | e = e['data'] 56 | event_id = e['id'] 57 | if event_id in ['user-joined', 'user-left']: 58 | try: 59 | attributes = e['attributes'] 60 | meeting_id = attributes['meeting']['external-meeting-id'] 61 | user_id = attributes['user']['external-user-id'] 62 | 63 | try: 64 | user_id_valid = int(user_id) 65 | except: 66 | user_id_valid = None 67 | 68 | if not user_id_valid: 69 | continue 70 | 71 | from django_bigbluebutton.models import Meeting 72 | meeting = Meeting.objects.filter(meeting_id=meeting_id).first() 73 | if not meeting: 74 | continue 75 | 76 | # When user-joined/left is received 77 | # By default we should set current date 78 | # To all logs of this user which are null yet 79 | MeetingLog.objects.filter( 80 | meeting=meeting, 81 | user_id=user_id_valid, 82 | left_date__isnull=True 83 | ).update( 84 | left_date=current_datetime 85 | ) 86 | 87 | if event_id == 'user-joined': 88 | MeetingLog.objects.create( 89 | meeting=meeting, 90 | user_id=user_id_valid 91 | ) 92 | except Exception as e: 93 | logging.error(str(e)) 94 | elif event_id == 'meeting-ended': 95 | try: 96 | attributes = e['attributes'] 97 | meeting_id = attributes['meeting']['external-meeting-id'] 98 | 99 | from django_bigbluebutton.models import Meeting 100 | meeting = Meeting.objects.filter(meeting_id=meeting_id).first() 101 | if not meeting: 102 | continue 103 | 104 | # Now bulk update all meetingLogs which their left_date is null 105 | MeetingLog.objects.filter( 106 | meeting=meeting, 107 | left_date__isnull=True 108 | ).update( 109 | left_date=current_datetime 110 | ) 111 | except Exception as e: 112 | logging.error(str(e)) 113 | 114 | except Exception as e: 115 | print(e) 116 | pass 117 | 118 | return Response({}) 119 | -------------------------------------------------------------------------------- /django_bigbluebutton/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BigBlueButtonAppConfig(AppConfig): 5 | name = 'django_bigbluebutton' 6 | 7 | def ready(self): 8 | import django_bigbluebutton.signals 9 | 10 | -------------------------------------------------------------------------------- /django_bigbluebutton/bbb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import urllib 3 | import random 4 | import requests 5 | 6 | from hashlib import sha1 7 | from django.utils.translation import ugettext_lazy as _ 8 | 9 | 10 | from .settings import * 11 | from .utils import parse_xml 12 | 13 | 14 | class BigBlueButton: 15 | """ Main class for BigBlueButton 16 | 17 | List of methods: 18 | - api_call 19 | - is_running 20 | - end_meeting 21 | - meeting_info 22 | - get_meetings 23 | - join_url 24 | - start 25 | """ 26 | secret_key = settings.BBB_SECRET_KEY 27 | api_url = settings.BBB_API_URL 28 | attendee_password = 'ap' 29 | moderator_password = 'mp' 30 | 31 | def api_call(self, query, call): 32 | """ Method to create valid API query 33 | to call on BigBlueButton. Because each query 34 | should have a encrypted checksum based on request Data. 35 | """ 36 | prepared = '{}{}{}'.format(call, query, self.secret_key) 37 | checksum = sha1(str(prepared).encode('utf-8')).hexdigest() 38 | result = "%s&checksum=%s" % (query, checksum) 39 | return result 40 | 41 | def is_running(self, meeting_id): 42 | """ Return whether meeting_id is running or not! """ 43 | call = 'isMeetingRunning' 44 | query = urllib.parse.urlencode(( 45 | ('meetingID', meeting_id), 46 | )) 47 | hashed = self.api_call(query, call) 48 | url = self.api_url + call + '?' + hashed 49 | result = parse_xml(requests.get(url).content) 50 | if result: 51 | return result.find('running').text 52 | return 'error' 53 | 54 | def end_meeting(self, meeting_id, password): 55 | """ End meeting, 56 | Should provide Moderator password as input to make it work! 57 | """ 58 | call = 'end' 59 | query = urllib.parse.urlencode(( 60 | ('meetingID', meeting_id), 61 | ('password', password), 62 | )) 63 | hashed = self.api_call(query, call) 64 | url = self.api_url + call + '?' + hashed 65 | req = requests.get(url) 66 | result = parse_xml(req.content) 67 | if result: 68 | return True 69 | return False 70 | 71 | def meeting_info(self, meeting_id, password): 72 | """ Get information about meeting. 73 | result includes below data: 74 | start_time 75 | end_time 76 | participant_count 77 | moderator_count 78 | attendee_pw 79 | moderator_pw 80 | """ 81 | call = 'getMeetingInfo' 82 | query = urllib.parse.urlencode(( 83 | ('meetingID', meeting_id), 84 | ('password', password), 85 | )) 86 | hashed = self.api_call(query, call) 87 | url = self.api_url + call + '?' + hashed 88 | r = parse_xml(requests.get(url).content) 89 | if r: 90 | attendee_list = [] 91 | try: 92 | for a in r.findall('attendees'): 93 | try: 94 | x = a.find('attendee') 95 | user_id = x.find('userID').text 96 | if not user_id: 97 | continue 98 | fullname = x.find('fullName').text 99 | role = x.find('role') 100 | attendee_list.append({ 101 | 'id': user_id, 102 | 'fullname': fullname, 103 | 'role': role 104 | }) 105 | except: 106 | continue 107 | except Exception as e: 108 | pass 109 | 110 | # Create dict of values for easy use in template 111 | d = { 112 | 'start_time': r.find('startTime').text, 113 | 'end_time': r.find('endTime').text, 114 | 'participant_count': r.find('participantCount').text, 115 | 'moderator_count': r.find('moderatorCount').text, 116 | 'moderator_pw': r.find('moderatorPW').text, 117 | 'attendee_pw': r.find('attendeePW').text, 118 | 'attendee_list': attendee_list, 119 | # 'invite_url': reverse('join', args=[meeting_id]), 120 | } 121 | return d 122 | return None 123 | 124 | def get_meetings(self): 125 | """ Will return list of running meetings. """ 126 | call = 'getMeetings' 127 | query = urllib.parse.urlencode(( 128 | ('random', 'random'), 129 | )) 130 | hashed = self.api_call(query, call) 131 | url = self.api_url + call + '?' + hashed 132 | result = parse_xml(requests.get(url).content) 133 | # Create dict of values for easy use in template 134 | d = [] 135 | if result: 136 | r = result[1].findall('meeting') 137 | for m in r: 138 | meeting_id = m.find('meetingID').text 139 | password = m.find('moderatorPW').text 140 | d.append({ 141 | 'meeting_id': meeting_id, 142 | 'running': m.find('running').text, 143 | 'moderator_pw': password, 144 | 'attendee_pw': m.find('attendeePW').text, 145 | 'info': self.meeting_info( 146 | meeting_id, 147 | password 148 | ) 149 | }) 150 | return d 151 | 152 | def join_url(self, meeting_id, name, password, **kwargs): 153 | """ Join existing meeting_id. 154 | 155 | can send userID also as **kwargs so it will be set on 156 | meeting join, and can track useful info about users later. 157 | """ 158 | call = 'join' 159 | data = ( 160 | ('fullName', name), 161 | ('meetingID', meeting_id), 162 | ('password', password), 163 | ) 164 | for key, value in kwargs.items(): 165 | # Iterate on kwargs keys, and set their key=value in request. 166 | data = data + ((key, value), ) 167 | 168 | query = urllib.parse.urlencode(data) 169 | hashed = self.api_call(query, call) 170 | url = self.api_url + call + '?' + hashed 171 | return url 172 | 173 | def start(self, name, meeting_id, **kwargs): 174 | """ Start meeting with provided info. 175 | 176 | Most of BigBlueButton info is provided now. 177 | TODO: will add more configs for bigbluebutton later! 178 | """ 179 | call = 'create' 180 | attendee_password = kwargs.get("attendee_password", self.attendee_password) 181 | moderator_password = kwargs.get("moderator_password", self.moderator_password) 182 | 183 | # Get extra configs or set default values 184 | welcome = kwargs.get('welcome_text', _('Welcome!')) 185 | record = kwargs.get('record', BBB_RECORD) 186 | auto_start_recording = kwargs.get('auto_start_recording', BBB_AUTO_RECORDING) 187 | allow_start_stop_recording = kwargs.get('allow_start_stop_recording', BBB_ALLOW_START_STOP_RECORDING) 188 | logout_url = kwargs.get('logout_url', BBB_LOGOUT_URL) 189 | webcam_only_for_moderators = kwargs.get('webcam_only_for_moderators', BBB_WEBCAM_ONLY_FOR_MODS) 190 | voice_bridge = 70000 + random.randint(0, 9999) 191 | 192 | # Making the query string 193 | query = urllib.parse.urlencode(( 194 | ('name', name), 195 | ('meetingID', meeting_id), 196 | ('attendeePW', attendee_password), 197 | ('moderatorPW', moderator_password), 198 | ('record', record), 199 | ('welcome', welcome), 200 | ('bannerText', welcome), 201 | ('copyright', BBB_COPYRIGHT_TEXT), 202 | ('logoutURL', logout_url), 203 | ('voiceBridge', voice_bridge), 204 | ('autoStartRecording', auto_start_recording), 205 | ('allowStartStopRecording', allow_start_stop_recording), 206 | ('webcamsOnlyForModerator', webcam_only_for_moderators), 207 | )) 208 | hashed = self.api_call(query, call) 209 | url = self.api_url + call + '?' + hashed 210 | result = parse_xml(requests.get(url).content.decode('utf-8')) 211 | if result: 212 | return result 213 | else: 214 | raise 215 | 216 | def get_hooks(self): 217 | """ Will return list of existing hooks. 218 | 219 | output sample: 220 | 221 | SUCCESS 222 | 223 | 224 | 2 225 | 226 | 227 | 228 | 229 | 230 | 231 | false 232 | false 233 | 234 | 235 | 236 | """ 237 | call = 'hooks/list' 238 | query = urllib.parse.urlencode(()) 239 | hashed = self.api_call(query, call) 240 | url = self.api_url + call + '?' + hashed 241 | r = parse_xml(requests.get(url).content) 242 | 243 | hooks_list = [] 244 | if r: 245 | try: 246 | for h in r.findall('hooks'): 247 | try: 248 | x = h.find('hook') 249 | callback_url = x.find('callbackURL').text 250 | meeting_id = x.find('meetingID').text 251 | hook_id = x.find('meetingID').text 252 | except Exception as e: 253 | # print(e) 254 | continue 255 | 256 | hooks_list.append({ 257 | 'hook_id': hook_id, 258 | 'meeting_id': meeting_id, 259 | 'callback_url': callback_url 260 | }) 261 | except Exception as e: 262 | # print(e) 263 | pass 264 | 265 | print(hooks_list) 266 | return hooks_list 267 | 268 | def create_hook(self, callback_url, meeting_id=None): 269 | """ Will create a hook for meeting 270 | 271 | :param meeting_id: name of meeting to append hook to 272 | :param callback_url: callback url to be called whenever hook happened for that meeting_id 273 | 274 | bbb request result: 275 | 276 | SUCCESS 277 | 2 278 | duplicateWarning 279 | There is already a hook for this callback URL. 280 | 281 | """ 282 | """ Return whether meeting_id is running or not! """ 283 | call = 'hooks/create' 284 | data = ( 285 | ('callbackURL', callback_url), 286 | ('meetingID', meeting_id), 287 | ) 288 | 289 | query = urllib.parse.urlencode(data) 290 | hashed = self.api_call(query, call) 291 | url = self.api_url + call + '?' + hashed 292 | print(url) 293 | r = parse_xml(requests.get(url).content) 294 | 295 | try: 296 | hook_id = r.find('hookID').text 297 | return {'hook_id': hook_id} 298 | except: 299 | return None 300 | 301 | def destroy_hook(self, hook_id): 302 | call = 'hooks/destroy' 303 | data = ( 304 | ('hookID', hook_id), 305 | ) 306 | query = urllib.parse.urlencode(data) 307 | hashed = self.api_call(query, call) 308 | url = self.api_url + call + '?' + hashed 309 | print(url) 310 | result = parse_xml(requests.get(url).content) 311 | print(result) 312 | if result: 313 | return True 314 | return False 315 | 316 | # Recordings 317 | def get_meeting_records(self, meeting_id): 318 | """ Will return list of records for provided meeting_id """ 319 | try: 320 | call = 'getRecordings' 321 | query = urllib.parse.urlencode(( 322 | ('meetingID', meeting_id), 323 | )) 324 | hashed = self.api_call(query, call) 325 | url = self.api_url + call + '?' + hashed 326 | content = requests.get(url).content 327 | result = parse_xml(content) 328 | print(result) 329 | # Create dict of values for easy use in template 330 | d = [] 331 | if result: 332 | r = result[1].findall('recording') 333 | for m in r: 334 | try: 335 | record_id = m.find('recordID').text 336 | meeting_id = m.find('meetingID').text 337 | name = m.find('name').text 338 | start_time = m.find('startTime').text 339 | end_time = m.find('endTime').text 340 | raw_size = m.find('rawSize').text 341 | try: 342 | url = m.find('playback').find('format').find('url').text 343 | except: 344 | url = '' 345 | d.append({ 346 | 'url': url, 347 | 'name': name, 348 | 'end_time': end_time, 349 | 'raw_size': raw_size, 350 | 'record_id': record_id, 351 | 'meeting_id': meeting_id, 352 | 'start_time': start_time, 353 | }) 354 | except Exception as e: 355 | logging.error(str(e)) 356 | return d 357 | except Exception as e: 358 | logging.error(str(e)) 359 | 360 | 361 | -------------------------------------------------------------------------------- /django_bigbluebutton/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class MeetingCreateLinkForm(forms.Form): 5 | fullname = forms.CharField(required=True) 6 | USER_TYPES = ( 7 | ('moderator', 'Moderator'), 8 | ('attendee', 'Attendee') 9 | ) 10 | user_type = forms.ChoiceField( 11 | required=True, 12 | choices=USER_TYPES, 13 | ) 14 | 15 | def create_link(self, meeting): 16 | fullname = self.cleaned_data.get('fullname') 17 | user_type = self.cleaned_data.get('user_type', 'attendee') 18 | return meeting.create_join_link(fullname, user_type) 19 | 20 | 21 | -------------------------------------------------------------------------------- /django_bigbluebutton/locale/fa/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Execut3/django-bigbluebutton/4cb2519a28c1e2024094ef9171ade0b0084b839d/django_bigbluebutton/locale/fa/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /django_bigbluebutton/locale/fa/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-02-02 17:03+0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: bbb.py:184 models.py:48 22 | msgid "Welcome!" 23 | msgstr "خوش آمدید!" 24 | 25 | #: models.py:22 26 | #, fuzzy 27 | #| msgid "Meeting" 28 | msgid "Meeting Name" 29 | msgstr "جلسه آنلاین" 30 | 31 | #: models.py:26 32 | msgid "Meeting ID" 33 | msgstr "شناسه جلسه" 34 | 35 | #: models.py:30 36 | msgid "Attendee Password" 37 | msgstr "پسورد شرکت کننده" 38 | 39 | #: models.py:34 40 | msgid "Moderator Password" 41 | msgstr "پسورد مجری/مدرس" 42 | 43 | #: models.py:38 44 | msgid "Is running" 45 | msgstr "در حال اجرا" 46 | 47 | #: models.py:39 48 | msgid "Indicates whether this meeting is running in BigBlueButton or not!" 49 | msgstr "نشان می‌دهد که آیان این جلسه در حال اجرا هست یا نه!" 50 | 51 | #: models.py:45 52 | msgid "Max Participants" 53 | msgstr "بیشترین تعداد شرکت‌کنندگان" 54 | 55 | #: models.py:49 56 | msgid "Meeting Text in Bigbluebutton" 57 | msgstr "متن جلسه برای نمایش در استودیو آنلاین" 58 | 59 | #: models.py:54 60 | msgid "URL to visit after user logged out" 61 | msgstr "آدرس وب برای هدایت کاربران پس از خروج از ملاقات" 62 | 63 | #: models.py:58 64 | msgid "Record" 65 | msgstr "ضبط کردن" 66 | 67 | #: models.py:62 68 | msgid "Auto Start Recording" 69 | msgstr "فعال‌سازی ضبط به صورت اتوماتیک" 70 | 71 | #: models.py:66 72 | msgid "Allow Stop/Start Recording" 73 | msgstr "اجازه دادن برای توقف/اجرای ضبط کردن" 74 | 75 | #: models.py:67 76 | msgid "Allow the user to start/stop recording. (default true)" 77 | msgstr "" 78 | "به کاربر اجازه داده شود تا بتواند ضبط کردن را متوقف یا شروع کن (پیش‌فرض بله " 79 | "است)" 80 | 81 | #: models.py:71 82 | msgid "Webcam Only for moderators?" 83 | msgstr "وب‌کم فقط برای مجری و مدرس‌ها فعال باشد." 84 | 85 | #: models.py:72 86 | msgid "" 87 | "will cause all webcams shared by viewers during this meeting to only appear " 88 | "for moderators" 89 | msgstr "" 90 | "در این حالت دوربین اشتراک گذاشته شده توسط شرکت‌کنندگان توسط تنها مجری‌ها قابل " 91 | "مشاهده باشد." 92 | 93 | #: models.py:79 94 | msgid "Disable Camera" 95 | msgstr "غیرفعال کردن دوربین" 96 | 97 | #: models.py:80 98 | msgid "will prevent users from sharing their camera in the meeting" 99 | msgstr "از اشتراک گذاری دوربین شرکت‌کننده‌ها در جلسه جلوگیری می‌کند." 100 | 101 | #: models.py:84 102 | msgid "Disable Mic" 103 | msgstr "غیرفعال کردن میکروفون" 104 | 105 | #: models.py:85 106 | msgid "will only allow user to join listen only" 107 | msgstr "به شرکت‌کننده‌ها فقط اجازه گوش‌دادن داده شود." 108 | 109 | #: models.py:89 110 | msgid "Disable Private chat" 111 | msgstr "غیرفعال کردن چت خصوصی بین شرکت‌کننده‌ها" 112 | 113 | #: models.py:90 114 | msgid "if True will disable private chats in the meeting" 115 | msgstr "اگر فعال باشد چت خصوصی غیرفعال می‌شود." 116 | 117 | #: models.py:94 118 | msgid "Disable public chat" 119 | msgstr "غیرفعال کردن چت عمومی" 120 | 121 | #: models.py:95 122 | msgid "if True will disable public chat in the meeting" 123 | msgstr "اگر بله باشد چت عمومی غیرفعال می‌شود." 124 | 125 | #: models.py:99 126 | msgid "Disable Note" 127 | msgstr "غیرفعال کردن یادداشت" 128 | 129 | #: models.py:100 130 | msgid "if True will disable notes in the meeting." 131 | msgstr "اگر بله باشد نمی‌توان در جلسه یادداشت اشتراک گذاشت." 132 | 133 | #: models.py:104 134 | msgid "Locked Layout" 135 | msgstr "قفل کردن layout " 136 | 137 | #: models.py:105 138 | msgid "will lock the layout in the meeting. " 139 | msgstr "layout در طول ملاقات غیرفعال می‌شود." 140 | 141 | #: models.py:113 142 | msgid "Parent Meeting ID" 143 | msgstr "شناسه ملاقات والد" 144 | 145 | #: models.py:119 146 | msgid "Internal Meeting ID" 147 | msgstr "شناسه ملاقات داخلی (نرم‌افزار)" 148 | 149 | #: models.py:124 150 | msgid "Voice Bridge" 151 | msgstr "پل صوتی" 152 | 153 | #: models.py:131 154 | msgid "Hook ID received from BBB" 155 | msgstr "شناسه Hook که از سمت BBB دریافت شده است" 156 | 157 | #: models.py:137 158 | msgid "Hook URL" 159 | msgstr "آدرس برای هوک کردن" 160 | 161 | #: models.py:150 models.py:410 models.py:483 162 | msgid "Meeting" 163 | msgstr "جلسه آنلاین" 164 | 165 | #: models.py:416 166 | msgid "Record name" 167 | msgstr "نام فایل ضبط شده" 168 | 169 | #: models.py:423 170 | msgid "Record ID" 171 | msgstr "شناسه ضبط" 172 | 173 | #: models.py:429 174 | msgid "Link" 175 | msgstr "لینک" 176 | 177 | #: models.py:438 178 | msgid "Meeting Record" 179 | msgstr "رکورد جلسه ضبط شده" 180 | 181 | #: models.py:467 182 | msgid "User" 183 | msgstr "کاربر" 184 | 185 | #: models.py:476 186 | msgid "User fullname" 187 | msgstr "نام کامل کاربر" 188 | 189 | #: models.py:493 190 | msgid "Join Date" 191 | msgstr "تاریخ وصل شدن" 192 | 193 | #: models.py:498 194 | msgid "Left Date" 195 | msgstr "تاریخ ترک کردن" 196 | 197 | #: models.py:511 198 | msgid "Meeting Log" 199 | msgstr "لاگ ملاقات" 200 | 201 | #~ msgid "Name of Meeting" 202 | #~ msgstr "نام ملاقات" 203 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-08-31 11:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Meeting', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('meeting_id', models.CharField(max_length=100, unique=True)), 20 | ('attendee_password', models.CharField(max_length=50)), 21 | ('moderator_password', models.CharField(max_length=50)), 22 | ('created_at', models.DateTimeField(auto_now_add=True)), 23 | ('updated_at', models.DateTimeField(auto_now=True)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0002_auto_20200831_1635.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-08-31 16:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meeting', 15 | name='internal_meeting_id', 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='meeting', 20 | name='parent_meeting_id', 21 | field=models.CharField(blank=True, max_length=100, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='meeting', 25 | name='welcome_text', 26 | field=models.TextField(default='Welcome!'), 27 | ), 28 | migrations.AlterField( 29 | model_name='meeting', 30 | name='name', 31 | field=models.CharField(max_length=100, unique=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0003_auto_20200831_1644.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-08-31 16:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0002_auto_20200831_1635'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meeting', 15 | name='voice_bridge', 16 | field=models.CharField(blank=True, max_length=50, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='meeting', 20 | name='name', 21 | field=models.CharField(max_length=100), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0004_meeting_is_running.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-09-03 10:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0003_auto_20200831_1644'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meeting', 15 | name='is_running', 16 | field=models.BooleanField(default=False, help_text='Indicates whether this meeting is running in BigBlueButton or not!'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0005_auto_20200911_0547.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-09-11 05:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0004_meeting_is_running'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meeting', 15 | name='logout_url', 16 | field=models.CharField(blank=True, default='', max_length=200, null=True, verbose_name='URL to visit after user logged out'), 17 | ), 18 | migrations.AddField( 19 | model_name='meeting', 20 | name='max_participants', 21 | field=models.IntegerField(default=10, verbose_name='Max Participants'), 22 | ), 23 | migrations.AddField( 24 | model_name='meeting', 25 | name='record', 26 | field=models.BooleanField(default=True, verbose_name='Record'), 27 | ), 28 | migrations.AlterField( 29 | model_name='meeting', 30 | name='attendee_password', 31 | field=models.CharField(max_length=50, verbose_name='Attendee Password'), 32 | ), 33 | migrations.AlterField( 34 | model_name='meeting', 35 | name='internal_meeting_id', 36 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Internal Meeting ID'), 37 | ), 38 | migrations.AlterField( 39 | model_name='meeting', 40 | name='is_running', 41 | field=models.BooleanField(default=False, help_text='Indicates whether this meeting is running in BigBlueButton or not!', verbose_name='Is running'), 42 | ), 43 | migrations.AlterField( 44 | model_name='meeting', 45 | name='meeting_id', 46 | field=models.CharField(max_length=100, unique=True, verbose_name='Meeting ID'), 47 | ), 48 | migrations.AlterField( 49 | model_name='meeting', 50 | name='moderator_password', 51 | field=models.CharField(max_length=50, verbose_name='Moderator Password'), 52 | ), 53 | migrations.AlterField( 54 | model_name='meeting', 55 | name='name', 56 | field=models.CharField(max_length=100, verbose_name='Name of Meeting'), 57 | ), 58 | migrations.AlterField( 59 | model_name='meeting', 60 | name='parent_meeting_id', 61 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Parent Meeting ID'), 62 | ), 63 | migrations.AlterField( 64 | model_name='meeting', 65 | name='voice_bridge', 66 | field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Voice Bridge'), 67 | ), 68 | migrations.AlterField( 69 | model_name='meeting', 70 | name='welcome_text', 71 | field=models.TextField(default='Welcome!', verbose_name='Meeting Text in Bigbluebutton'), 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0006_auto_20200911_0622.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-09-11 06:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0005_auto_20200911_0547'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meeting', 15 | name='allow_start_stop_recording', 16 | field=models.BooleanField(default=True, help_text='Allow the user to start/stop recording. (default true)', verbose_name='Allow Stop/Start Recording'), 17 | ), 18 | migrations.AddField( 19 | model_name='meeting', 20 | name='auto_start_recording', 21 | field=models.BooleanField(default=True, verbose_name='Auto Start Recording'), 22 | ), 23 | migrations.AddField( 24 | model_name='meeting', 25 | name='lock_settings_disable_cam', 26 | field=models.BooleanField(default=False, help_text='will prevent users from sharing their camera in the meeting', verbose_name='Disable Camera'), 27 | ), 28 | migrations.AddField( 29 | model_name='meeting', 30 | name='lock_settings_disable_mic', 31 | field=models.BooleanField(default=False, help_text='will only allow user to join listen only', verbose_name='Disable Mic'), 32 | ), 33 | migrations.AddField( 34 | model_name='meeting', 35 | name='lock_settings_disable_note', 36 | field=models.BooleanField(default=False, help_text='if True will disable notes in the meeting.', verbose_name='Disable Note'), 37 | ), 38 | migrations.AddField( 39 | model_name='meeting', 40 | name='lock_settings_disable_private_chat', 41 | field=models.BooleanField(default=False, help_text='if True will disable private chats in the meeting', verbose_name='Disable Private chat'), 42 | ), 43 | migrations.AddField( 44 | model_name='meeting', 45 | name='lock_settings_disable_public_chat', 46 | field=models.BooleanField(default=False, help_text='if True will disable public chat in the meeting', verbose_name='Disable public chat'), 47 | ), 48 | migrations.AddField( 49 | model_name='meeting', 50 | name='lock_settings_locked_layout', 51 | field=models.BooleanField(default=False, help_text='will lock the layout in the meeting. ', verbose_name='Locked Layout'), 52 | ), 53 | migrations.AddField( 54 | model_name='meeting', 55 | name='webcam_only_for_moderators', 56 | field=models.BooleanField(default=False, help_text='will cause all webcams shared by viewers during this meeting to only appear for moderators', verbose_name='Webcam Only for moderators?'), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0007_auto_20200925_0959.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-09-25 09:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0006_auto_20200911_0622'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='meeting', 15 | name='allow_start_stop_recording', 16 | field=models.BooleanField(default=True, help_text='به کاربر اجازه داده شود تا بتواند ضبط کردن را متوقف یا شروع کن (پیش\u200cفرض بله است)', verbose_name='اجازه دادن برای توقف/اجرای ضبط کردن'), 17 | ), 18 | migrations.AlterField( 19 | model_name='meeting', 20 | name='attendee_password', 21 | field=models.CharField(max_length=50, verbose_name='پسورد شرکت کننده'), 22 | ), 23 | migrations.AlterField( 24 | model_name='meeting', 25 | name='auto_start_recording', 26 | field=models.BooleanField(default=True, verbose_name='فعال\u200cسازی ضبط به صورت اتوماتیک'), 27 | ), 28 | migrations.AlterField( 29 | model_name='meeting', 30 | name='internal_meeting_id', 31 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='شناسه ملاقات داخلی (نرم\u200cافزار)'), 32 | ), 33 | migrations.AlterField( 34 | model_name='meeting', 35 | name='is_running', 36 | field=models.BooleanField(default=False, help_text='Indicates whether this meeting is running in BigBlueButton or not!', verbose_name='در حال اجرا'), 37 | ), 38 | migrations.AlterField( 39 | model_name='meeting', 40 | name='lock_settings_disable_cam', 41 | field=models.BooleanField(default=False, help_text='از اشتراک گذاری دوربین شرکت\u200cکننده\u200cها در جلسه جلوگیری می\u200cکند.', verbose_name='غیرفعال کردن دوربین'), 42 | ), 43 | migrations.AlterField( 44 | model_name='meeting', 45 | name='lock_settings_disable_mic', 46 | field=models.BooleanField(default=False, help_text='به شرکت\u200cکننده\u200cها فقط اجازه گوش\u200cدادن داده شود.', verbose_name='غیرفعال کردن میکروفون'), 47 | ), 48 | migrations.AlterField( 49 | model_name='meeting', 50 | name='lock_settings_disable_note', 51 | field=models.BooleanField(default=False, help_text='اگر بله باشد نمی\u200cتوان در جلسه یادداشت اشتراک گذاشت.', verbose_name='غیرفعال کردن یادداشت'), 52 | ), 53 | migrations.AlterField( 54 | model_name='meeting', 55 | name='lock_settings_disable_private_chat', 56 | field=models.BooleanField(default=False, help_text='اگر فعال باشد چت خصوصی غیرفعال می\u200cشود.', verbose_name='غیرفعال کردن چت خصوصی بین شرکت\u200cکننده\u200cها'), 57 | ), 58 | migrations.AlterField( 59 | model_name='meeting', 60 | name='lock_settings_disable_public_chat', 61 | field=models.BooleanField(default=False, help_text='اگر بله باشد چت عمومی غیرفعال می\u200cشود.', verbose_name='غیرفعال کردن چت عمومی'), 62 | ), 63 | migrations.AlterField( 64 | model_name='meeting', 65 | name='lock_settings_locked_layout', 66 | field=models.BooleanField(default=False, help_text='layout در طول ملاقات غیرفعال می\u200cشود.', verbose_name='قفل کردن layout '), 67 | ), 68 | migrations.AlterField( 69 | model_name='meeting', 70 | name='logout_url', 71 | field=models.CharField(blank=True, default='', max_length=200, null=True, verbose_name='آدرس وب برای هدایت کاربران پس از خروج از ملاقات'), 72 | ), 73 | migrations.AlterField( 74 | model_name='meeting', 75 | name='max_participants', 76 | field=models.IntegerField(default=10, verbose_name='بیشترین تعداد شرکت\u200cکنندگان'), 77 | ), 78 | migrations.AlterField( 79 | model_name='meeting', 80 | name='meeting_id', 81 | field=models.CharField(max_length=100, unique=True, verbose_name='شناسه جلسه'), 82 | ), 83 | migrations.AlterField( 84 | model_name='meeting', 85 | name='moderator_password', 86 | field=models.CharField(max_length=50, verbose_name='پسورد مجری/مدرس'), 87 | ), 88 | migrations.AlterField( 89 | model_name='meeting', 90 | name='name', 91 | field=models.CharField(max_length=100, verbose_name='نام ملاقات'), 92 | ), 93 | migrations.AlterField( 94 | model_name='meeting', 95 | name='parent_meeting_id', 96 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='شناسه ملاقات والد'), 97 | ), 98 | migrations.AlterField( 99 | model_name='meeting', 100 | name='record', 101 | field=models.BooleanField(default=True, verbose_name='ضبط کردن'), 102 | ), 103 | migrations.AlterField( 104 | model_name='meeting', 105 | name='voice_bridge', 106 | field=models.CharField(blank=True, max_length=50, null=True, verbose_name='پل صوتی'), 107 | ), 108 | migrations.AlterField( 109 | model_name='meeting', 110 | name='webcam_only_for_moderators', 111 | field=models.BooleanField(default=False, help_text='در این حالت دوربین اشتراک گذاشته شده توسط شرکت\u200cکنندگان توسط تنها مجری\u200cها قابل مشاهده باشد.', verbose_name='وب\u200cکم فقط برای مجری و مدرس\u200cها فعال باشد.'), 112 | ), 113 | ] 114 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0008_auto_20201008_1609.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-10-08 16:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0007_auto_20200925_0959'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='meeting', 15 | name='allow_start_stop_recording', 16 | field=models.BooleanField(default=True, help_text='Allow the user to start/stop recording. (default true)', verbose_name='Allow Stop/Start Recording'), 17 | ), 18 | migrations.AlterField( 19 | model_name='meeting', 20 | name='attendee_password', 21 | field=models.CharField(max_length=50, verbose_name='Attendee Password'), 22 | ), 23 | migrations.AlterField( 24 | model_name='meeting', 25 | name='auto_start_recording', 26 | field=models.BooleanField(default=True, verbose_name='Auto Start Recording'), 27 | ), 28 | migrations.AlterField( 29 | model_name='meeting', 30 | name='created_at', 31 | field=models.DateTimeField(auto_now_add=True), 32 | ), 33 | migrations.AlterField( 34 | model_name='meeting', 35 | name='internal_meeting_id', 36 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Internal Meeting ID'), 37 | ), 38 | migrations.AlterField( 39 | model_name='meeting', 40 | name='is_running', 41 | field=models.BooleanField(default=False, help_text='Indicates whether this meeting is running in BigBlueButton or not!', verbose_name='Is running'), 42 | ), 43 | migrations.AlterField( 44 | model_name='meeting', 45 | name='lock_settings_disable_cam', 46 | field=models.BooleanField(default=False, help_text='will prevent users from sharing their camera in the meeting', verbose_name='Disable Camera'), 47 | ), 48 | migrations.AlterField( 49 | model_name='meeting', 50 | name='lock_settings_disable_mic', 51 | field=models.BooleanField(default=False, help_text='will only allow user to join listen only', verbose_name='Disable Mic'), 52 | ), 53 | migrations.AlterField( 54 | model_name='meeting', 55 | name='lock_settings_disable_note', 56 | field=models.BooleanField(default=False, help_text='if True will disable notes in the meeting.', verbose_name='Disable Note'), 57 | ), 58 | migrations.AlterField( 59 | model_name='meeting', 60 | name='lock_settings_disable_private_chat', 61 | field=models.BooleanField(default=False, help_text='if True will disable private chats in the meeting', verbose_name='Disable Private chat'), 62 | ), 63 | migrations.AlterField( 64 | model_name='meeting', 65 | name='lock_settings_disable_public_chat', 66 | field=models.BooleanField(default=False, help_text='if True will disable public chat in the meeting', verbose_name='Disable public chat'), 67 | ), 68 | migrations.AlterField( 69 | model_name='meeting', 70 | name='lock_settings_locked_layout', 71 | field=models.BooleanField(default=False, help_text='will lock the layout in the meeting. ', verbose_name='Locked Layout'), 72 | ), 73 | migrations.AlterField( 74 | model_name='meeting', 75 | name='logout_url', 76 | field=models.CharField(blank=True, default='', max_length=200, null=True, verbose_name='URL to visit after user logged out'), 77 | ), 78 | migrations.AlterField( 79 | model_name='meeting', 80 | name='max_participants', 81 | field=models.IntegerField(default=10, verbose_name='Max Participants'), 82 | ), 83 | migrations.AlterField( 84 | model_name='meeting', 85 | name='meeting_id', 86 | field=models.CharField(max_length=100, unique=True, verbose_name='Meeting ID'), 87 | ), 88 | migrations.AlterField( 89 | model_name='meeting', 90 | name='moderator_password', 91 | field=models.CharField(max_length=50, verbose_name='Moderator Password'), 92 | ), 93 | migrations.AlterField( 94 | model_name='meeting', 95 | name='name', 96 | field=models.CharField(max_length=100, verbose_name='Name of Meeting'), 97 | ), 98 | migrations.AlterField( 99 | model_name='meeting', 100 | name='parent_meeting_id', 101 | field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Parent Meeting ID'), 102 | ), 103 | migrations.AlterField( 104 | model_name='meeting', 105 | name='record', 106 | field=models.BooleanField(default=True, verbose_name='Record'), 107 | ), 108 | migrations.AlterField( 109 | model_name='meeting', 110 | name='updated_at', 111 | field=models.DateTimeField(auto_now=True), 112 | ), 113 | migrations.AlterField( 114 | model_name='meeting', 115 | name='voice_bridge', 116 | field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Voice Bridge'), 117 | ), 118 | migrations.AlterField( 119 | model_name='meeting', 120 | name='webcam_only_for_moderators', 121 | field=models.BooleanField(default=False, help_text='will cause all webcams shared by viewers during this meeting to only appear for moderators', verbose_name='Webcam Only for moderators?'), 122 | ), 123 | ] 124 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0009_auto_20201112_1032.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-11-12 10:32 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0008_auto_20201008_1609'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='meeting', 15 | options={'verbose_name': 'Meeting', 'verbose_name_plural': 'Meeting'}, 16 | ), 17 | migrations.AlterModelTable( 18 | name='meeting', 19 | table='meeting', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0010_auto_20201126_0923.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-11-26 09:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0009_auto_20201112_1032'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='meeting', 15 | name='auto_start_recording', 16 | field=models.BooleanField(default=False, verbose_name='Auto Start Recording'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0011_meetinglog.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-12-10 15:21 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('django_bigbluebutton', '0010_auto_20201126_0923'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='MeetingLog', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('fullname', models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='User fullname')), 21 | ('join_date', models.DateTimeField(auto_now_add=True, verbose_name='Join Date')), 22 | ('left_date', models.DateTimeField(blank=True, null=True, verbose_name='Left Date')), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='meeting_log', to=settings.AUTH_USER_MODEL, verbose_name='User')), 26 | ], 27 | options={ 28 | 'verbose_name': 'Meeting Log', 29 | 'verbose_name_plural': 'Meeting Log', 30 | 'db_table': 'meeting_log', 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0012_meetinglog_meeting.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2020-12-10 19:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_bigbluebutton', '0011_meetinglog'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='meetinglog', 16 | name='meeting', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs', to='django_bigbluebutton.Meeting', verbose_name='Meeting'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0013_auto_20201211_0601.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2020-12-11 06:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0012_meetinglog_meeting'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='meetinglog', 15 | name='meeting', 16 | ), 17 | migrations.AddField( 18 | model_name='meetinglog', 19 | name='meeting_id', 20 | field=models.CharField(default='', max_length=100, verbose_name='Meeting ID'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0014_auto_20201211_0920.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2020-12-11 09:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0013_auto_20201211_0601'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meeting', 15 | name='hook_id', 16 | field=models.CharField(blank=True, default='', max_length=50, null=True, verbose_name='Hook ID received from BBB'), 17 | ), 18 | migrations.AddField( 19 | model_name='meeting', 20 | name='hook_url', 21 | field=models.CharField(blank=True, default='', max_length=500, null=True, verbose_name='Hook URL'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0015_auto_20201214_1423.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2020-12-14 14:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0014_auto_20201211_0920'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='meeting', 15 | name='logout_url', 16 | field=models.CharField(blank=True, default='', max_length=500, null=True, verbose_name='URL to visit after user logged out'), 17 | ), 18 | migrations.AlterField( 19 | model_name='meeting', 20 | name='meeting_id', 21 | field=models.CharField(max_length=200, unique=True, verbose_name='Meeting ID'), 22 | ), 23 | migrations.AlterField( 24 | model_name='meeting', 25 | name='name', 26 | field=models.CharField(max_length=200, verbose_name='Name of Meeting'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0016_auto_20210202_1546.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2021-02-02 15:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('django_bigbluebutton', '0015_auto_20201214_1423'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='meetinglog', 16 | name='meeting_id', 17 | ), 18 | migrations.AddField( 19 | model_name='meetinglog', 20 | name='meeting', 21 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs', to='django_bigbluebutton.Meeting', verbose_name='Meeting'), 22 | ), 23 | migrations.CreateModel( 24 | name='MeetingRecord', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('record_id', models.CharField(max_length=255, unique=True, verbose_name='Record ID')), 28 | ('meeting', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='records', to='django_bigbluebutton.Meeting', verbose_name='Meeting')), 29 | ], 30 | options={ 31 | 'verbose_name': 'Meeting Record', 32 | 'verbose_name_plural': 'Meeting Record', 33 | 'db_table': 'meeting_record', 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/0017_auto_20210202_1600.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2021-02-02 16:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('django_bigbluebutton', '0016_auto_20210202_1546'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='meetingrecord', 15 | name='link', 16 | field=models.CharField(blank=True, max_length=500, null=True, verbose_name='Link'), 17 | ), 18 | migrations.AddField( 19 | model_name='meetingrecord', 20 | name='name', 21 | field=models.CharField(default='', max_length=255, verbose_name='Record name'), 22 | ), 23 | migrations.AlterField( 24 | model_name='meeting', 25 | name='name', 26 | field=models.CharField(max_length=255, verbose_name='Meeting Name'), 27 | ), 28 | migrations.AlterField( 29 | model_name='meetingrecord', 30 | name='record_id', 31 | field=models.CharField(blank=True, db_index=True, max_length=255, null=True, verbose_name='Record ID'), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /django_bigbluebutton/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Execut3/django-bigbluebutton/4cb2519a28c1e2024094ef9171ade0b0084b839d/django_bigbluebutton/migrations/__init__.py -------------------------------------------------------------------------------- /django_bigbluebutton/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import datetime 3 | 4 | from django.db import models 5 | from django.contrib.auth import get_user_model 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from .settings import * 9 | from .bbb import BigBlueButton 10 | from .utils import xml_to_json 11 | 12 | User = get_user_model() 13 | 14 | 15 | class Meeting(models.Model): 16 | """ This models hold information about each meeting room. 17 | When creating a big blue button room with BBB APIs, 18 | Will store it's info here for later usages. 19 | """ 20 | name = models.CharField( 21 | max_length=255, 22 | verbose_name=_('Meeting Name') 23 | ) 24 | meeting_id = models.CharField( 25 | max_length=200, unique=True, 26 | verbose_name=_('Meeting ID') 27 | ) 28 | attendee_password = models.CharField( 29 | max_length=50, 30 | verbose_name=_('Attendee Password') 31 | ) 32 | moderator_password = models.CharField( 33 | max_length=50, 34 | verbose_name=_('Moderator Password') 35 | ) 36 | is_running = models.BooleanField( 37 | default=False, 38 | verbose_name=_('Is running'), 39 | help_text=_('Indicates whether this meeting is running in BigBlueButton or not!') 40 | ) 41 | 42 | # Configs 43 | max_participants = models.IntegerField( 44 | default=10, 45 | verbose_name=_('Max Participants') 46 | ) 47 | welcome_text = models.TextField( 48 | default=_('Welcome!'), 49 | verbose_name=_('Meeting Text in Bigbluebutton') 50 | ) 51 | logout_url = models.CharField( 52 | max_length=500, 53 | default='', null=True, blank=True, 54 | verbose_name=_('URL to visit after user logged out') 55 | ) 56 | record = models.BooleanField( 57 | default=True, 58 | verbose_name=_('Record') 59 | ) 60 | auto_start_recording = models.BooleanField( 61 | default=False, 62 | verbose_name=_('Auto Start Recording') 63 | ) 64 | allow_start_stop_recording = models.BooleanField( 65 | default=True, 66 | verbose_name=_('Allow Stop/Start Recording'), 67 | help_text=_('Allow the user to start/stop recording. (default true)') 68 | ) 69 | webcam_only_for_moderators = models.BooleanField( 70 | default=False, 71 | verbose_name=_('Webcam Only for moderators?'), 72 | help_text=_('will cause all webcams shared by viewers ' 73 | 'during this meeting to only appear for moderators') 74 | ) 75 | 76 | # Lock settings 77 | lock_settings_disable_cam = models.BooleanField( 78 | default=False, 79 | verbose_name=_('Disable Camera'), 80 | help_text=_('will prevent users from sharing their camera in the meeting') 81 | ) 82 | lock_settings_disable_mic = models.BooleanField( 83 | default=False, 84 | verbose_name=_('Disable Mic'), 85 | help_text=_('will only allow user to join listen only') 86 | ) 87 | lock_settings_disable_private_chat = models.BooleanField( 88 | default=False, 89 | verbose_name=_('Disable Private chat'), 90 | help_text=_('if True will disable private chats in the meeting') 91 | ) 92 | lock_settings_disable_public_chat = models.BooleanField( 93 | default=False, 94 | verbose_name=_('Disable public chat'), 95 | help_text=_('if True will disable public chat in the meeting') 96 | ) 97 | lock_settings_disable_note = models.BooleanField( 98 | default=False, 99 | verbose_name=_('Disable Note'), 100 | help_text=_('if True will disable notes in the meeting.') 101 | ) 102 | lock_settings_locked_layout = models.BooleanField( 103 | default=False, 104 | verbose_name=_('Locked Layout'), 105 | help_text=_('will lock the layout in the meeting. ') 106 | ) 107 | 108 | # Not important Info 109 | parent_meeting_id = models.CharField( 110 | null=True, 111 | blank=True, 112 | max_length=100, 113 | verbose_name=_('Parent Meeting ID') 114 | ) 115 | internal_meeting_id = models.CharField( 116 | null=True, 117 | blank=True, 118 | max_length=100, 119 | verbose_name=_('Internal Meeting ID') 120 | ) 121 | voice_bridge = models.CharField( 122 | max_length=50, 123 | null=True, blank=True, 124 | verbose_name=_('Voice Bridge') 125 | ) 126 | 127 | # Hook related info 128 | hook_id = models.CharField( 129 | null=True, blank=True, 130 | max_length=50, default='', 131 | verbose_name=_('Hook ID received from BBB') 132 | ) 133 | hook_url = models.CharField( 134 | default='', 135 | max_length=500, 136 | null=True, blank=True, 137 | verbose_name=_('Hook URL') 138 | ) 139 | 140 | # Time related Info 141 | created_at = models.DateTimeField(auto_now_add=True) 142 | updated_at = models.DateTimeField(auto_now=True) 143 | 144 | def __str__(self): 145 | return '{}-{}'.format(self.id, self.name) 146 | 147 | class Meta: 148 | db_table = 'meeting' 149 | verbose_name = 'Meeting' 150 | verbose_name_plural = _('Meeting') 151 | 152 | def save(self, force_insert=False, force_update=False, using=None, 153 | update_fields=None): 154 | if not self.name: 155 | self.name = self.meeting_id 156 | super(Meeting, self).save() 157 | 158 | @property 159 | def info(self): 160 | # Will return result of bbb.get_meeting_info 161 | return BigBlueButton().meeting_info( 162 | self.meeting_id, 163 | self.moderator_password 164 | ) 165 | 166 | def check_is_running(self, commit=True): 167 | """ Call bbb is_running method, and see if this meeting_id is running! """ 168 | is_running = BigBlueButton().is_running(self.meeting_id) 169 | self.is_running = True if is_running in ['true', True, 'True'] else False 170 | if commit: 171 | self.save() 172 | return self.is_running 173 | 174 | def start(self): 175 | """ Will start already created meeting again. """ 176 | result = BigBlueButton().start( 177 | name=self.name, 178 | meeting_id=self.meeting_id, 179 | attendee_password=self.attendee_password, 180 | moderator_password=self.moderator_password 181 | ) 182 | 183 | if result: 184 | # It's better to create hook again, 185 | # So if by any reason is removed from bbb, again be created 186 | # If already exist will just give warning and will be ignored 187 | self.create_hook() 188 | 189 | return result 190 | 191 | def end(self): 192 | # If successfully ended, will return True 193 | ended = BigBlueButton().end_meeting( 194 | meeting_id=self.meeting_id, 195 | password=self.moderator_password 196 | ) 197 | ended = True if ended == True else False 198 | if ended: 199 | self.is_running = False 200 | self.save() 201 | 202 | # Now send a signal so other apps also be notified 203 | from .signals import meeting_ended 204 | meeting_ended.send(sender=self) 205 | return ended 206 | 207 | def create_join_link(self, fullname, role='moderator', **kwargs): 208 | pw = self.moderator_password if role == 'moderator' else self.attendee_password 209 | link = BigBlueButton().join_url(self.meeting_id, fullname, pw, **kwargs) 210 | return link 211 | 212 | def create_hook(self): 213 | """ By calling this method, will create a hook to callback-url for this meeting-id 214 | 215 | TODO: Maybe it's better to delete hooks first then create new one. 216 | """ 217 | callback_url = settings.BBB_CALLBACK_URL 218 | if callback_url: 219 | try: 220 | # Be noted meeting_id is different from id, 221 | # meeting_id is like meeting-11 (value to know in bbb) 222 | # id is just primary key for Meeting instance 223 | if not self.hook_url: 224 | self.hook_url = '{callback}/api/meeting/{id}/callback/'.format( 225 | id=self.id, 226 | callback=str(callback_url).rstrip('/'), 227 | ) 228 | output = BigBlueButton().create_hook(self.hook_url, self.meeting_id) 229 | if output.get('hook_id'): 230 | self.hook_id = output['hook_id'] 231 | self.save() 232 | except Exception as e: 233 | error_msg = 'Error in setting api hook for meeting: {}, {}'.format(self.meeting_id, str(e)) 234 | logging.error(error_msg) 235 | 236 | def delete_hook(self): 237 | if self.hook_id: 238 | BigBlueButton().destroy_hook(self.hook_id) 239 | 240 | def get_report(self): 241 | """ This method will return useful info about participants in meeting. 242 | 243 | return will be like this: 244 | 245 | { 246 | "activity_logs": [ 247 | { 248 | 'user': { 249 | 'id': '432', 250 | 'fullname': 'Execut3' 251 | }, 252 | 'join_date': '2020-10-20', 253 | 'left_date': None 254 | } 255 | ], 256 | "presence_time_logs": { 257 | '432': { 258 | 'duration': 123, 259 | 'fullname': 'Execut3' 260 | } 261 | } 262 | } 263 | """ 264 | logs = MeetingLog.objects. \ 265 | filter(meeting__meeting_id=self.meeting_id). \ 266 | select_related('user'). \ 267 | order_by('-updated_at') 268 | logs = list(logs) # Convert to list 269 | 270 | # A dict which it's keys are user_id s (overall seconds each user was in meeting) 271 | presence_time_logs = {} 272 | 273 | # A list of logs of users in the meeting. for example when they joined, when left and ... 274 | activity_logs = [] 275 | 276 | # Now iterate on logs and calc duration and fill update variables. 277 | for log in logs: 278 | try: 279 | userkey = log.fullname 280 | if log.user: 281 | userkey = log.user.id 282 | userkey = str(userkey) 283 | 284 | # Now will try to find diff of left_date and join date 285 | # To calc duration of log. If already not left, will check 286 | # current date for left date. If error will be 0 287 | try: 288 | if log.left_date: 289 | duration = (log.left_date - log.join_date).seconds 290 | else: 291 | now = datetime.datetime.now() 292 | duration = (now - log.join_date).seconds 293 | except: 294 | duration = 0 295 | 296 | if userkey in presence_time_logs.keys(): 297 | try: 298 | c = presence_time_logs[userkey]['duration'] 299 | except Exception as e: 300 | print(e) 301 | c = 0 302 | try: 303 | presence_time_logs[userkey]['duration'] = c + duration 304 | except: 305 | presence_time_logs[userkey] = { 306 | 'duration': duration, 307 | 'fullname': log.fullname 308 | } 309 | else: 310 | presence_time_logs[userkey] = { 311 | 'duration': duration, 312 | 'fullname': log.fullname 313 | } 314 | 315 | tmp = { 316 | 'user': { 317 | 'id': userkey, 318 | 'fullname': log.fullname 319 | }, 320 | 'join_date': log.join_date, 321 | 'left_date': log.left_date 322 | } 323 | activity_logs.append(tmp) 324 | except Exception as e: 325 | logging.error(str(e)) 326 | 327 | return { 328 | 'activity_logs': activity_logs, 329 | 'presence_time_logs': presence_time_logs, 330 | } 331 | 332 | @classmethod 333 | def create(cls, name, meeting_id, **kwargs): 334 | kwargs.update({ 335 | 'record': kwargs.get('record', BBB_RECORD), 336 | 'logout_url': kwargs.get('logout_url', BBB_LOGOUT_URL), 337 | 'welcome_text': kwargs.get('welcome_text', BBB_WELCOME_TEXT), 338 | 'auto_start_recording': kwargs.get('auto_start_recording', BBB_AUTO_RECORDING), 339 | 'allow_start_stop_recording': kwargs.get('allow_start_stop_recording', BBB_ALLOW_START_STOP_RECORDING), 340 | }) 341 | 342 | m_xml = BigBlueButton().start(name=name, meeting_id=meeting_id, **kwargs) 343 | print(m_xml) 344 | meeting_json = xml_to_json(m_xml) 345 | if meeting_json['returncode'] != 'SUCCESS': 346 | raise ValueError('Unable to create meeting!') 347 | 348 | # Now create a model for it. 349 | meeting, _ = Meeting.objects.get_or_create(meeting_id=meeting_id) 350 | 351 | meeting.name = name 352 | meeting.is_running = True 353 | meeting.record = kwargs.get('record', True) 354 | meeting.logout_url = kwargs.get('logout_url', '') 355 | meeting.voice_bridge = meeting_json['voiceBridge'] 356 | meeting.attendee_password = meeting_json['attendeePW'] 357 | meeting.moderator_password = meeting_json['moderatorPW'] 358 | meeting.parent_meeting_id = meeting_json['parentMeetingID'] 359 | meeting.internal_meeting_id = meeting_json['internalMeetingID'] 360 | meeting.welcome_text = kwargs.get('welcome_text', BBB_WELCOME_TEXT) 361 | meeting.auto_start_recording = kwargs.get('auto_start_recording', True) 362 | meeting.allow_start_stop_recording = kwargs.get('allow_start_stop_recording', True) 363 | meeting.save() 364 | 365 | # Register a hook for this meeting 366 | meeting.create_hook() 367 | 368 | return meeting 369 | 370 | @classmethod 371 | def update_running_meetings(cls): 372 | """ This method will call bigbluebutton, 373 | fetch running meetings on bbb, and update local 374 | database with running meetings info. """ 375 | running_meetings = BigBlueButton().get_meetings() 376 | 377 | """ A sample response from bigbluebutton.getMeeting(): 378 | [ 379 | { 380 | 'name': 'meeting-10', 'running': 'true', 381 | 'moderator_pw': 'mp', 'attendee_pw': 'ap', 382 | 'info': { 383 | 'start_time': '1599199672948', 'end_time': '0', 384 | 'participant_count': '1', 'moderator_count': '1', 385 | 'moderator_pw': 'mp', 'attendee_pw': 'ap' 386 | } 387 | } 388 | ] 389 | """ 390 | 391 | try: 392 | # First get list of running meetings from bbb 393 | meetings_id_list = [item['meeting_id'] for item in running_meetings] 394 | 395 | # Now update all to not running 396 | Meeting.objects.all().update(is_running=False) 397 | 398 | # Find meetings with proper id_list running, and update their model status to running 399 | Meeting.objects.filter(meeting_id__in=meetings_id_list).update(is_running=True) 400 | except Exception as e: 401 | logging.error('[-] Exception in update_running_meetings, {}'.format(str(e))) 402 | 403 | 404 | class MeetingRecord(models.Model): 405 | """ Will hold recorded sessions of each meeting in this model. 406 | """ 407 | meeting = models.ForeignKey( 408 | null=True, 409 | blank=True, 410 | to=Meeting, 411 | db_index=True, 412 | related_name='records', 413 | verbose_name=_('Meeting'), 414 | on_delete=models.SET_NULL, 415 | ) 416 | name = models.CharField( 417 | default='', 418 | max_length=255, 419 | verbose_name=_('Record name') 420 | ) 421 | record_id = models.CharField( 422 | null=True, 423 | blank=True, 424 | db_index=True, 425 | max_length=255, 426 | verbose_name=_('Record ID'), 427 | ) 428 | link = models.CharField( 429 | null=True, 430 | blank=True, 431 | max_length=500, 432 | verbose_name=_('Link'), 433 | ) 434 | 435 | def __str__(self): 436 | return '{}, {}'.format(self.meeting.name, self.record_id) 437 | 438 | class Meta: 439 | db_table = 'meeting_record' 440 | verbose_name = 'Meeting Record' 441 | verbose_name_plural = _("Meeting Record") 442 | 443 | 444 | class MeetingLog(models.Model): 445 | """ Will store detail logs about user joins and disconnects. 446 | 447 | By joining each user, a new record will be created, 448 | and when user left the meeting or the meeting ended, 449 | It will close that log and set the end_date for it. 450 | """ 451 | user = models.ForeignKey( 452 | to=User, 453 | null=True, 454 | blank=True, 455 | db_index=True, 456 | verbose_name=_('User'), 457 | on_delete=models.SET_NULL, 458 | related_name='meeting_log', 459 | ) # It can be null 460 | fullname = models.CharField( 461 | null=True, 462 | blank=True, 463 | default='', 464 | max_length=50, 465 | verbose_name=_('User fullname') 466 | ) 467 | meeting = models.ForeignKey( 468 | null=True, 469 | blank=True, 470 | to=Meeting, 471 | related_name='logs', 472 | verbose_name=_('Meeting'), 473 | on_delete=models.SET_NULL, 474 | ) 475 | join_date = models.DateTimeField( 476 | auto_now_add=True, 477 | verbose_name=_('Join Date') 478 | ) 479 | left_date = models.DateTimeField( 480 | null=True, 481 | blank=True, 482 | verbose_name=_('Left Date') 483 | ) 484 | 485 | # Time related Info 486 | created_at = models.DateTimeField(auto_now_add=True) 487 | updated_at = models.DateTimeField(auto_now=True) 488 | 489 | def __str__(self): 490 | return '{}-{}'.format(self.id, self.fullname) 491 | 492 | class Meta: 493 | db_table = 'meeting_log' 494 | verbose_name = 'Meeting Log' 495 | verbose_name_plural = _('Meeting Log') 496 | 497 | def save(self, force_insert=False, force_update=False, using=None, 498 | update_fields=None): 499 | 500 | # If already created, should update all prev logs for this meeting and set their left_date 501 | if not self.pk: 502 | now_date = datetime.datetime.now() 503 | MeetingLog.objects.filter( 504 | user=self.user, 505 | meeting_id=self.meeting_id, 506 | left_date__isnull=True 507 | ).update(left_date=now_date) 508 | 509 | if self.user: 510 | self.fullname = self.user.fullname 511 | 512 | super(MeetingLog, self).save() 513 | -------------------------------------------------------------------------------- /django_bigbluebutton/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | """ Below two variables are really important and should be set in project!""" 5 | BBB_API_URL = getattr(settings, 'BBB_API_URL', None) 6 | BBB_SECRET_KEY = getattr(settings, 'BBB_SECRET_KEY', None) 7 | 8 | BBB_CALLBACK_URL = getattr(settings, 'BBB_CALLBACK_URL', None) 9 | 10 | 11 | """ UPDATE_RUNNING_ON_EACH_CALL: 12 | 13 | If UPDATE_RUNNING_ON_EACH_CALL=True, means that on each call of queryset for 14 | example getting list of meetings and ..., call getMeetings from bigbluebutton 15 | And update status of each meetings. So sync local database Meetings is_running 16 | variable with getMeetings() output from bigbluebutton APIs. 17 | If UPDATE_RUNNING_ON_EACH_CALL=False, skip and only update is_running status 18 | when Refresh Meeting action is called from django admin. 19 | """ 20 | UPDATE_RUNNING_ON_EACH_CALL = getattr(settings, 'UPDATE_RUNNING_ON_EACH_CALL', True) 21 | 22 | 23 | """ Other default variables for each meeting """ 24 | BBB_MAX_PARTICIPANTS = getattr(settings, 'BBB_MAX_PARTICIPANTS', 10) 25 | BBB_WELCOME_TEXT = getattr(settings, 'BBB_WELCOME_TEXT', 'Welcome to meeting') 26 | BBB_LOGOUT_URL = getattr(settings, 'BBB_LOGOUT_URL', BBB_API_URL) 27 | BBB_RECORD = getattr(settings, 'BBB_RECORD', True) 28 | BBB_AUTO_RECORDING = getattr(settings, 'BBB_AUTO_RECORDING', False) 29 | BBB_ALLOW_START_STOP_RECORDING = getattr(settings, 'BBB_ALLOW_START_STOP_RECORDING', True) 30 | BBB_WEBCAM_ONLY_FOR_MODS = getattr(settings, 'BBB_WEBCAM_ONLY_FOR_MODS', False) 31 | BBB_LOCK_SETTINGS_DISABLE_CAM = getattr(settings, 'BBB_LOCK_SETTINGS_DISABLE_CAM', False) 32 | BBB_LOCK_SETTINGS_DISABLE_MIC = getattr(settings, 'BBB_LOCK_SETTINGS_DISABLE_MIC', False) 33 | BBB_LOCK_SETTINGS_DISABLE_PRIVATE_CHAT = getattr(settings, 'BBB_LOCK_SETTINGS_DISABLE_PRIVATE_CHAT', False) 34 | BBB_LOCK_SETTINGS_DISABLE_PUBLIC_CHAT = getattr(settings, 'BBB_LOCK_SETTINGS_DISABLE_PUBLIC_CHAT', False) 35 | BBB_LOCK_SETTINGS_DISABLE_NOTE = getattr(settings, 'BBB_LOCK_SETTINGS_DISABLE_NOTE', False) 36 | BBB_LOCK_SETTINGS_LOCKED_LAYOUT = getattr(settings, 'BBB_LOCK_SETTINGS_LOCKED_LAYOUT', False) 37 | BBB_COPYRIGHT_TEXT = getattr(settings, 'BBB_COPYRIGHT_TEXT', 'Copyright to Execut3 (CPOL Co)') 38 | -------------------------------------------------------------------------------- /django_bigbluebutton/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | 4 | meeting_ended = Signal() 5 | -------------------------------------------------------------------------------- /django_bigbluebutton/tasks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Here some sample methods to call in other apps, 3 | projects to do some syncing functions. 4 | 5 | Like updating records of meetings, update logs and ... 6 | """ 7 | import logging 8 | import datetime 9 | 10 | from .bbb import BigBlueButton 11 | from .models import Meeting, MeetingLog, MeetingRecord 12 | 13 | 14 | def update_meetings_logs(): 15 | """ Will iterate on all meetings, and check if they are closed, 16 | will close meetingLogs joined to them. 17 | """ 18 | logging.info('[+] Check meetings and close left out meetingLogs.') 19 | 20 | meetings = Meeting.objects.all() 21 | for meeting in meetings: 22 | try: 23 | is_running = meeting.check_is_running(commit=True) 24 | if not is_running: 25 | now_date = datetime.datetime.now() 26 | MeetingLog.objects.filter( 27 | meeting__meeting_id=meeting.meeting_id, 28 | left_date__isnull=True 29 | ).update(left_date=now_date) 30 | except Exception as e: 31 | logging.error(str(e)) 32 | logging.info('[+] Done checking meetings and updating logs for them.') 33 | 34 | 35 | def update_meetings_records(): 36 | """ 37 | Will iterate on meetings, 38 | check if new record is created for them by bbb, 39 | then save it in database for showing to customers. 40 | """ 41 | 42 | logging.info('[+] Checking meetings and store new records if created any!') 43 | 44 | meetings = Meeting.objects.all() 45 | for meeting in meetings: 46 | try: 47 | bbb = BigBlueButton() 48 | recordings = bbb.get_meeting_records(meeting.meeting_id) 49 | for record in recordings: 50 | """ record sample: 51 | { 52 | 'url': 'https://meeting.cpol.co/playback/presentation/2.0/playback.html?meetingId=0c19812ecd8955d77a3351a4e489fe50afcc-1612087443063', 53 | 'name': 'name of meeting', 54 | 'end_time': 1612099936637, 55 | 'raw_size': 165691706, 56 | 'record_id': '0c19812ecd8955d77a3351a4e489fe50afcc-1612087443063', 57 | 'meeting_id': 'meeting-id', 58 | 'start_time': 1612087443063, 59 | } 60 | """ 61 | MeetingRecord.objects.get_or_create( 62 | link=record['url'], 63 | name=record['name'], 64 | record_id=record['record_id'], 65 | meeting=meeting, 66 | ) 67 | except Exception as e: 68 | logging.error(str(e)) 69 | -------------------------------------------------------------------------------- /django_bigbluebutton/templates/admin/meeting_create_link.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n admin_static admin_modify %} 3 | 4 | 5 | {% block content %} 6 |
7 |
8 | {% csrf_token %} 9 | {% if form.non_field_errors|length > 0 %} 10 |

11 | "Please correct the errors below." 12 |

13 | {{ form.non_field_errors }} 14 | {% endif %} 15 |
16 | {% for field in form %} 17 |
18 | {{ field.errors }} 19 | {{ field.label_tag }} 20 | {{ field }} 21 | {% if field.field.help_text %} 22 |

23 | {{ field.field.help_text|safe }} 24 |

25 | {% endif %} 26 |
27 | {% endfor %} 28 |
29 |
30 | 31 |
32 |
33 | 34 | {% if link %} 35 |

36 | Access Link: 37 | 38 | {{ link }} 39 | 40 |

41 | {% endif %} 42 |
43 | {% endblock %} -------------------------------------------------------------------------------- /django_bigbluebutton/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .models import Meeting 4 | from .bbb import BigBlueButton 5 | from .utils import xml_to_json 6 | 7 | 8 | class BBBTest(TestCase): 9 | 10 | def test_get_meetings(self): 11 | """ Test if BigBlueButton's get_meeting method is working or not. 12 | It should return list of all running meetings right now!. """ 13 | meetings = BigBlueButton().get_meetings() 14 | print(meetings) 15 | 16 | # If error in getMeetings() will return 'error' value instead of list of meeting rooms 17 | self.assertTrue(meetings != 'error') 18 | 19 | self.assertTrue(type(meetings) == list) 20 | 21 | def test_create_meeting(self): 22 | """ Will try to create a meeting with bbb. 23 | 24 | Example output as json from bbb 'create' command: 25 | 26 | {'returncode': 'SUCCESS', 'meetingID': 'test', 27 | 'internalMeetingID': 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3-1598891360456', 28 | 'parentMeetingID': 'bbb-none', 'attendeePW': 'ap', 'moderatorPW': 'mp', 29 | 'createTime': '1598891360456', 'voiceBridge': '73362', 'dialNumber': '613-555-1234', 30 | 'createDate': 'Mon Aug 31 12:29:20 EDT 2020', 'hasUserJoined': 'false', 31 | 'duration': '0', 'hasBeenForciblyEnded': 'false', 'messageKey': None, 'message': None} 32 | """ 33 | meeting_name = 'test' 34 | meeting_id = 'test' 35 | meeting_welcome = 'test meeting welcome!' 36 | 37 | # First step is to request BBB and create a meeting 38 | m_xml = BigBlueButton().start( 39 | name=meeting_name, 40 | meeting_id=meeting_id, 41 | welcome=meeting_welcome 42 | ) 43 | meeting_json = xml_to_json(m_xml) 44 | self.assertTrue(meeting_json['returncode'] == 'SUCCESS') 45 | self.assertTrue(meeting_json['meetingID'] == meeting_id) 46 | 47 | # Now create a model for it. 48 | current_meetings = Meeting.objects.count() 49 | meeting, _ = Meeting.objects.get_or_create(meeting_id=meeting_json['meetingID']) 50 | meeting.meeting_id = meeting_json['meetingID'] 51 | meeting.name = meeting_name 52 | meeting.welcome_text = meeting_json['meetingID'] 53 | meeting.attendee_password = meeting_json['attendeePW'] 54 | meeting.moderator_password = meeting_json['moderatorPW'] 55 | meeting.internal_meeting_id = meeting_json['internalMeetingID'] 56 | meeting.parent_meeting_id = meeting_json['parentMeetingID'] 57 | meeting.voice_bridge = meeting_json['voiceBridge'] 58 | meeting.save() 59 | 60 | self.assertFalse(Meeting.objects.count() == current_meetings) 61 | 62 | def test_create_meeting2(self): 63 | """ Will just call cls method in Meeting model. """ 64 | meeting_name = 'test2' 65 | meeting_id = 'test2' 66 | meeting_welcome = 'test meeting welcome!' 67 | m = Meeting.create(meeting_name, meeting_id, meeting_welcome=meeting_welcome) 68 | self.assertTrue(type(m) == Meeting) 69 | 70 | def test_create_and_join_meeting(self): 71 | """ Will just call cls method in Meeting model. """ 72 | meeting_name = 'test' 73 | meeting_id = 'testtttt' 74 | meeting_welcome = 'test meeting welcome!' 75 | meeting = Meeting.create(meeting_name, meeting_id, meeting_welcome=meeting_welcome) 76 | 77 | b = BigBlueButton().join_url(meeting.meeting_id, 'Moderator of Class', meeting.moderator_password) 78 | print('As moderator: {}'.format(b)) 79 | # It will print a link. join with it and see if it's ok or not! 80 | 81 | b = BigBlueButton().join_url(meeting.meeting_id, 'reza torkaman ahmadi', meeting.attendee_password) 82 | print('As attendee: {}'.format(b)) 83 | 84 | def test_join_existing_meeting(self): 85 | meeting_id = 'ranxbqe6jfh1g53ymcnfr2p8elhcduoxsklwb2kr' 86 | b = BigBlueButton().join_url(meeting_id, 'Test User', 'dYHwpBBjlZoI') 87 | print(b) 88 | 89 | def test_end_meeting(self): 90 | meeting_name = 'test' 91 | meeting_id = 'test' 92 | meeting_welcome = 'test meeting welcome!' 93 | meeting = Meeting.create(meeting_name, meeting_id, meeting_welcome=meeting_welcome) 94 | 95 | status = BigBlueButton().end_meeting('test', meeting.moderator_password) 96 | print(status) 97 | 98 | def test_hook_create(self): 99 | meeting_id = 'testmeeting-11' 100 | Meeting.create('test meeting', meeting_id, meeting_welcome='welcome') 101 | result = BigBlueButton().create_hook('https://webhook.site/7897a4f6-9388-4335-8055-d4550ba39fea', meeting_id) 102 | self.assertTrue(result) 103 | 104 | def test_hook_delete(self): 105 | meeting_id = 'testmeeting-11' 106 | 107 | # First create a meeting 108 | Meeting.create('test meeting', meeting_id, meeting_welcome='welcome') 109 | 110 | # Now create a hook for it 111 | res = BigBlueButton().create_hook('https://webhook.site/7897a4f6-9388-4335-8055-d4550ba39fea', meeting_id) 112 | hook_id = res['hook_id'] 113 | 114 | # Now try to remove that hook 115 | status = BigBlueButton().destroy_hook(hook_id) 116 | self.assertTrue(status) 117 | 118 | def test_hook_list(self): 119 | meeting_id = 'testmeeting-11' 120 | result = BigBlueButton().get_hooks() 121 | len1 = len(result) 122 | 123 | # Now create a new hook 124 | Meeting.create('test meeting', meeting_id, meeting_welcome='welcome') 125 | BigBlueButton().create_hook('https://webhook.site/7897a4f6-9388-4335-8055-d4550ba39fea', meeting_id) 126 | 127 | # Now check new list of hooks see if it is increased 128 | result = BigBlueButton().get_hooks() 129 | len2 = len(result) 130 | 131 | self.assertTrue(len1 != len2) 132 | self.assertTrue(len2 - len1 == 1) 133 | 134 | def test_callback_api(self): 135 | data = ''' 136 | [{ 137 | "data":{ 138 | "type":"event", 139 | "id":"user-joined", 140 | "attributes":{ 141 | "meeting":{ 142 | "internal-meeting-id":"914732701ca2fba8097f125f20eff107203b303e-1624350420282", 143 | "external-meeting-id":"meeting-id" 144 | }, 145 | "user":{ 146 | "internal-user-id":"w_eunlmvgzcnlz", 147 | "external-user-id":"161", 148 | "name":"test user", 149 | "role":"VIEWER", 150 | "presenter":False 151 | } 152 | }, 153 | "event":{ 154 | "ts":1624364288797 155 | } 156 | } 157 | }] 158 | ''' 159 | url = 'api/meeting/66/callback/' 160 | res = self.client.post(url, data, content_type='application/json') 161 | print(res.status_code) 162 | print(res.content) 163 | 164 | -------------------------------------------------------------------------------- /django_bigbluebutton/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | urlpatterns = [ 4 | path("api/", include(("django_bigbluebutton.api.urls", "django_bigbluebutton.api.urls"), "api_bbb"), ), 5 | ] 6 | -------------------------------------------------------------------------------- /django_bigbluebutton/utils.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def parse_xml(response): 5 | try: 6 | xml = ET.XML(response) 7 | code = xml.find('returncode').text 8 | if code == 'SUCCESS': 9 | return xml 10 | else: 11 | raise 12 | except: 13 | return None 14 | 15 | 16 | def xml_to_json(xml): 17 | result = {} 18 | for x in xml: 19 | result[x.tag] = x.text 20 | return result 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | README = open(os.path.join(here, 'README.md')).read() 6 | 7 | setup( 8 | name='django-bigbluebutton', 9 | version='0.5.9', 10 | packages=['django_bigbluebutton'], 11 | description='A Django APP to Integrate BigBlueButton APIs with your current Project', 12 | long_description=README, 13 | long_description_content_type='text/markdown', 14 | author='Execut3', 15 | author_email='execut3.binarycodes@gmail.com', 16 | url='https://github.com/Execut3/django-bigbluebutton', 17 | license='GPT', 18 | install_requires=[ 19 | 'Django>=2.0', 20 | 'requests>=2.0', 21 | 'djangorestframework>=3.0.0' 22 | ], 23 | include_package_data=True, 24 | ) 25 | --------------------------------------------------------------------------------