├── .gitignore
├── LICENSE.md
├── README.md
├── config.yaml
├── mxpp
├── __init__.py
├── client_xmpp.py
└── main.py
└── requirements.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | __pycache__/
3 | *.pyc
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | GNU Affero General Public License
2 | =================================
3 |
4 | _Version 3, 19 November 2007_
5 | _Copyright © 2007 Free Software Foundation, Inc. <>_
6 |
7 | Everyone is permitted to copy and distribute verbatim copies
8 | of this license document, but changing it is not allowed.
9 |
10 | ## Preamble
11 |
12 | The GNU Affero General Public License is a free, copyleft license for
13 | software and other kinds of works, specifically designed to ensure
14 | cooperation with the community in the case of network server software.
15 |
16 | The licenses for most software and other practical works are designed
17 | to take away your freedom to share and change the works. By contrast,
18 | our General Public Licenses are intended to guarantee your freedom to
19 | share and change all versions of a program--to make sure it remains free
20 | software for all its users.
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 | Developers that use our General Public Licenses protect your rights
30 | with two steps: **(1)** assert copyright on the software, and **(2)** offer
31 | you this License which gives you legal permission to copy, distribute
32 | and/or modify the software.
33 |
34 | A secondary benefit of defending all users' freedom is that
35 | improvements made in alternate versions of the program, if they
36 | receive widespread use, become available for other developers to
37 | incorporate. Many developers of free software are heartened and
38 | encouraged by the resulting cooperation. However, in the case of
39 | software used on network servers, this result may fail to come about.
40 | The GNU General Public License permits making a modified version and
41 | letting the public access it on a server without ever releasing its
42 | source code to the public.
43 |
44 | The GNU Affero General Public License is designed specifically to
45 | ensure that, in such cases, the modified source code becomes available
46 | to the community. It requires the operator of a network server to
47 | provide the source code of the modified version running there to the
48 | users of that server. Therefore, public use of a modified version, on
49 | a publicly accessible server, gives the public access to the source
50 | code of the modified version.
51 |
52 | An older license, called the Affero General Public License and
53 | published by Affero, was designed to accomplish similar goals. This is
54 | a different license, not a version of the Affero GPL, but Affero has
55 | released a new version of the Affero GPL which permits relicensing under
56 | this license.
57 |
58 | The precise terms and conditions for copying, distribution and
59 | modification follow.
60 |
61 | ## TERMS AND CONDITIONS
62 |
63 | ### 0. Definitions
64 |
65 | “This License” refers to version 3 of the GNU Affero General Public License.
66 |
67 | “Copyright” also means copyright-like laws that apply to other kinds of
68 | works, such as semiconductor masks.
69 |
70 | “The Program” refers to any copyrightable work licensed under this
71 | License. Each licensee is addressed as “you”. “Licensees” and
72 | “recipients” may be individuals or organizations.
73 |
74 | To “modify” a work means to copy from or adapt all or part of the work
75 | in a fashion requiring copyright permission, other than the making of an
76 | exact copy. The resulting work is called a “modified version” of the
77 | earlier work or a work “based on” the earlier work.
78 |
79 | A “covered work” means either the unmodified Program or a work based
80 | on the Program.
81 |
82 | To “propagate” a work means to do anything with it that, without
83 | permission, would make you directly or secondarily liable for
84 | infringement under applicable copyright law, except executing it on a
85 | computer or modifying a private copy. Propagation includes copying,
86 | distribution (with or without modification), making available to the
87 | public, and in some countries other activities as well.
88 |
89 | To “convey” a work means any kind of propagation that enables other
90 | parties to make or receive copies. Mere interaction with a user through
91 | a computer network, with no transfer of a copy, is not conveying.
92 |
93 | An interactive user interface displays “Appropriate Legal Notices”
94 | to the extent that it includes a convenient and prominently visible
95 | feature that **(1)** displays an appropriate copyright notice, and **(2)**
96 | tells the user that there is no warranty for the work (except to the
97 | extent that warranties are provided), that licensees may convey the
98 | work under this License, and how to view a copy of this License. If
99 | the interface presents a list of user commands or options, such as a
100 | menu, a prominent item in the list meets this criterion.
101 |
102 | ### 1. Source Code
103 |
104 | The “source code” for a work means the preferred form of the work
105 | for making modifications to it. “Object code” means any non-source
106 | form of a work.
107 |
108 | A “Standard Interface” means an interface that either is an official
109 | standard defined by a recognized standards body, or, in the case of
110 | interfaces specified for a particular programming language, one that
111 | is widely used among developers working in that language.
112 |
113 | The “System Libraries” of an executable work include anything, other
114 | than the work as a whole, that **(a)** is included in the normal form of
115 | packaging a Major Component, but which is not part of that Major
116 | Component, and **(b)** serves only to enable use of the work with that
117 | Major Component, or to implement a Standard Interface for which an
118 | implementation is available to the public in source code form. A
119 | “Major Component”, in this context, means a major essential component
120 | (kernel, window system, and so on) of the specific operating system
121 | (if any) on which the executable work runs, or a compiler used to
122 | produce the work, or an object code interpreter used to run it.
123 |
124 | The “Corresponding Source” for a work in object code form means all
125 | the source code needed to generate, install, and (for an executable
126 | work) run the object code and to modify the work, including scripts to
127 | control those activities. However, it does not include the work's
128 | System Libraries, or general-purpose tools or generally available free
129 | programs which are used unmodified in performing those activities but
130 | which are not part of the work. For example, Corresponding Source
131 | includes interface definition files associated with source files for
132 | the work, and the source code for shared libraries and dynamically
133 | linked subprograms that the work is specifically designed to require,
134 | such as by intimate data communication or control flow between those
135 | subprograms and other parts of the work.
136 |
137 | The Corresponding Source need not include anything that users
138 | can regenerate automatically from other parts of the Corresponding
139 | Source.
140 |
141 | The Corresponding Source for a work in source code form is that
142 | same work.
143 |
144 | ### 2. Basic Permissions
145 |
146 | All rights granted under this License are granted for the term of
147 | copyright on the Program, and are irrevocable provided the stated
148 | conditions are met. This License explicitly affirms your unlimited
149 | permission to run the unmodified Program. The output from running a
150 | covered work is covered by this License only if the output, given its
151 | content, constitutes a covered work. This License acknowledges your
152 | rights of fair use or other equivalent, as provided by copyright law.
153 |
154 | You may make, run and propagate covered works that you do not
155 | convey, without conditions so long as your license otherwise remains
156 | in force. You may convey covered works to others for the sole purpose
157 | of having them make modifications exclusively for you, or provide you
158 | with facilities for running those works, provided that you comply with
159 | the terms of this License in conveying all material for which you do
160 | not control copyright. Those thus making or running the covered works
161 | for you must do so exclusively on your behalf, under your direction
162 | and control, on terms that prohibit them from making any copies of
163 | your copyrighted material outside their relationship with you.
164 |
165 | Conveying under any other circumstances is permitted solely under
166 | the conditions stated below. Sublicensing is not allowed; section 10
167 | makes it unnecessary.
168 |
169 | ### 3. Protecting Users' Legal Rights From Anti-Circumvention Law
170 |
171 | No covered work shall be deemed part of an effective technological
172 | measure under any applicable law fulfilling obligations under article
173 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
174 | similar laws prohibiting or restricting circumvention of such
175 | measures.
176 |
177 | When you convey a covered work, you waive any legal power to forbid
178 | circumvention of technological measures to the extent such circumvention
179 | is effected by exercising rights under this License with respect to
180 | the covered work, and you disclaim any intention to limit operation or
181 | modification of the work as a means of enforcing, against the work's
182 | users, your or third parties' legal rights to forbid circumvention of
183 | technological measures.
184 |
185 | ### 4. Conveying Verbatim Copies
186 |
187 | You may convey verbatim copies of the Program's source code as you
188 | receive it, in any medium, provided that you conspicuously and
189 | appropriately publish on each copy an appropriate copyright notice;
190 | keep intact all notices stating that this License and any
191 | non-permissive terms added in accord with section 7 apply to the code;
192 | keep intact all notices of the absence of any warranty; and give all
193 | recipients a copy of this License along with the Program.
194 |
195 | You may charge any price or no price for each copy that you convey,
196 | and you may offer support or warranty protection for a fee.
197 |
198 | ### 5. Conveying Modified Source Versions
199 |
200 | You may convey a work based on the Program, or the modifications to
201 | produce it from the Program, in the form of source code under the
202 | terms of section 4, provided that you also meet all of these conditions:
203 |
204 | * **a)** The work must carry prominent notices stating that you modified
205 | it, and giving a relevant date.
206 | * **b)** The work must carry prominent notices stating that it is
207 | released under this License and any conditions added under section 7.
208 | This requirement modifies the requirement in section 4 to
209 | “keep intact all notices”.
210 | * **c)** You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 | * **d)** If the work has interactive user interfaces, each must display
218 | Appropriate Legal Notices; however, if the Program has interactive
219 | interfaces that do not display Appropriate Legal Notices, your
220 | work need not make them do so.
221 |
222 | A compilation of a covered work with other separate and independent
223 | works, which are not by their nature extensions of the covered work,
224 | and which are not combined with it such as to form a larger program,
225 | in or on a volume of a storage or distribution medium, is called an
226 | “aggregate” if the compilation and its resulting copyright are not
227 | used to limit the access or legal rights of the compilation's users
228 | beyond what the individual works permit. Inclusion of a covered work
229 | in an aggregate does not cause this License to apply to the other
230 | parts of the aggregate.
231 |
232 | ### 6. Conveying Non-Source Forms
233 |
234 | You may convey a covered work in object code form under the terms
235 | of sections 4 and 5, provided that you also convey the
236 | machine-readable Corresponding Source under the terms of this License,
237 | in one of these ways:
238 |
239 | * **a)** Convey the object code in, or embodied in, a physical product
240 | (including a physical distribution medium), accompanied by the
241 | Corresponding Source fixed on a durable physical medium
242 | customarily used for software interchange.
243 | * **b)** Convey the object code in, or embodied in, a physical product
244 | (including a physical distribution medium), accompanied by a
245 | written offer, valid for at least three years and valid for as
246 | long as you offer spare parts or customer support for that product
247 | model, to give anyone who possesses the object code either **(1)** a
248 | copy of the Corresponding Source for all the software in the
249 | product that is covered by this License, on a durable physical
250 | medium customarily used for software interchange, for a price no
251 | more than your reasonable cost of physically performing this
252 | conveying of source, or **(2)** access to copy the
253 | Corresponding Source from a network server at no charge.
254 | * **c)** Convey individual copies of the object code with a copy of the
255 | written offer to provide the Corresponding Source. This
256 | alternative is allowed only occasionally and noncommercially, and
257 | only if you received the object code with such an offer, in accord
258 | with subsection 6b.
259 | * **d)** Convey the object code by offering access from a designated
260 | place (gratis or for a charge), and offer equivalent access to the
261 | Corresponding Source in the same way through the same place at no
262 | further charge. You need not require recipients to copy the
263 | Corresponding Source along with the object code. If the place to
264 | copy the object code is a network server, the Corresponding Source
265 | may be on a different server (operated by you or a third party)
266 | that supports equivalent copying facilities, provided you maintain
267 | clear directions next to the object code saying where to find the
268 | Corresponding Source. Regardless of what server hosts the
269 | Corresponding Source, you remain obligated to ensure that it is
270 | available for as long as needed to satisfy these requirements.
271 | * **e)** Convey the object code using peer-to-peer transmission, provided
272 | you inform other peers where the object code and Corresponding
273 | Source of the work are being offered to the general public at no
274 | charge under subsection 6d.
275 |
276 | A separable portion of the object code, whose source code is excluded
277 | from the Corresponding Source as a System Library, need not be
278 | included in conveying the object code work.
279 |
280 | A “User Product” is either **(1)** a “consumer product”, which means any
281 | tangible personal property which is normally used for personal, family,
282 | or household purposes, or **(2)** anything designed or sold for incorporation
283 | into a dwelling. In determining whether a product is a consumer product,
284 | doubtful cases shall be resolved in favor of coverage. For a particular
285 | product received by a particular user, “normally used” refers to a
286 | typical or common use of that class of product, regardless of the status
287 | of the particular user or of the way in which the particular user
288 | actually uses, or expects or is expected to use, the product. A product
289 | is a consumer product regardless of whether the product has substantial
290 | commercial, industrial or non-consumer uses, unless such uses represent
291 | the only significant mode of use of the product.
292 |
293 | “Installation Information” for a User Product means any methods,
294 | procedures, authorization keys, or other information required to install
295 | and execute modified versions of a covered work in that User Product from
296 | a modified version of its Corresponding Source. The information must
297 | suffice to ensure that the continued functioning of the modified object
298 | code is in no case prevented or interfered with solely because
299 | modification has been made.
300 |
301 | If you convey an object code work under this section in, or with, or
302 | specifically for use in, a User Product, and the conveying occurs as
303 | part of a transaction in which the right of possession and use of the
304 | User Product is transferred to the recipient in perpetuity or for a
305 | fixed term (regardless of how the transaction is characterized), the
306 | Corresponding Source conveyed under this section must be accompanied
307 | by the Installation Information. But this requirement does not apply
308 | if neither you nor any third party retains the ability to install
309 | modified object code on the User Product (for example, the work has
310 | been installed in ROM).
311 |
312 | The requirement to provide Installation Information does not include a
313 | requirement to continue to provide support service, warranty, or updates
314 | for a work that has been modified or installed by the recipient, or for
315 | the User Product in which it has been modified or installed. Access to a
316 | network may be denied when the modification itself materially and
317 | adversely affects the operation of the network or violates the rules and
318 | protocols for communication across the network.
319 |
320 | Corresponding Source conveyed, and Installation Information provided,
321 | in accord with this section must be in a format that is publicly
322 | documented (and with an implementation available to the public in
323 | source code form), and must require no special password or key for
324 | unpacking, reading or copying.
325 |
326 | ### 7. Additional Terms
327 |
328 | “Additional permissions” are terms that supplement the terms of this
329 | License by making exceptions from one or more of its conditions.
330 | Additional permissions that are applicable to the entire Program shall
331 | be treated as though they were included in this License, to the extent
332 | that they are valid under applicable law. If additional permissions
333 | apply only to part of the Program, that part may be used separately
334 | under those permissions, but the entire Program remains governed by
335 | this License without regard to the additional permissions.
336 |
337 | When you convey a copy of a covered work, you may at your option
338 | remove any additional permissions from that copy, or from any part of
339 | it. (Additional permissions may be written to require their own
340 | removal in certain cases when you modify the work.) You may place
341 | additional permissions on material, added by you to a covered work,
342 | for which you have or can give appropriate copyright permission.
343 |
344 | Notwithstanding any other provision of this License, for material you
345 | add to a covered work, you may (if authorized by the copyright holders of
346 | that material) supplement the terms of this License with terms:
347 |
348 | * **a)** Disclaiming warranty or limiting liability differently from the
349 | terms of sections 15 and 16 of this License; or
350 | * **b)** Requiring preservation of specified reasonable legal notices or
351 | author attributions in that material or in the Appropriate Legal
352 | Notices displayed by works containing it; or
353 | * **c)** Prohibiting misrepresentation of the origin of that material, or
354 | requiring that modified versions of such material be marked in
355 | reasonable ways as different from the original version; or
356 | * **d)** Limiting the use for publicity purposes of names of licensors or
357 | authors of the material; or
358 | * **e)** Declining to grant rights under trademark law for use of some
359 | trade names, trademarks, or service marks; or
360 | * **f)** Requiring indemnification of licensors and authors of that
361 | material by anyone who conveys the material (or modified versions of
362 | it) with contractual assumptions of liability to the recipient, for
363 | any liability that these contractual assumptions directly impose on
364 | those licensors and authors.
365 |
366 | All other non-permissive additional terms are considered “further
367 | restrictions” within the meaning of section 10. If the Program as you
368 | received it, or any part of it, contains a notice stating that it is
369 | governed by this License along with a term that is a further
370 | restriction, you may remove that term. If a license document contains
371 | a further restriction but permits relicensing or conveying under this
372 | License, you may add to a covered work material governed by the terms
373 | of that license document, provided that the further restriction does
374 | not survive such relicensing or conveying.
375 |
376 | If you add terms to a covered work in accord with this section, you
377 | must place, in the relevant source files, a statement of the
378 | additional terms that apply to those files, or a notice indicating
379 | where to find the applicable terms.
380 |
381 | Additional terms, permissive or non-permissive, may be stated in the
382 | form of a separately written license, or stated as exceptions;
383 | the above requirements apply either way.
384 |
385 | ### 8. Termination
386 |
387 | You may not propagate or modify a covered work except as expressly
388 | provided under this License. Any attempt otherwise to propagate or
389 | modify it is void, and will automatically terminate your rights under
390 | this License (including any patent licenses granted under the third
391 | paragraph of section 11).
392 |
393 | However, if you cease all violation of this License, then your
394 | license from a particular copyright holder is reinstated **(a)**
395 | provisionally, unless and until the copyright holder explicitly and
396 | finally terminates your license, and **(b)** permanently, if the copyright
397 | holder fails to notify you of the violation by some reasonable means
398 | prior to 60 days after the cessation.
399 |
400 | Moreover, your license from a particular copyright holder is
401 | reinstated permanently if the copyright holder notifies you of the
402 | violation by some reasonable means, this is the first time you have
403 | received notice of violation of this License (for any work) from that
404 | copyright holder, and you cure the violation prior to 30 days after
405 | your receipt of the notice.
406 |
407 | Termination of your rights under this section does not terminate the
408 | licenses of parties who have received copies or rights from you under
409 | this License. If your rights have been terminated and not permanently
410 | reinstated, you do not qualify to receive new licenses for the same
411 | material under section 10.
412 |
413 | ### 9. Acceptance Not Required for Having Copies
414 |
415 | You are not required to accept this License in order to receive or
416 | run a copy of the Program. Ancillary propagation of a covered work
417 | occurring solely as a consequence of using peer-to-peer transmission
418 | to receive a copy likewise does not require acceptance. However,
419 | nothing other than this License grants you permission to propagate or
420 | modify any covered work. These actions infringe copyright if you do
421 | not accept this License. Therefore, by modifying or propagating a
422 | covered work, you indicate your acceptance of this License to do so.
423 |
424 | ### 10. Automatic Licensing of Downstream Recipients
425 |
426 | Each time you convey a covered work, the recipient automatically
427 | receives a license from the original licensors, to run, modify and
428 | propagate that work, subject to this License. You are not responsible
429 | for enforcing compliance by third parties with this License.
430 |
431 | An “entity transaction” is a transaction transferring control of an
432 | organization, or substantially all assets of one, or subdividing an
433 | organization, or merging organizations. If propagation of a covered
434 | work results from an entity transaction, each party to that
435 | transaction who receives a copy of the work also receives whatever
436 | licenses to the work the party's predecessor in interest had or could
437 | give under the previous paragraph, plus a right to possession of the
438 | Corresponding Source of the work from the predecessor in interest, if
439 | the predecessor has it or can get it with reasonable efforts.
440 |
441 | You may not impose any further restrictions on the exercise of the
442 | rights granted or affirmed under this License. For example, you may
443 | not impose a license fee, royalty, or other charge for exercise of
444 | rights granted under this License, and you may not initiate litigation
445 | (including a cross-claim or counterclaim in a lawsuit) alleging that
446 | any patent claim is infringed by making, using, selling, offering for
447 | sale, or importing the Program or any portion of it.
448 |
449 | ### 11. Patents
450 |
451 | A “contributor” is a copyright holder who authorizes use under this
452 | License of the Program or a work on which the Program is based. The
453 | work thus licensed is called the contributor's “contributor version”.
454 |
455 | A contributor's “essential patent claims” are all patent claims
456 | owned or controlled by the contributor, whether already acquired or
457 | hereafter acquired, that would be infringed by some manner, permitted
458 | by this License, of making, using, or selling its contributor version,
459 | but do not include claims that would be infringed only as a
460 | consequence of further modification of the contributor version. For
461 | purposes of this definition, “control” includes the right to grant
462 | patent sublicenses in a manner consistent with the requirements of
463 | this License.
464 |
465 | Each contributor grants you a non-exclusive, worldwide, royalty-free
466 | patent license under the contributor's essential patent claims, to
467 | make, use, sell, offer for sale, import and otherwise run, modify and
468 | propagate the contents of its contributor version.
469 |
470 | In the following three paragraphs, a “patent license” is any express
471 | agreement or commitment, however denominated, not to enforce a patent
472 | (such as an express permission to practice a patent or covenant not to
473 | sue for patent infringement). To “grant” such a patent license to a
474 | party means to make such an agreement or commitment not to enforce a
475 | patent against the party.
476 |
477 | If you convey a covered work, knowingly relying on a patent license,
478 | and the Corresponding Source of the work is not available for anyone
479 | to copy, free of charge and under the terms of this License, through a
480 | publicly available network server or other readily accessible means,
481 | then you must either **(1)** cause the Corresponding Source to be so
482 | available, or **(2)** arrange to deprive yourself of the benefit of the
483 | patent license for this particular work, or **(3)** arrange, in a manner
484 | consistent with the requirements of this License, to extend the patent
485 | license to downstream recipients. “Knowingly relying” means you have
486 | actual knowledge that, but for the patent license, your conveying the
487 | covered work in a country, or your recipient's use of the covered work
488 | in a country, would infringe one or more identifiable patents in that
489 | country that you have reason to believe are valid.
490 |
491 | If, pursuant to or in connection with a single transaction or
492 | arrangement, you convey, or propagate by procuring conveyance of, a
493 | covered work, and grant a patent license to some of the parties
494 | receiving the covered work authorizing them to use, propagate, modify
495 | or convey a specific copy of the covered work, then the patent license
496 | you grant is automatically extended to all recipients of the covered
497 | work and works based on it.
498 |
499 | A patent license is “discriminatory” if it does not include within
500 | the scope of its coverage, prohibits the exercise of, or is
501 | conditioned on the non-exercise of one or more of the rights that are
502 | specifically granted under this License. You may not convey a covered
503 | work if you are a party to an arrangement with a third party that is
504 | in the business of distributing software, under which you make payment
505 | to the third party based on the extent of your activity of conveying
506 | the work, and under which the third party grants, to any of the
507 | parties who would receive the covered work from you, a discriminatory
508 | patent license **(a)** in connection with copies of the covered work
509 | conveyed by you (or copies made from those copies), or **(b)** primarily
510 | for and in connection with specific products or compilations that
511 | contain the covered work, unless you entered into that arrangement,
512 | or that patent license was granted, prior to 28 March 2007.
513 |
514 | Nothing in this License shall be construed as excluding or limiting
515 | any implied license or other defenses to infringement that may
516 | otherwise be available to you under applicable patent law.
517 |
518 | ### 12. No Surrender of Others' Freedom
519 |
520 | If conditions are imposed on you (whether by court order, agreement or
521 | otherwise) that contradict the conditions of this License, they do not
522 | excuse you from the conditions of this License. If you cannot convey a
523 | covered work so as to satisfy simultaneously your obligations under this
524 | License and any other pertinent obligations, then as a consequence you may
525 | not convey it at all. For example, if you agree to terms that obligate you
526 | to collect a royalty for further conveying from those to whom you convey
527 | the Program, the only way you could satisfy both those terms and this
528 | License would be to refrain entirely from conveying the Program.
529 |
530 | ### 13. Remote Network Interaction; Use with the GNU General Public License
531 |
532 | Notwithstanding any other provision of this License, if you modify the
533 | Program, your modified version must prominently offer all users
534 | interacting with it remotely through a computer network (if your version
535 | supports such interaction) an opportunity to receive the Corresponding
536 | Source of your version by providing access to the Corresponding Source
537 | from a network server at no charge, through some standard or customary
538 | means of facilitating copying of software. This Corresponding Source
539 | shall include the Corresponding Source for any work covered by version 3
540 | of the GNU General Public License that is incorporated pursuant to the
541 | following paragraph.
542 |
543 | Notwithstanding any other provision of this License, you have
544 | permission to link or combine any covered work with a work licensed
545 | under version 3 of the GNU General Public License into a single
546 | combined work, and to convey the resulting work. The terms of this
547 | License will continue to apply to the part which is the covered work,
548 | but the work with which it is combined will remain governed by version
549 | 3 of the GNU General Public License.
550 |
551 | ### 14. Revised Versions of this License
552 |
553 | The Free Software Foundation may publish revised and/or new versions of
554 | the GNU Affero General Public License from time to time. Such new versions
555 | will be similar in spirit to the present version, but may differ in detail to
556 | address new problems or concerns.
557 |
558 | Each version is given a distinguishing version number. If the
559 | Program specifies that a certain numbered version of the GNU Affero General
560 | Public License “or any later version” applies to it, you have the
561 | option of following the terms and conditions either of that numbered
562 | version or of any later version published by the Free Software
563 | Foundation. If the Program does not specify a version number of the
564 | GNU Affero General Public License, you may choose any version ever published
565 | by the Free Software Foundation.
566 |
567 | If the Program specifies that a proxy can decide which future
568 | versions of the GNU Affero General Public License can be used, that proxy's
569 | public statement of acceptance of a version permanently authorizes you
570 | to choose that version for the Program.
571 |
572 | Later license versions may give you additional or different
573 | permissions. However, no additional obligations are imposed on any
574 | author or copyright holder as a result of your choosing to follow a
575 | later version.
576 |
577 | ### 15. Disclaimer of Warranty
578 |
579 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
580 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
581 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY
582 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
583 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
584 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
585 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
586 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
587 |
588 | ### 16. Limitation of Liability
589 |
590 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
591 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
592 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
593 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
594 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
595 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
596 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
597 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
598 | SUCH DAMAGES.
599 |
600 | ### 17. Interpretation of Sections 15 and 16
601 |
602 | If the disclaimer of warranty and limitation of liability provided
603 | above cannot be given local legal effect according to their terms,
604 | reviewing courts shall apply local law that most closely approximates
605 | an absolute waiver of all civil liability in connection with the
606 | Program, unless a warranty or assumption of liability accompanies a
607 | copy of the Program in return for a fee.
608 |
609 | _END OF TERMS AND CONDITIONS_
610 |
611 | ## How to Apply These Terms to Your New Programs
612 |
613 | If you develop a new program, and you want it to be of the greatest
614 | possible use to the public, the best way to achieve this is to make it
615 | free software which everyone can redistribute and change under these terms.
616 |
617 | To do so, attach the following notices to the program. It is safest
618 | to attach them to the start of each source file to most effectively
619 | state the exclusion of warranty; and each file should have at least
620 | the “copyright” line and a pointer to where the full notice is found.
621 |
622 |
623 | Copyright (C)
624 |
625 | This program is free software: you can redistribute it and/or modify
626 | it under the terms of the GNU Affero General Public License as published by
627 | the Free Software Foundation, either version 3 of the License, or
628 | (at your option) any later version.
629 |
630 | This program is distributed in the hope that it will be useful,
631 | but WITHOUT ANY WARRANTY; without even the implied warranty of
632 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
633 | GNU Affero General Public License for more details.
634 |
635 | You should have received a copy of the GNU Affero General Public License
636 | along with this program. If not, see .
637 |
638 | Also add information on how to contact you by electronic and paper mail.
639 |
640 | If your software can interact with users remotely through a computer
641 | network, you should also make sure that it provides a way for users to
642 | get its source. For example, if your program is a web application, its
643 | interface could display a “Source” link that leads users to an archive
644 | of the code. There are many ways you could offer source, and different
645 | solutions will be better for different programs; see section 13 for the
646 | specific requirements.
647 |
648 | You should also get your employer (if you work as a programmer) or school,
649 | if any, to sign a “copyright disclaimer” for the program, if necessary.
650 | For more information on this, and how to apply and follow the GNU AGPL, see
651 | <>.
652 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mxpp
2 |
3 | **mxpp** is a bot which bridges Matrix and one-to-one XMPP chat.
4 |
5 | I wrote this bot to finally get persistent chat history for my
6 | gchat/hangouts/google talk conversations, and to evaluate Matrix
7 | for future use, so it should probably work for those use cases.
8 |
9 |
10 | **Functionality**
11 |
12 | * The bot creates one Matrix room for each user on your contact list,
13 | then invites a list of Matrix users (of your choosing) to all the rooms.
14 | - Enabled with ```send_messages_to_jid_rooms``` option in ```config.yaml```
15 | and can be controlled with per-user granularity
16 | - Room name is set to the contact's name.
17 | - Room topic is set to the contact's JID.
18 | - Any text sent to the room is sent to the contact's JID.
19 | - Any text received from the contact's JID is sent as a notice
20 | to the room.
21 | * A room named "XMPP Control Room" is created
22 | - Presence info ("available" or "unavailable") is sent to this room,
23 | controllable per-user with the ```send_presences_to_control``` option
24 | in ```config.yaml```
25 | - Text command ```purge``` makes the bot leave from any rooms which do
26 | not correspond to a roster entry (excluding the two special rooms),
27 | and also from any unoccupied rooms (eg. if the user left).
28 | - Text command ```refresh``` probes the presence of all XMPP contacts
29 | and requests a roster update from the server.
30 | - Text commands ```joinmuc room_jid@roomserver.com``` and ```leavemuc room_jid@roomserver.com```
31 | allow you to join and leave multi-user chats.
32 | * A room named "XMPP All Chat" is created
33 | - All inbound and outbound chat messages are logged here.
34 | - Enabled with per-user granularity using the ```send_messages_to_all_chat```
35 | option in ```config.yaml```
36 | - You can send a message directly to a jid without creating a room using
37 | the ```/m jid@example.com your message here``` syntax in this room.
38 | * If the bot is restarted, it recreates its room-JID map based on the
39 | room topics, and continues as before.
40 | * Currently, the bot automatically accepts anytime anyone asks to add
41 | you on XMPP, and also automatically adds them to your contact roster.
42 | * Multi-user chats (MUCs) are handled by creating additional rooms
43 | - Room topic is set to "room_jid@roomserver.com"
44 | - To join a MUC, send a message saying ```joinmuc room_jid@roomserver.com```
45 | to the "XMPP Control Room"
46 | - To leave a MUC, send a message saying ```leavemuc room_jid@roomserver.com```
47 | to the "XMPP Control Room". Alternatively, leave the MUC room and send the message
48 | ```purge``` instead.
49 |
50 |
51 | ## Installation
52 | Install the dependencies:
53 | ```bash
54 | pip3 install -r requirements.txt
55 | ```
56 |
57 | Edit config.yaml to set your usernames, passwords, and servers.
58 |
59 | If you're using your own homeserver and you have more than a handful of
60 | XMPP contacts, you'll probably want to loosen the rate limits on your
61 | homeserver (see ```homeserver.yaml``` for synapse), or you'll have to
62 | wait multiple minutes while the bot creates a bunch of new rooms.
63 |
64 | You should probably also set your Matrix client to auto-accept new room
65 | invitations for the first run of the bot, so you don't have to
66 | manually accept each invitation.
67 |
68 | From the same directory as ```config.yaml```, run
69 | ```bash
70 | python3 -m mxpp.main
71 | ```
72 |
73 | **Dependencies:**
74 |
75 | * python >=3.5 (written and tested with 3.5)
76 | * [sleekXMPP](https://pypi.python.org/pypi/sleekxmpp/1.3.1)
77 | * [matrix_client](https://github.com/matrix-org/matrix-python-sdk)
78 | (currently requires git version)
79 | * [pyyaml](https://pypi.python.org/pypi/PyYAML/3.12)
80 | * and their dependencies (dnspython, requests, others?)
81 |
82 |
83 | ## TODO
84 |
85 | * Set bot's presence for each room individually
86 | (impossible with current Matrix m.presence API)
87 | * Require higher-than-default power-level to speak in All-chat (i.e.,
88 | only let the bot talk in all-chat)
89 | (waiting on matrix_client pull request)
90 |
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | matrix:
2 | server:
3 | # Server's base url, without trailing /
4 | base_url: 'https://matrix.org'
5 | valid_cert_check: true
6 |
7 | login:
8 | # username is also used as the id that the bot will be known as
9 | # (eg. to make sure it ignores its own messages)
10 | # so make sure it's of the form @user:host
11 | username: '@xmpp-bot-username:matrix.org'
12 | password: ''
13 |
14 | # Users who will be invited to all of the bot's rooms
15 | users_to_invite:
16 | - '@owner:matrix.org'
17 |
18 | # Topics set in the special rooms, used by the bot to recognize these rooms.
19 | room_topics:
20 | control: 'xmpp-bot-control'
21 | all_chat: 'xmpp-bot-all_chat'
22 |
23 | # Groupchats will have a topic starting with this string.
24 | # Ideally, this should include some characters which are illegal in JIDs
25 | # (e.g. <>*&') so that it won't ever be confused for a JID
26 | groupchat_flag: ''
27 |
28 |
29 | xmpp:
30 | server:
31 | host: talk.google.com
32 | port: 5222
33 |
34 | login:
35 | # JID should be of the form of what@where.net
36 | jid: 'myname@gmail.com'
37 |
38 | # If you use 2-factor auth with google, you'll need to generate an app password for here
39 | password: ''
40 |
41 | roster_options:
42 | # Automatically accept any buddy requests
43 | auto_authorize: true
44 | # Automatically add and buddy who asks
45 | auto_subscribe: true
46 |
47 | groupchat_nick: 'my_groupchat_name'
48 |
49 | # Ignore any groupchat messages that were sent by our own nick
50 | groupchat_mute_own_nick: true
51 |
52 | # Send groupchat messages to the all_chat channel
53 | groupchat_send_messages_to_all_chat: false
54 |
55 |
56 | # Send a copy of all messages to the all_chat channel
57 | send_messages_to_all_chat: true
58 |
59 | # Create and user per-jid rooms
60 | send_messages_to_jid_rooms: false
61 |
62 | # Send presence notices to the control channel
63 | send_presences_to_control: false
64 |
65 |
66 | jid_groups:
67 | # An example whitelist
68 | - send_messages_to_all_chat: false
69 | send_messages_to_jid_rooms: true
70 | #send_presences_to_control: false
71 | jids:
72 | - 'friend1@example.com'
73 | - 'friend2@example2.com'
74 |
75 | # An example blacklist
76 | - send_messages_to_all_chat: false
77 | send_messages_to_jid_rooms: false
78 | send_presences_to_control: false
79 | jids:
80 | - 'blacklisted@example.com'
81 |
--------------------------------------------------------------------------------
/mxpp/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = 1.0
2 |
--------------------------------------------------------------------------------
/mxpp/client_xmpp.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Dict
3 | from queue import Queue
4 |
5 | import sleekxmpp
6 | from sleekxmpp.exceptions import IqError, IqTimeout
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | class ClientXMPP(sleekxmpp.ClientXMPP):
12 | roster_dict = {} # type: Dict[str, str]
13 | jid_nick_map = {} # type: Dict[str, str]
14 | inbound_queue = None # type: Queue
15 |
16 | def __init__(self,
17 | inbound_queue: Queue,
18 | jid: str,
19 | password: str,
20 | auto_authorize: bool=True,
21 | auto_subscribe: bool=True):
22 | self.inbound_queue = inbound_queue
23 |
24 | sleekxmpp.ClientXMPP.__init__(self, jid, password)
25 |
26 | self.add_event_handler('session_start', self.handle_session_start)
27 | self.add_event_handler('disconnected', self.handle_disconnected)
28 | self.add_event_handler('roster_update', self.handle_roster_update)
29 | self.add_event_handler('presence_available', self.handle_presence_available)
30 | self.add_event_handler('presence_unavailable', self.handle_presence_unavailable)
31 | self.add_event_handler('message', self.handle_message)
32 | self.add_event_handler('groupchat_message', self.handle_groupchat_message)
33 |
34 | self.register_plugin('xep_0030') # Service Discovery
35 | self.register_plugin('xep_0004') # Data Forms
36 | self.register_plugin('xep_0060') # PubSub
37 | self.register_plugin('xep_0199') # XMPP Ping
38 | self.register_plugin('xep_0045') # Multi-User Chats (MUC)
39 |
40 | self.auto_authorize = auto_authorize
41 | self.auto_subscribe = auto_subscribe
42 |
43 | def handle_session_start(self, _event):
44 | try:
45 | try:
46 | self.send_presence()
47 | except IqError as err:
48 | logger.error('There was an error sending presence')
49 | logger.error(err.iq['error']['condition'])
50 | self.disconnect()
51 |
52 | try:
53 | self.get_roster(block=True)
54 | except IqError as err:
55 | logger.error('There was an error getting the roster')
56 | logger.error(err.iq['error']['condition'])
57 | self.disconnect()
58 |
59 | except IqTimeout:
60 | logger.error('Server is taking too long to respond')
61 | self.disconnect()
62 |
63 | logger.info('XMPP Logged in!')
64 |
65 | def handle_disconnected(self, _event):
66 | logger.info('XMPP Disconnected!')
67 |
68 | def handle_roster_update(self, roster):
69 | logger.info('XMPP Roster update')
70 | self.inbound_queue.put(roster)
71 |
72 | def handle_presence_available(self, presence):
73 | logger.debug('XMPP Received presence_available: {}'.format(presence))
74 | self.inbound_queue.put(presence)
75 |
76 | def handle_presence_unavailable(self, presence):
77 | logger.debug('XMPP Received presence_unavailable: {}'.format(presence))
78 | self.inbound_queue.put(presence)
79 |
80 | def handle_message(self, message):
81 | logger.debug('XMPP Received message: {}'.format(message))
82 | self.inbound_queue.put(message)
83 |
84 | def handle_groupchat_message(self, message):
85 | logger.debug('XMPP Received groupchat_message: {}'.format(message))
86 | self.inbound_queue.put(message)
87 |
--------------------------------------------------------------------------------
/mxpp/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Dict, Tuple, List
3 | import sys
4 | import time
5 | from queue import Queue
6 |
7 | if sys.version_info[0] != 3 or sys.version_info[1] < 5:
8 | raise Exception('mxpp requires python >= 3.5')
9 |
10 | import sleekxmpp
11 | from sleekxmpp import stanza
12 | import requests
13 | import yaml
14 |
15 | from matrix_client.client import MatrixClient
16 | from matrix_client.errors import MatrixError
17 | from matrix_client.room import Room as MatrixRoom
18 | from mxpp.client_xmpp import ClientXMPP
19 |
20 | CONFIG_FILE = 'config.yaml'
21 |
22 | logging.basicConfig(level=logging.INFO,
23 | format='%(levelname)-8s %(message)s')
24 | logging.getLogger(sleekxmpp.__name__).setLevel(logging.ERROR)
25 | logging.getLogger(requests.__name__).setLevel(logging.ERROR)
26 |
27 | logger = logging.getLogger(__name__)
28 |
29 |
30 | class BridgeBot:
31 | xmpp = None # type: ClientXMPP
32 | matrix = None # type: MatrixClient
33 | topic_room_id_map = None # type: Dict[str, str]
34 | special_rooms = None # type: Dict[str, MatrixRoom]
35 | special_room_names = None # type: Dict[str, str]
36 | groupchat_flag = None # type: str
37 | groupchat_jids = None # type: List[str]
38 |
39 | users_to_invite = None # type: List[str]
40 | matrix_room_topics = None # type: Dict[str, str]
41 | matrix_server = None # type: Dict[str, str]
42 | matrix_login = None # type: Dict[str, str]
43 | xmpp_server = None # type: Tuple[str, int]
44 | xmpp_login = None # type: Dict[str, str]
45 | xmpp_roster_options = None # type: Dict[str, bool]
46 | xmpp_groupchat_nick = None # type: str
47 |
48 | default_actions = None # type: Dict[str, bool]
49 | jid_actions = None # type: Dict[str, Dict[str, bool]]
50 | groupchat_mute_own_nick = True # type: bool
51 | groupchat_send_messages_to_all_chat = True # type: bool
52 |
53 | inbound_xmpp = None # type: Queue
54 |
55 | exception = None # type: Exception or None
56 |
57 |
58 | @property
59 | def bot_id(self) -> str:
60 | return self.matrix_login['username']
61 |
62 | def __init__(self, config_file: str=CONFIG_FILE):
63 | self.groupchat_jids = []
64 | self.topic_room_id_map = {}
65 | self.special_rooms = {
66 | 'control': None,
67 | 'all_chat': None,
68 | }
69 | self.special_room_names = {
70 | 'control': 'XMPP Control Room',
71 | 'all_chat': 'XMPP All Chat',
72 | }
73 | self.xmpp_roster_options = {}
74 | self.inbound_xmpp = Queue()
75 |
76 | self.load_config(config_file)
77 |
78 | self.matrix = MatrixClient(**self.matrix_server)
79 | self.xmpp = ClientXMPP(self.inbound_xmpp,
80 | **self.xmpp_login,
81 | **self.xmpp_roster_options)
82 |
83 | self.matrix.login_with_password(**self.matrix_login)
84 |
85 | # Recover existing matrix rooms
86 | for room in list(self.matrix.get_rooms().values()):
87 | room.update_room_topic()
88 | topic = room.topic
89 |
90 | if topic in self.special_rooms.keys():
91 | logger.debug('Recovering special room: ' + topic)
92 | self.special_rooms[topic] = room
93 |
94 | elif topic is None:
95 | room.leave()
96 |
97 | elif topic.startswith(self.groupchat_flag):
98 | room_jid = topic[len(self.groupchat_flag):]
99 | self.groupchat_jids.append(room_jid)
100 |
101 | elif not self.jid_actions.get(topic, self.default_actions)['send_messages_to_jid_rooms']:
102 | logger.info('Room ' + topic + ' is not needed due to send_messages_to_jid_rooms setting, leaving!')
103 | room.leave()
104 |
105 | # Prepare matrix special rooms and their listeners
106 | for topic, room in self.special_rooms.items():
107 | if room is None:
108 | room = self.matrix.create_room()
109 | self.setup_special_room(room, topic)
110 |
111 | self.special_rooms['control'].add_listener(self.matrix_control_message, 'm.room.message')
112 | self.special_rooms['all_chat'].add_listener(self.matrix_all_chat_message, 'm.room.message')
113 |
114 | # Invite users to special rooms
115 | for room in self.special_rooms.values():
116 | for user_id in self.users_to_invite:
117 | room.invite_user(user_id)
118 |
119 | # Connect to XMPP and start processing XMPP events
120 | self.xmpp.connect(self.xmpp_server)
121 | self.xmpp.process(block=False)
122 |
123 | # Rejoin group chats
124 | logger.debug('Rejoining group chats')
125 | for room_jid in self.groupchat_jids:
126 | self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick)
127 |
128 | # Listen for Matrix events
129 | def exception_handler(e: Exception):
130 | self.exception = e
131 |
132 | self.matrix.start_listener_thread(exception_handler=exception_handler)
133 |
134 | logger.debug('Done with bot init')
135 |
136 | def shutdown(self):
137 | self.matrix.stop_listener_thread()
138 | self.xmpp.disconnect()
139 |
140 | def handle_inbound_xmpp(self):
141 | while self.exception is None:
142 | event = self.inbound_xmpp.get()
143 |
144 | if isinstance(event, sleekxmpp.Presence):
145 | handler = {
146 | 'available': self.xmpp_presence_available,
147 | 'unavailable': self.xmpp_presence_unavailable,
148 | }.get(event.get_type(), self.xmpp_unrecognized_event)
149 |
150 | elif isinstance(event, sleekxmpp.Message):
151 | handler = {
152 | 'normal': self.xmpp_message,
153 | 'chat': self.xmpp_message,
154 | 'groupchat': self.xmpp_groupchat_message,
155 | }.get(event.get_type(), self.xmpp_unrecognized_event)
156 |
157 | elif isinstance(event, sleekxmpp.Iq) and event.get_query() == 'jabber:iq:roster':
158 | handler = self.xmpp_roster_update
159 |
160 | else:
161 | handler = self.xmpp_unrecognized_event
162 |
163 | handler(event)
164 | raise self.exception
165 |
166 | def load_config(self, path: str):
167 | with open(path, 'r') as conf_file:
168 | config = yaml.safe_load(conf_file)
169 |
170 | self.users_to_invite = config['matrix']['users_to_invite']
171 | self.matrix_room_topics = config['matrix']['room_topics']
172 | self.groupchat_flag = config['matrix']['groupchat_flag']
173 |
174 | self.matrix_server = config['matrix']['server']
175 | self.matrix_login = config['matrix']['login']
176 | self.xmpp_server = (config['xmpp']['server']['host'],
177 | config['xmpp']['server']['port'])
178 | self.xmpp_login = config['xmpp']['login']
179 | self.xmpp_groupchat_nick = config['xmpp']['groupchat_nick']
180 |
181 | self.default_actions = {k: config[k] for k in ('send_messages_to_all_chat',
182 | 'send_messages_to_jid_rooms',
183 | 'send_presences_to_control')}
184 | self.jid_actions = {}
185 | for group in config['jid_groups']:
186 | group_data = group.copy()
187 | group_jids = group_data.pop('jids')
188 | for jid in group['jids']:
189 | self.jid_actions.setdefault(jid, self.default_actions.copy())
190 | self.jid_actions[jid].update(group_data)
191 |
192 | self.groupchat_mute_own_nick = config['groupchat_mute_own_nick']
193 | self.groupchat_send_messages_to_all_chat = config['groupchat_send_messages_to_all_chat']
194 |
195 | self.xmpp_roster_options = config['xmpp']['roster_options']
196 |
197 | def get_room_for_topic(self, jid: str) -> MatrixRoom:
198 | """
199 | Return the room corresponding to the given XMPP JID
200 | :param jid: bare XMPP JID, should not include the resource
201 | :return: Matrix room object for chatting with that JID
202 | """
203 | room_id = self.topic_room_id_map[jid]
204 | return self.matrix.get_rooms()[room_id]
205 |
206 | def get_unmapped_rooms(self) -> List[MatrixRoom]:
207 | """
208 | Returns a list of all Matrix rooms which are not a special room (e.g., the control room) and
209 | do not have a corresponding entry in the topic -> room map.
210 | :return: List of unmapped, non-special Matrix room objects.
211 | """
212 | special_room_ids = [r.room_id for r in self.special_rooms.values()]
213 | valid_room_ids = [v for v in self.topic_room_id_map.values()] + special_room_ids
214 | unmapped_rooms = [room for room_id, room in self.matrix.get_rooms().items()
215 | if room_id not in valid_room_ids]
216 | return unmapped_rooms
217 |
218 | def get_empty_rooms(self) -> List[MatrixRoom]:
219 | """
220 | Returns a list of all Matrix rooms which are occupied by only one user
221 | (the bot itself).
222 | :return: List of Matrix rooms occupied by only the bot.
223 | """
224 | empty_rooms = [room for room in self.matrix.get_rooms().values()
225 | if len(room.get_joined_members()) < 2]
226 | return empty_rooms
227 |
228 | def setup_special_room(self, room, topic: str):
229 | """
230 | Sets up a Matrix room with the requested topic and adds it to the self.special_rooms map.
231 |
232 | If a special room with that topic already exists, it is replaced in the special_rooms
233 | map by the new room.
234 | :param room: Room to set up
235 | :param topic: Topic for the room
236 | """
237 | room.set_room_topic(topic)
238 | room.set_room_name(self.special_room_names[topic])
239 | self.special_rooms[topic] = room
240 |
241 | logger.debug('Set up special room with topic {} and id'.format(
242 | str(room.topic), room.room_id))
243 |
244 | def create_mapped_room(self, topic: str, name: str=None) -> MatrixRoom or None:
245 | """
246 | Create a new room and add it to self.topic_room_id_map.
247 |
248 | :param topic: Topic for the new room
249 | :param name: (Optional) Name for the new room
250 | :return: Room which was created
251 | """
252 | if topic in self.groupchat_jids:
253 | logger.debug('Topic {} is a groupchat without its flag, ignoring'.format(topic))
254 | return None
255 | elif topic in self.topic_room_id_map.keys():
256 | room_id = self.topic_room_id_map[topic]
257 | room = self.matrix.get_rooms()[room_id]
258 | logger.debug('Room with topic {} already exists!'.format(topic))
259 | else:
260 | room = self.matrix.create_room()
261 | room.set_room_topic(topic)
262 | self.topic_room_id_map[topic] = room.room_id
263 | logger.info('Created mapped room with topic {} and id {}'.format(topic, str(room.room_id)))
264 | room.add_listener(self.matrix_message, 'm.room.message')
265 |
266 | if room.name != name:
267 | if name != "":
268 | room.set_room_name(name)
269 | room.set_user_profile(displayname=name)
270 | else:
271 | room.set_room_name(topic.split('@')[0])
272 |
273 | return room
274 |
275 | def leave_mapped_room(self, topic: str) -> bool:
276 | """
277 | Leave an existing, mapped room and remove it from self.topic_room_id_map.
278 |
279 | :param topic: Topic for room to leave
280 | :retrun: True if the room was left, False if the room was not found.
281 | """
282 | if topic in self.groupchat_jids:
283 | logger.debug('Topic {} is a groupchat without its flag, ignoring'.format(topic))
284 | return False
285 |
286 | if topic not in self.topic_room_id_map.keys():
287 | err_msg = 'Room with topic {} isn\'t mapped or doesn\'t exist'.format(topic)
288 | logger.warning(err_msg)
289 | return False
290 |
291 | if topic.startswith(self.groupchat_flag):
292 | # Leave the groupchat
293 | room_jid = topic[len(self.groupchat_flag):]
294 | if room_jid in self.groupchat_jids:
295 | self.groupchat_jids.remove(room_jid)
296 | logger.info('XMPP MUC leave: {}'.format(room_jid))
297 | self.xmpp.plugin['xep_0045'].leaveMUC(room_jid, self.xmpp_groupchat_nick)
298 |
299 | room = self.get_room_for_topic(topic)
300 | del self.topic_room_id_map[topic]
301 | room.leave()
302 | logger.info('Left mapped room with topic {}'.format(topic))
303 | return True
304 |
305 | def map_rooms_by_topic(self):
306 | """
307 | Add unmapped rooms to self.topic_room_id_map, and listen to messages from those rooms.
308 |
309 | Rooms whose topics are empty or do not contain an '@' symbol are assumed to be special
310 | rooms, and will not be mapped.
311 | """
312 | unmapped_rooms = self.get_unmapped_rooms()
313 |
314 | for room in unmapped_rooms:
315 | room.update_room_topic()
316 |
317 | logger.debug('Unmapped room {} ({}) [{}]'.format(room.room_id, room.name, room.topic))
318 |
319 | if room.topic is None or '@' not in room.topic:
320 | logger.debug('Leaving it as-is (special room, topic does not contain @)')
321 | else:
322 | self.topic_room_id_map[room.topic] = room.room_id
323 | room.add_listener(self.matrix_message, 'm.room.message')
324 |
325 | def matrix_control_message(self, room: MatrixRoom, event: Dict):
326 | """
327 | Handle a message sent to the control room.
328 |
329 | Does nothing unless a valid command is received:
330 | refresh Probes the presence of all XMPP contacts, and updates the roster.
331 | purge Leaves any ((un-mapped and non-special) or empty) Matrix rooms.
332 | joinmuc some@muc.com Joins a muc
333 | leavemuc some@muc.com Leaves a muc
334 |
335 | :param room: Matrix room object representing the control room
336 | :param event: The Matrix event that was received. Assumed to be an m.room.message .
337 | """
338 | # Always ignore our own messages
339 | if event['sender'] == self.bot_id:
340 | return
341 |
342 | logger.debug('matrix_control_message: {} {}'.format(room.room_id, str(event)))
343 |
344 | if event['content']['msgtype'] == 'm.text':
345 | message_body = event['content']['body']
346 | logger.info('Matrix received control message: ' + message_body)
347 |
348 | message_parts = message_body.split()
349 | if len(message_parts) < 1:
350 | logger.warning('Received empty control message, ignoring')
351 | return
352 |
353 | if message_parts[0] == 'refresh':
354 | for jid in self.topic_room_id_map.keys():
355 | self.xmpp.send_presence(pto=jid, ptype='probe')
356 | self.xmpp.send_presence()
357 | self.xmpp.get_roster()
358 |
359 | elif message_parts[0] == 'purge':
360 | self.special_rooms['control'].send_text('Purging unused rooms')
361 |
362 | # Leave from unwanted rooms
363 | for room in self.get_unmapped_rooms() + self.get_empty_rooms():
364 | logger.info('Leaving room {r.room_id} ({r.name}) [{r.topic}]'.format(r=room))
365 |
366 | if room.topic in self.topic_room_id_map.keys():
367 | self.leave_mapped_room(room.topic)
368 | else:
369 | room.leave()
370 |
371 | elif message_parts[0] == 'joinmuc':
372 | if len(message_parts) < 2:
373 | logger.warning('joinmuc command didn\'t specify a room, ignoring')
374 | return
375 |
376 | room_jid = message_parts[1]
377 | logger.info('XMPP MUC join: {}'.format(room_jid))
378 | self.create_groupchat_room(room_jid)
379 | self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick)
380 |
381 | elif message_parts[0] == 'leavemuc':
382 | if len(message_parts) < 2:
383 | logger.warning('leavemuc command didn\'t specify a room, ignoring')
384 | return
385 |
386 | room_jid = message_parts[1]
387 | room_topic = self.groupchat_flag + room_jid
388 |
389 | success = self.leave_mapped_room(room_topic)
390 | if not success:
391 | msg = 'Groupchat {} isn\'t mapped or doesn\'t exist'.format(room_jid)
392 | else:
393 | msg = 'Left groupchat {}'.format(room_jid)
394 | self.special_rooms['control'].send_notice(msg)
395 |
396 | def matrix_all_chat_message(self, room: MatrixRoom, event: Dict):
397 | """
398 | Handle a message sent to Matrix all-chat room.
399 |
400 | Allows manual sending of xmpp messages: "/m target_jid your message here".
401 | Sends a notice with the expected format if it isn't there by default.
402 |
403 | :param room: Matrix room object representing the all-chat room
404 | :param event: The Matrix event that was received. Assumed to be an m.room.message .
405 | """
406 | # Always ignore our own messages
407 | if event['sender'] == self.bot_id:
408 | return
409 |
410 | logger.debug('matrix_all_chat_message: {} {}'.format(room.room_id, str(event)))
411 |
412 | if event['content']['msgtype'] == 'm.text':
413 | message_body = event['content']['body']
414 | message_parts = message_body.split()
415 | if message_parts[0] == '/m':
416 | jid = message_parts[1]
417 | payload = message_body[message_body.find(jid) + len(jid) + 1:]
418 | logger.info('sending manual message to '+ jid + ' : ' + payload)
419 | self.xmpp.send_message(mto=jid, mbody=payload, mtype='chat')
420 | else:
421 | room.send_notice('Expected message format: "/m DEST_JID your message here"')
422 |
423 | def matrix_message(self, room: MatrixRoom, event: Dict):
424 | """
425 | Handle a message sent to a mapped Matrix room.
426 |
427 | Sends the message to the xmpp handle specified by the room's topic.
428 |
429 | :param room: Matrix room object representing the room in which the message was received.
430 | :param event: The Matrix event that was received. Assumed to be an m.room.message .
431 | """
432 | if event['sender'] == self.bot_id:
433 | return
434 |
435 | if room.topic in self.special_rooms.keys():
436 | logger.error('matrix_message called on special channel')
437 |
438 | logger.debug('matrix_message: {} {}'.format(room.room_id, event))
439 |
440 | if event['content']['msgtype'] == 'm.text':
441 | message_body = event['content']['body']
442 |
443 | if room.topic.startswith(self.groupchat_flag):
444 | jid = room.topic[len(self.groupchat_flag):]
445 | message_type = 'groupchat'
446 | else:
447 | jid = room.topic
448 | message_type = 'chat'
449 |
450 | logger.info('Matrix received message to {} : {}'.format(jid, message_body))
451 | self.xmpp.send_message(mto=jid, mbody=message_body, mtype=message_type)
452 |
453 | # Possible that we're in a room that wasn't mapped
454 | if jid not in self.xmpp.jid_nick_map:
455 | logger.error('Received message in matrix room with topic {},'.format(jid) +
456 | 'which wasn\'t in the jid_nick_map')
457 | name = self.xmpp.jid_nick_map.get(jid, jid)
458 |
459 | send_message = self.jid_actions.get(jid, self.default_actions)['send_messages_to_all_chat']
460 | if send_message:
461 | self.special_rooms['all_chat'].send_notice('To {} : {}'.format(name, message_body))
462 |
463 | def xmpp_message(self, message: Dict):
464 | """
465 | Handle a message received by the XMPP client.
466 |
467 | Sends the message to the relevant mapped Matrix room, as well as the Matrix all-chat room.
468 |
469 | :param message: The message that was received.
470 | :return:
471 | """
472 | logger.info('XMPP received {} : {}'.format(message['from'].full, message['body']))
473 |
474 | if message['type'] in ('normal', 'chat'):
475 | from_jid = message['from'].bare
476 | from_name = self.xmpp.jid_nick_map.get(from_jid, from_jid)
477 |
478 | send_message2all = self.jid_actions.get(from_jid, self.default_actions)['send_messages_to_all_chat']
479 | if send_message2all:
480 | self.special_rooms['all_chat'].send_text('From ({})\n{}: {}'.format(from_jid, from_name, message['body']))
481 |
482 | send_message2room = self.jid_actions.get(from_jid, self.default_actions)['send_messages_to_jid_rooms']
483 | if send_message2room:
484 | if from_jid not in self.xmpp.jid_nick_map.keys():
485 | logger.error('xmpp_message: JID {} NOT IN ROSTER!?'.format(from_jid))
486 | self.xmpp.get_roster(block=True)
487 |
488 | room = self.get_room_for_topic(from_jid)
489 | room.send_text(message['body'])
490 |
491 | def xmpp_groupchat_message(self, message: Dict):
492 | """
493 | Handle a groupchat message received by the XMPP client.
494 |
495 | Sends the message to the relevant mapped Matrix room, as well as the Matrix all-chat room.
496 |
497 | :param message: The message that was received.
498 | :return:
499 | """
500 | logger.info('XMPP MUC received {} : {}'.format(message['from'].full, message['body']))
501 |
502 | if message['type'] == 'groupchat':
503 | from_jid = message['from'].bare
504 | from_name = message['mucnick']
505 |
506 | if self.groupchat_mute_own_nick and from_name == self.xmpp_groupchat_nick:
507 | return
508 |
509 | room = self.get_room_for_topic(self.groupchat_flag + from_jid)
510 | room.send_text(from_name + ': ' + message['body'])
511 |
512 | if self.groupchat_send_messages_to_all_chat:
513 | self.special_rooms['all_chat'].send_text(
514 | 'Room {}, from {}: {}'.format(from_jid, from_name, message['body']))
515 |
516 | def create_groupchat_room(self, room_jid: str):
517 | room = self.create_mapped_room(topic=self.groupchat_flag + room_jid)
518 | if room_jid not in self.groupchat_jids:
519 | self.groupchat_jids.append(room_jid)
520 | for user_id in self.users_to_invite:
521 | room.invite_user(user_id)
522 |
523 | def xmpp_presence_available(self, presence: Dict):
524 | """
525 | Handle a presence of type "available".
526 |
527 | Sends a notice to the control channel.
528 |
529 | :param presence: The presence that was received.
530 | """
531 | logger.debug('XMPP received {} : (available)'.format(presence['from'].full))
532 |
533 | jid = presence['from'].bare
534 | if jid not in self.xmpp.jid_nick_map.keys():
535 | logger.error('xmpp_presence_available: JID {} NOT IN ROSTER!?'.format(jid))
536 | self.xmpp.get_roster(block=True)
537 |
538 | send_presence = self.jid_actions.get(jid, self.default_actions)['send_presences_to_control']
539 | if send_presence:
540 | name = self.xmpp.jid_nick_map.get(jid, jid)
541 | self.special_rooms['control'].send_notice('{} available ({})'.format(name, jid))
542 |
543 | def xmpp_presence_unavailable(self, presence):
544 | """
545 | Handle a presence of type "unavailable".
546 |
547 | Sends a notice to the control channel.
548 |
549 | :param presence: The presence that was received.
550 | """
551 | logger.debug('XMPP received {} : (unavailable)'.format(presence['from'].full))
552 |
553 | jid = presence['from'].bare
554 | if jid not in self.xmpp.jid_nick_map.keys():
555 | logger.error('xmpp_presence_unavailable: JID {} NOT IN ROSTER!?'.format(jid))
556 | self.xmpp.get_roster(block=True)
557 |
558 | send_presence = self.jid_actions.get(jid, self.default_actions)['send_presences_to_control']
559 | if send_presence:
560 | name = self.xmpp.jid_nick_map.get(jid, jid)
561 | self.special_rooms['control'].send_notice('{} unavailable ({})'.format(name, jid))
562 |
563 | def xmpp_roster_update(self, _event):
564 | """
565 | Handle an XMPP roster update.
566 |
567 | Maps all existing Matrix rooms, creates a new mapped room for each JID in the roster
568 | which doesn't have one yet, and invites the users specified in the config in to all the rooms.
569 |
570 | :param _event: The received roster update event (unused).
571 | """
572 | logger.debug('######### ROSTER UPDATE ###########')
573 |
574 | rjids = [jid for jid in self.xmpp.roster]
575 | if len(rjids) > 1:
576 | raise Exception('Not sure what to do with more than one roster...')
577 |
578 | roster0 = self.xmpp.roster[rjids[0]]
579 | self.xmpp.roster_dict = {jid: roster0[jid] for jid in roster0}
580 | roster = self.xmpp.roster_dict
581 |
582 | self.map_rooms_by_topic()
583 |
584 | # Create new rooms where none exist
585 | for jid, info in roster.items():
586 | if '@' not in jid:
587 | logger.warning('Skipping fake jid in roster: ' + jid)
588 | continue
589 |
590 | name = info['name']
591 | self.xmpp.jid_nick_map[jid] = name
592 |
593 | # Check if we need to create a room
594 | if self.jid_actions.get(jid, self.default_actions)['send_messages_to_jid_rooms']:
595 | self.create_mapped_room(topic=jid, name=name)
596 |
597 | logger.debug('Sending invitations..')
598 | # Invite to all rooms
599 | for room in self.matrix.get_rooms().values():
600 | users_in_room = room.get_joined_members()
601 | for user_id in self.users_to_invite:
602 | if user_id not in users_in_room:
603 | room.invite_user(user_id)
604 |
605 | logger.debug('######## Done with roster update #######')
606 |
607 | def xmpp_unrecognized_event(self, event):
608 | logger.error('Unrecognized event: {} || {}'.format(type(event), event))
609 |
610 |
611 | def main():
612 | while True:
613 | try:
614 | bot = BridgeBot()
615 | bot.handle_inbound_xmpp()
616 | except Exception as e:
617 | logger.error('Fatal Exception: {}'.format(e))
618 | try:
619 | bot.shutdown()
620 | except Exception:
621 | pass
622 | time.sleep(1);
623 |
624 |
625 | if __name__ == "__main__":
626 | main()
627 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | sleekxmpp
2 | pyyaml
3 | -e git+https://github.com/matrix-org/matrix-python-sdk.git#egg=matrix_client
4 |
--------------------------------------------------------------------------------