Compare commits
1 commit
master
...
wip_xmpp_a
Author | SHA1 | Date | |
---|---|---|---|
4d16a3e436 |
45 changed files with 1403 additions and 7499 deletions
|
@ -1,7 +0,0 @@
|
||||||
pipeline:
|
|
||||||
build:
|
|
||||||
image: golang:stretch
|
|
||||||
commands:
|
|
||||||
- go get -d -v
|
|
||||||
- go build -v
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,4 +2,3 @@ easybridge
|
||||||
config.json
|
config.json
|
||||||
registration.yaml
|
registration.yaml
|
||||||
easybridge.db
|
easybridge.db
|
||||||
__pycache__
|
|
||||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -1,16 +0,0 @@
|
||||||
FROM archlinux:latest
|
|
||||||
RUN pacman -Sy python-pip --noconfirm; pacman -Scc --noconfirm; find /var/cache/pacman/ -type f -delete; find /var/lib/pacman/sync/ -type f -delete
|
|
||||||
|
|
||||||
#FROM python:3.8.6-buster
|
|
||||||
|
|
||||||
RUN pip install fbchat==1.9.7
|
|
||||||
|
|
||||||
RUN mkdir /app
|
|
||||||
WORKDIR /app
|
|
||||||
ADD static /app/static
|
|
||||||
ADD easybridge.jpg /app/easybridge.jpg
|
|
||||||
ADD external /app/external
|
|
||||||
ADD easybridge /app/easybridge
|
|
||||||
ADD templates /app/templates
|
|
||||||
|
|
||||||
CMD "/app/easybridge"
|
|
675
LICENSE
675
LICENSE
|
@ -1,675 +0,0 @@
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|
31
Makefile
31
Makefile
|
@ -1,29 +1,2 @@
|
||||||
BIN=easybridge
|
all:
|
||||||
DOCKER=lxpz/easybridge_amd64
|
go build
|
||||||
|
|
||||||
SRC= mxlib/registration.go mxlib/api.go mxlib/mediaobject.go mxlib/client.go \
|
|
||||||
connector/connector.go connector/config.go \
|
|
||||||
connector/mediaobject.go connector/marshal.go \
|
|
||||||
connector/irc/irc.go connector/irc/config.go \
|
|
||||||
connector/xmpp/config.go connector/xmpp/xmpp.go \
|
|
||||||
connector/mattermost/mattermost.go connector/mattermost/config.go \
|
|
||||||
connector/external/external.go connector/external/config.go \
|
|
||||||
web.go account.go main.go server.go db.go util.go
|
|
||||||
|
|
||||||
all: $(BIN)
|
|
||||||
|
|
||||||
$(BIN): $(SRC)
|
|
||||||
go get -d -v
|
|
||||||
go build -v -o $(BIN)
|
|
||||||
|
|
||||||
docker: $(BIN)
|
|
||||||
docker build -t $(DOCKER):$(TAG) .
|
|
||||||
docker push $(DOCKER):$(TAG)
|
|
||||||
docker tag $(DOCKER):$(TAG) $(DOCKER):latest
|
|
||||||
docker push $(DOCKER):latest
|
|
||||||
|
|
||||||
docker_clean: $(BIN)
|
|
||||||
docker build --no-cache -t $(DOCKER):$(TAG) .
|
|
||||||
docker push $(DOCKER):$(TAG)
|
|
||||||
docker tag $(DOCKER):$(TAG) $(DOCKER):latest
|
|
||||||
docker push $(DOCKER):latest
|
|
||||||
|
|
120
README.md
120
README.md
|
@ -1,120 +0,0 @@
|
||||||
# Easybridge
|
|
||||||
|
|
||||||
Easybridge is a Matrix-to-everything (almost) bridge.
|
|
||||||
It acts as a client under your name on all instant messaging networks where
|
|
||||||
you have an account, and presents your private messages as well as group
|
|
||||||
conversations in Matrix in a uniform view.
|
|
||||||
Basically, Matrix becomes your everything chat client!
|
|
||||||
Easybridge is a multi-user bridge for small Matrix servers,
|
|
||||||
with the idea of making it easy for non-technical users to bridge their external accounts into Matrix.
|
|
||||||
Once configured next to a server,
|
|
||||||
users can just go to a dedicated web page and add their accounts with a simple form.
|
|
||||||
|
|
||||||
**WARNING** Easybridge is still very experimental and crashes sometimes.
|
|
||||||
|
|
||||||
Current protocol status:
|
|
||||||
|
|
||||||
- IRC: text messages only (private messages and public channels). Has bugs, most notably: messages will be duplicated if several users connect to the same channel.
|
|
||||||
- XMPP: text messages only (private chat and MUCs), no backlog, no avatars, no file transfer.
|
|
||||||
- Mattermost: in quite good shape. Private & group conversations with text messages and attachments (images or other files). Handles retrieving of message backlog, user avatars and room avatars from the server.
|
|
||||||
- Facebook messenger: in quite good shape, handles private messages and groups, text messages, attachments, stickers, profile pictures (low res only), backlog. Sometimes disconnects from the server and messages stop arriving.
|
|
||||||
|
|
||||||
Adding a backend shouldn't be too hard if a Golang library exists to connect to that protocol.
|
|
||||||
Easybridge can also spawn an external process to communicate using a certain protocol if no Golang library
|
|
||||||
is available (this is currently used by the Facebook Messenger backend which is written in Python using the `fbchat` library).
|
|
||||||
|
|
||||||
Current features:
|
|
||||||
|
|
||||||
- Handles private chats (one-to-one conversations) as well as group chats (sometimes called channels, multi-user chats, or chat rooms)
|
|
||||||
- Automatic setup of Matrix rooms that bridge to outside rooms
|
|
||||||
- Room name and topic synchronization (partially)
|
|
||||||
- Images and file transfers (Mattermost and FB Messenger backends)
|
|
||||||
- Avatar and room pictures (Mattermost backend and FB Messenger backends, one-way only)
|
|
||||||
- Retrieving old messages / missed messages when backend was disconnected (Mattermost and FB Messenger)
|
|
||||||
- Web interface for setting up accounts so that new accounts can be easily
|
|
||||||
added and you don't have to type your credentials in a clear-text Matrix room
|
|
||||||
- Credentials are stored encrypted in the database using users' Matrix passwords
|
|
||||||
|
|
||||||
There is lot to do! See the issues if you want to join us on this project.
|
|
||||||
|
|
||||||
A Docker image is provided on the [Docker hub](https://hub.docker.com/r/lxpz/easybridge_amd64).
|
|
||||||
|
|
||||||
Easybridge is licensed under the terms of the GPLv3.
|
|
||||||
|
|
||||||
Contact me if you have questions or are interested in contributing to the project: [@lx:deuxfleurs.fr](https://matrix.to/#/@lx:deuxfleurs.fr).
|
|
||||||
|
|
||||||
|
|
||||||
## Building Easybridge
|
|
||||||
|
|
||||||
Easybridge requires go 1.13 or later. The Facebook Messenger backend requires Python 3 and the `fbchat` library (tested with v1.9.6).
|
|
||||||
|
|
||||||
To build Easybridge, clone this repository outside of your `$GOPATH`.
|
|
||||||
Then, run `make` in the root of the repo.
|
|
||||||
|
|
||||||
|
|
||||||
## Operating Easybridge
|
|
||||||
|
|
||||||
Easybridge acts as a Matrix application service: in this regard,
|
|
||||||
it requires a registration file to be added to your homeserver.
|
|
||||||
It uses a database to store configuration and state information,
|
|
||||||
which can be any backend supported by [Gorm](https://gorm.io).
|
|
||||||
|
|
||||||
Easybridge takes a single command line argument, `-config <filename>`, which is the
|
|
||||||
path to its config file (defaults to `./config.json`).
|
|
||||||
The configuration file is a JSON file whose contents is described in the following section.
|
|
||||||
|
|
||||||
If the config file does not exist, a template will be created.
|
|
||||||
A template appservice registration file will also be created if necessary.
|
|
||||||
|
|
||||||
|
|
||||||
## Configurating Easybridge
|
|
||||||
|
|
||||||
Easybridge is configured using a single JSON configuration file, which contains
|
|
||||||
a dictionnary whose keys are the following:
|
|
||||||
|
|
||||||
- `log_level`: what log level Easybridge runs with (trace, debug, info, warn, error, fatal, panic). **Warning:** in `trace` level, the content of all calls to the Matrix API and some other information will be dumped, exposing user's credentials and messages. In `debug` level, room join/leave information will be exposed. The `info` level (default) does not expose any user's private information.
|
|
||||||
- `easybridge_avatar`: path to the image that Easybridge uses as an avatar on Matrix
|
|
||||||
|
|
||||||
### Matrix configuration
|
|
||||||
|
|
||||||
- `registration`: path to the YAML appservice registration file
|
|
||||||
- `appservice_bind_addr`: on what IP/port to bind as a Matrix app service (HTTP only, no HTTPS)
|
|
||||||
- `homeserver_url`: HTTP address of the Matrix homeserver
|
|
||||||
- `matrix_domain`: the domain name of the Matrix homeserver (i.e. the domain used in user identifiers, room identifiers, etc)
|
|
||||||
- `name_format`: the format of identifiers that are created on Matrix for users and room aliases. `{}` is replaced by the concatenation of user/room identifier and protocol. Typically you want either `_ezbr_{}` or `{}_ezbr`, the latter having the advantage that the briged user's names are then used as prefixes for the created identifiers.
|
|
||||||
|
|
||||||
### Web interface configuration
|
|
||||||
|
|
||||||
- `web_bind_addr`: on what IP/port to bind for the web interface that allows adding and configuring accounts (HTTP only, no HTTPS, use a reverse proxy for that)
|
|
||||||
- `web_url`: the outside HTTP/HTTPS address at which the web interface is made available. If set, a widget will be added in the Easybridge room so that users can configure the bridge without leaving the Riot client.
|
|
||||||
- `session_key`: a key with which session cookies are encrypted for the web interface
|
|
||||||
|
|
||||||
### Storage configuration
|
|
||||||
|
|
||||||
- `db_type` and `db_path`: the database backend and path to use (see the [Gorm documentation](http://gorm.io/docs/connecting_to_the_database.html))
|
|
||||||
|
|
||||||
|
|
||||||
## Facebook Messenger alternative login procedure
|
|
||||||
|
|
||||||
The default login procedure for the Messenger backend is to log in with your email and password.
|
|
||||||
Unfortunately, if this login procedure happenned to often, you would get rate limited on login
|
|
||||||
attempts quite rapidly.
|
|
||||||
To bypass this issue, once sucessfully logged in Easybridge will store a *client pickle* in your account
|
|
||||||
configuration, i.e. a blob that contains your Facebook cookies so that the username+password procedure
|
|
||||||
doesn't need to be repeated everytime Easybridge restarts.
|
|
||||||
|
|
||||||
If the automated username+password procedure fails, the user can generate a client pickle from the
|
|
||||||
command line and use that in the configuration instead of entering a username and a password.
|
|
||||||
To use this method, **do not enter your password in Easybridge when configuring the Messenger backend (only enter your email address)**.
|
|
||||||
Generate your client pickle by running the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
./external/messenger.py create_client_pickle
|
|
||||||
```
|
|
||||||
|
|
||||||
This procedure will ask for your email and password and attempt to log you in to Facebook.
|
|
||||||
If it succeeds, it will print several dozen lines of data looking like `eJyVVlt....X9cgyfgY7mJaK`.
|
|
||||||
Then, when configuring the Messenger backend in Easybridge,
|
|
||||||
instead of entering your password,
|
|
||||||
enter the obtained client pickle string in the appropriate field.
|
|
||||||
|
|
541
account.go
541
account.go
|
@ -1,541 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
|
|
||||||
// Necessary for them to register their protocols
|
|
||||||
_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/external"
|
|
||||||
_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc"
|
|
||||||
_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/mattermost"
|
|
||||||
_ "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/xmpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Account struct {
|
|
||||||
MatrixUser string
|
|
||||||
AccountName string
|
|
||||||
Protocol string
|
|
||||||
Config map[string]string
|
|
||||||
|
|
||||||
Conn Connector
|
|
||||||
JoinedRooms map[RoomID]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountsLock sync.Mutex
|
|
||||||
var registeredAccounts = map[string]map[string]*Account{}
|
|
||||||
|
|
||||||
func SetAccount(mxid string, name string, protocol string, config map[string]string) error {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
if _, ok := registeredAccounts[mxid]; !ok {
|
|
||||||
registeredAccounts[mxid] = make(map[string]*Account)
|
|
||||||
}
|
|
||||||
accounts := registeredAccounts[mxid]
|
|
||||||
|
|
||||||
// Check we can create connector
|
|
||||||
proto, ok := Protocols[protocol]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Invalid protocol: %s", protocol)
|
|
||||||
}
|
|
||||||
conn := proto.NewConnector()
|
|
||||||
if conn == nil {
|
|
||||||
return fmt.Errorf("Could not create connector for protocol %s", protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the account existed already, close and drop connector
|
|
||||||
if prev_acct, ok := accounts[name]; ok {
|
|
||||||
if prev_acct.Protocol == protocol && reflect.DeepEqual(config, prev_acct.Config) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
go prev_acct.Conn.Close()
|
|
||||||
delete(accounts, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure and connect
|
|
||||||
account := &Account{
|
|
||||||
MatrixUser: mxid,
|
|
||||||
AccountName: name,
|
|
||||||
Protocol: protocol,
|
|
||||||
Config: config,
|
|
||||||
Conn: conn,
|
|
||||||
JoinedRooms: map[RoomID]bool{},
|
|
||||||
}
|
|
||||||
conn.SetHandler(account)
|
|
||||||
|
|
||||||
accounts[name] = account
|
|
||||||
go account.connect()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListAccounts(mxUser string) []*Account {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
ret := []*Account{}
|
|
||||||
if accts, ok := registeredAccounts[mxUser]; ok {
|
|
||||||
for _, acct := range accts {
|
|
||||||
ret = append(ret, acct)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindAccount(mxUser string, name string) *Account {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
if u, ok := registeredAccounts[mxUser]; ok {
|
|
||||||
if a, ok := u[name]; ok {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindJoinedAccount(mxUser string, protocol string, room RoomID) *Account {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
if u, ok := registeredAccounts[mxUser]; ok {
|
|
||||||
for _, acct := range u {
|
|
||||||
if acct.Protocol == protocol {
|
|
||||||
if j, ok := acct.JoinedRooms[room]; ok && j {
|
|
||||||
return acct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveAccount(mxUser string, name string) {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
if u, ok := registeredAccounts[mxUser]; ok {
|
|
||||||
if acct, ok := u[name]; ok {
|
|
||||||
acct.Conn.Close()
|
|
||||||
}
|
|
||||||
delete(u, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CloseAllAccountsForShutdown() {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
for _, accl := range registeredAccounts {
|
|
||||||
for _, acct := range accl {
|
|
||||||
log.Printf("Closing %s %s (%s)", acct.MatrixUser, acct.AccountName, acct.Protocol)
|
|
||||||
acct.Conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func SaveDbAccounts(mxid string, key *[32]byte) {
|
|
||||||
accountsLock.Lock()
|
|
||||||
defer accountsLock.Unlock()
|
|
||||||
|
|
||||||
if accounts, ok := registeredAccounts[mxid]; ok {
|
|
||||||
for name, acct := range accounts {
|
|
||||||
var entry DbAccountConfig
|
|
||||||
db.Where(&DbAccountConfig{
|
|
||||||
MxUserID: mxid,
|
|
||||||
Name: name,
|
|
||||||
}).Assign(&DbAccountConfig{
|
|
||||||
Protocol: acct.Protocol,
|
|
||||||
Config: encryptAccountConfig(acct.Config, key),
|
|
||||||
}).FirstOrCreate(&entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadDbAccounts(mxid string, key *[32]byte) {
|
|
||||||
var allAccounts []DbAccountConfig
|
|
||||||
db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts)
|
|
||||||
for _, acct := range allAccounts {
|
|
||||||
config, err := decryptAccountConfig(acct.Config, key)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf("Could not decrypt stored configuration for account %s (%s)", acct.Name, acct.Protocol)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = SetAccount(mxid, acct.Name, acct.Protocol, config)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(mxid, "Could not setup account %s: %s", acct.Name, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (a *Account) ezbrMessagef(format string, args ...interface{}) {
|
|
||||||
msg := fmt.Sprintf(format, args...)
|
|
||||||
msg = fmt.Sprintf("%s %s: %s", a.Protocol, a.AccountName, msg)
|
|
||||||
ezbrSystemSend(a.MatrixUser, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) connect() {
|
|
||||||
log.Printf("Connecting %s %s (%s)", a.MatrixUser, a.AccountName, a.Protocol)
|
|
||||||
ezbrSystemSendf(a.MatrixUser, "Connecting to account %s (%s)", a.AccountName, a.Protocol)
|
|
||||||
|
|
||||||
err := a.Conn.Configure(a.Config)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(a.MatrixUser, "%s (%s) cannot connect: %s", a.AccountName, a.Protocol, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var autojoin []DbJoinedRoom
|
|
||||||
db.Where(&DbJoinedRoom{
|
|
||||||
MxUserID: a.MatrixUser,
|
|
||||||
Protocol: a.Protocol,
|
|
||||||
AccountName: a.AccountName,
|
|
||||||
}).Find(&autojoin)
|
|
||||||
for _, aj := range autojoin {
|
|
||||||
err := a.Conn.Join(aj.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(a.MatrixUser, "%s (%s) cannot join %s: %s", a.AccountName, a.Protocol, aj.RoomID, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) addAutojoin(roomId RoomID) {
|
|
||||||
var entry DbJoinedRoom
|
|
||||||
db.Where(&DbJoinedRoom{
|
|
||||||
MxUserID: a.MatrixUser,
|
|
||||||
Protocol: a.Protocol,
|
|
||||||
AccountName: a.AccountName,
|
|
||||||
RoomID: roomId,
|
|
||||||
}).FirstOrCreate(&entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) delAutojoin(roomId RoomID) {
|
|
||||||
db.Where(&DbJoinedRoom{
|
|
||||||
MxUserID: a.MatrixUser,
|
|
||||||
Protocol: a.Protocol,
|
|
||||||
AccountName: a.AccountName,
|
|
||||||
RoomID: roomId,
|
|
||||||
}).Delete(&DbJoinedRoom{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Begin event handlers ----
|
|
||||||
|
|
||||||
func (a *Account) SystemMessage(msg string) {
|
|
||||||
a.ezbrMessagef("%s", msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) SaveConfig(config Configuration) {
|
|
||||||
a.Config = config
|
|
||||||
if key, ok := userKeys[a.MatrixUser]; ok {
|
|
||||||
var entry DbAccountConfig
|
|
||||||
db.Where(&DbAccountConfig{
|
|
||||||
MxUserID: a.MatrixUser,
|
|
||||||
Name: a.AccountName,
|
|
||||||
}).Assign(&DbAccountConfig{
|
|
||||||
Protocol: a.Protocol,
|
|
||||||
Config: encryptAccountConfig(a.Config, key),
|
|
||||||
}).FirstOrCreate(&entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) Joined(roomId RoomID) {
|
|
||||||
err := a.joinedInternal(roomId)
|
|
||||||
if err != nil {
|
|
||||||
a.ezbrMessagef("Dropping Account.Joined %s: %s", roomId, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) joinedInternal(roomId RoomID) error {
|
|
||||||
a.JoinedRooms[roomId] = true
|
|
||||||
|
|
||||||
a.addAutojoin(roomId)
|
|
||||||
|
|
||||||
mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("Joined %s (%s)\n", roomId, a.MatrixUser)
|
|
||||||
|
|
||||||
err = mx.RoomInvite(mx_room_id, a.MatrixUser)
|
|
||||||
if err != nil && strings.Contains(err.Error(), "already in the room") {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (a *Account) Left(roomId RoomID) {
|
|
||||||
err := a.leftInternal(roomId)
|
|
||||||
if err != nil {
|
|
||||||
a.ezbrMessagef("Dropping Account.Left %s: %s", roomId, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) leftInternal(roomId RoomID) error {
|
|
||||||
delete(a.JoinedRooms, roomId)
|
|
||||||
|
|
||||||
a.delAutojoin(roomId)
|
|
||||||
|
|
||||||
mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("Left %s (%s)\n", roomId, a.MatrixUser)
|
|
||||||
|
|
||||||
err = mx.RoomKick(mx_room_id, a.MatrixUser, fmt.Sprintf("got leave room event on %s", a.Protocol))
|
|
||||||
if err != nil && strings.Contains(err.Error(), "not in the room") {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (a *Account) UserInfoUpdated(user UserID, info *UserInfo) {
|
|
||||||
err := a.userInfoUpdatedInternal(user, info)
|
|
||||||
if err != nil {
|
|
||||||
a.ezbrMessagef("Dropping Account.UserInfoUpdated %s: %s", user, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) userInfoUpdatedInternal(user UserID, info *UserInfo) error {
|
|
||||||
mx_user_id, err := dbGetMxUser(a.Protocol, user)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.DisplayName != "" {
|
|
||||||
err2 := mx.ProfileDisplayname(mx_user_id, fmt.Sprintf("%s (%s)", info.DisplayName, a.Protocol))
|
|
||||||
if err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Avatar.MediaObject != nil {
|
|
||||||
cache_key := fmt.Sprintf("%s/user_avatar/%s", a.Protocol, user)
|
|
||||||
cache_val := info.Avatar.Filename()
|
|
||||||
if cache_val == "" || dbKvGet(cache_key) != cache_val {
|
|
||||||
err2 := mx.ProfileAvatar(mx_user_id, info.Avatar)
|
|
||||||
if err2 == nil {
|
|
||||||
dbKvPut(cache_key, cache_val)
|
|
||||||
} else {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (a *Account) RoomInfoUpdated(roomId RoomID, author UserID, info *RoomInfo) {
|
|
||||||
err := a.roomInfoUpdatedInternal(roomId, author, info)
|
|
||||||
if err != nil {
|
|
||||||
a.ezbrMessagef("Dropping Account.RoomInfoUpdated %s: %s", roomId, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) roomInfoUpdatedInternal(roomId RoomID, author UserID, info *RoomInfo) error {
|
|
||||||
mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
as_mxid := ezbrMxId()
|
|
||||||
if author == a.Conn.User() {
|
|
||||||
as_mxid = a.MatrixUser
|
|
||||||
} else if len(author) > 0 {
|
|
||||||
mx_user_id, err2 := dbGetMxUser(a.Protocol, author)
|
|
||||||
if err2 == nil {
|
|
||||||
as_mxid = mx_user_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Topic != "" {
|
|
||||||
err2 := mx.RoomTopicAs(mx_room_id, info.Topic, as_mxid)
|
|
||||||
if err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Name != "" {
|
|
||||||
name := fmt.Sprintf("%s (%s)", info.Name, a.Protocol)
|
|
||||||
err2 := mx.RoomNameAs(mx_room_id, name, as_mxid)
|
|
||||||
if err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Picture.MediaObject != nil {
|
|
||||||
cache_key := fmt.Sprintf("%s/room_picture/%s", a.Protocol, roomId)
|
|
||||||
cache_val := info.Picture.Filename()
|
|
||||||
if cache_val == "" || dbKvGet(cache_key) != cache_val {
|
|
||||||
err2 := mx.RoomAvatarAs(mx_room_id, info.Picture, as_mxid)
|
|
||||||
if err2 == nil {
|
|
||||||
dbKvPut(cache_key, cache_val)
|
|
||||||
} else {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (a *Account) Event(event *Event) {
|
|
||||||
err := a.eventInternal(event)
|
|
||||||
if err != nil {
|
|
||||||
a.ezbrMessagef("Dropping Account.Event %s %s %s: %s", event.Author, event.Recipient, event.Room, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) eventInternal(event *Event) error {
|
|
||||||
// TODO: automatically ignore events that come from one of our bridged matrix users
|
|
||||||
// TODO: deduplicate events if we have several matrix users joined the same room (hard problem)
|
|
||||||
|
|
||||||
mx_user_id, err := dbGetMxUser(a.Protocol, event.Author)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Type == EVENT_JOIN {
|
|
||||||
log.Tracef("%s join %s %s", a.Protocol, event.Author, event.Room)
|
|
||||||
mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mx.RoomInvite(mx_room_id, mx_user_id)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "already in the room") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mx.RoomJoinAs(mx_room_id, mx_user_id)
|
|
||||||
} else if event.Type == EVENT_LEAVE {
|
|
||||||
log.Tracef("%s join %s %s", a.Protocol, event.Author, event.Room)
|
|
||||||
mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mx.RoomLeaveAs(mx_room_id, mx_user_id)
|
|
||||||
} else {
|
|
||||||
log.Tracef("%s msg %s %s", a.Protocol, event.Author, event.Room)
|
|
||||||
mx_room_id := ""
|
|
||||||
|
|
||||||
if len(event.Room) > 0 {
|
|
||||||
mx_room_id, err = dbGetMxRoom(a.Protocol, event.Room)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mx_room_id, err = dbGetMxPmRoom(a.Protocol, event.Author, mx_user_id, a.MatrixUser, a.AccountName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cache_key string
|
|
||||||
if event.Id != "" {
|
|
||||||
// Use mx_room_id as a lock slot key, because this section is
|
|
||||||
// concurrent with the part that sends events out in server.go,
|
|
||||||
// and at that point we don't know the event's ID yet
|
|
||||||
// since it will be attributed by the backend during the call to send
|
|
||||||
dbLockSlot(mx_room_id)
|
|
||||||
defer dbUnlockSlot(mx_room_id)
|
|
||||||
|
|
||||||
// If the event has an ID, make sure it is processed only once
|
|
||||||
cache_key = fmt.Sprintf("%s/event_seen/%s/%s",
|
|
||||||
a.Protocol, mx_room_id, event.Id)
|
|
||||||
if dbKvGet(cache_key) == "yes" {
|
|
||||||
// false: cache key was not modified, meaning we
|
|
||||||
// already saw the event
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typ := "m.text"
|
|
||||||
if event.Type == EVENT_ACTION {
|
|
||||||
typ = "m.emote"
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mx.SendMessageAs(mx_room_id, typ, event.Text, mx_user_id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Attachments != nil {
|
|
||||||
for _, file := range event.Attachments {
|
|
||||||
mxfile, err := mx.UploadMedia(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
content := map[string]interface{}{
|
|
||||||
"body": mxfile.Filename(),
|
|
||||||
"filename": mxfile.Filename(),
|
|
||||||
"url": fmt.Sprintf("mxc://%s/%s", mxfile.MxcServer, mxfile.MxcMediaId),
|
|
||||||
}
|
|
||||||
if sz := mxfile.ImageSize(); sz != nil {
|
|
||||||
content["msgtype"] = "m.image"
|
|
||||||
content["info"] = map[string]interface{}{
|
|
||||||
"mimetype": mxfile.Mimetype(),
|
|
||||||
"size": mxfile.Size(),
|
|
||||||
"w": sz.Width,
|
|
||||||
"h": sz.Height,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content["msgtype"] = "m.file"
|
|
||||||
content["info"] = map[string]interface{}{
|
|
||||||
"mimetype": mxfile.Mimetype(),
|
|
||||||
"size": mxfile.Size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = mx.SendAs(mx_room_id, "m.room.message", content, mx_user_id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark message as received in db
|
|
||||||
if cache_key != "" {
|
|
||||||
dbKvPutLocked(cache_key, "yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (a *Account) CacheGet(key string) string {
|
|
||||||
cache_key := fmt.Sprintf("%s/account/%s/%s/%s",
|
|
||||||
a.Protocol, a.MatrixUser, a.AccountName, key)
|
|
||||||
return dbKvGet(cache_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Account) CachePut(key string, value string) {
|
|
||||||
cache_key := fmt.Sprintf("%s/account/%s/%s/%s",
|
|
||||||
a.Protocol, a.MatrixUser, a.AccountName, key)
|
|
||||||
dbKvPut(cache_key, value)
|
|
||||||
}
|
|
192
appservice/account.go
Normal file
192
appservice/account.go
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
package appservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
MatrixUser string
|
||||||
|
AccountName string
|
||||||
|
Protocol string
|
||||||
|
Conn Connector
|
||||||
|
|
||||||
|
JoinedRooms map[RoomID]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredAccounts = map[string]map[string]*Account{}
|
||||||
|
|
||||||
|
func AddAccount(a *Account) {
|
||||||
|
if _, ok := registeredAccounts[a.MatrixUser]; !ok {
|
||||||
|
registeredAccounts[a.MatrixUser] = make(map[string]*Account)
|
||||||
|
}
|
||||||
|
registeredAccounts[a.MatrixUser][a.AccountName] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindAccount(mxUser string, name string) *Account {
|
||||||
|
if u, ok := registeredAccounts[mxUser]; ok {
|
||||||
|
if a, ok := u[name]; ok {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindJoinedAccount(mxUser string, protocol string, room RoomID) *Account {
|
||||||
|
if u, ok := registeredAccounts[mxUser]; ok {
|
||||||
|
for _, acct := range u {
|
||||||
|
if acct.Protocol == protocol {
|
||||||
|
if j, ok := acct.JoinedRooms[room]; ok && j {
|
||||||
|
return acct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveAccount(mxUser string, name string) {
|
||||||
|
if u, ok := registeredAccounts[mxUser]; ok {
|
||||||
|
delete(u, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
|
func (a *Account) Joined(roomId RoomID) {
|
||||||
|
a.JoinedRooms[roomId] = true
|
||||||
|
|
||||||
|
mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Joined %s (%s)\n", roomId, a.MatrixUser)
|
||||||
|
|
||||||
|
err = mxRoomInvite(mx_room_id, a.MatrixUser)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not invite %s to %s", a.MatrixUser, mx_room_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) Left(roomId RoomID) {
|
||||||
|
delete(a.JoinedRooms, roomId)
|
||||||
|
|
||||||
|
mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Joined %s (%s)\n", roomId, a.MatrixUser)
|
||||||
|
|
||||||
|
err = mxRoomKick(mx_room_id, a.MatrixUser, fmt.Sprintf("got leave room event on %s", a.Protocol))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not invite %s to %s", a.MatrixUser, mx_room_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) UserInfoUpdated(user UserID, info *UserInfo) {
|
||||||
|
mx_user_id, err := dbGetMxUser(a.Protocol, user)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.DisplayName != "" {
|
||||||
|
mxProfileDisplayname(mx_user_id, fmt.Sprintf("%s (%s)", info.DisplayName, a.Protocol))
|
||||||
|
}
|
||||||
|
if info.Avatar != nil {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) RoomInfoUpdated(roomId RoomID, author UserID, info *RoomInfo) {
|
||||||
|
mx_room_id, err := dbGetMxRoom(a.Protocol, roomId)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
as_mxid := ezbrMxId()
|
||||||
|
if len(author) > 0 {
|
||||||
|
mx_user_id, err := dbGetMxUser(a.Protocol, author)
|
||||||
|
if err == nil {
|
||||||
|
as_mxid = mx_user_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Topic != "" {
|
||||||
|
mxRoomTopicAs(mx_room_id, info.Topic, as_mxid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Name != "" {
|
||||||
|
mxRoomNameAs(mx_room_id, info.Name, as_mxid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Picture != nil {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) Event(event *Event) {
|
||||||
|
mx_user_id, err := dbGetMxUser(a.Protocol, event.Author)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Type == EVENT_JOIN {
|
||||||
|
log.Printf("%s join %s %s", a.Protocol, event.Author, event.Room)
|
||||||
|
mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mxRoomInvite(mx_room_id, mx_user_id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not invite %s to %s", a.MatrixUser, mx_room_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mxRoomJoinAs(mx_room_id, mx_user_id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not join %s as %s", a.MatrixUser, mx_room_id)
|
||||||
|
}
|
||||||
|
} else if event.Type == EVENT_LEAVE {
|
||||||
|
log.Printf("%s join %s %s", a.Protocol, event.Author, event.Room)
|
||||||
|
mx_room_id, err := dbGetMxRoom(a.Protocol, event.Room)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mxRoomLeaveAs(mx_room_id, mx_user_id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not leave %s as %s", a.MatrixUser, mx_room_id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("%s msg %s %s", a.Protocol, event.Author, event.Room)
|
||||||
|
mx_room_id := ""
|
||||||
|
|
||||||
|
if len(event.Room) > 0 {
|
||||||
|
mx_room_id, err = dbGetMxRoom(a.Protocol, event.Room)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mx_room_id, err = dbGetMxPmRoom(a.Protocol, event.Author, mx_user_id, a.MatrixUser, a.AccountName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typ := "m.text"
|
||||||
|
if event.Type == EVENT_ACTION {
|
||||||
|
typ = "m.emote"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mxSendMessageAs(mx_room_id, typ, event.Text, mx_user_id)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not send %s as %s", event.Text, mx_user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
202
appservice/db.go
Normal file
202
appservice/db.go
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
package appservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
|
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||||
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
|
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *gorm.DB
|
||||||
|
|
||||||
|
func InitDb() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
db, err = gorm.Open(config.DbType, config.DbPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db.AutoMigrate(&DbUserMap{})
|
||||||
|
db.Model(&DbUserMap{}).AddIndex("idx_protocol_user", "protocol", "user_id")
|
||||||
|
|
||||||
|
db.AutoMigrate(&DbRoomMap{})
|
||||||
|
db.Model(&DbRoomMap{}).AddIndex("idx_protocol_room", "protocol", "room_id")
|
||||||
|
|
||||||
|
db.AutoMigrate(&DbPmRoomMap{})
|
||||||
|
db.Model(&DbPmRoomMap{}).AddIndex("idx_protocol_user_account_user", "protocol", "user_id", "mx_user_id", "account_name")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User mapping between protocol user IDs and puppeted matrix ids
|
||||||
|
type DbUserMap struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
Protocol string
|
||||||
|
UserID connector.UserID
|
||||||
|
MxUserID string `gorm:"index:mxuserid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room mapping between Matrix rooms and outside rooms
|
||||||
|
type DbRoomMap struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
// Network protocol
|
||||||
|
Protocol string
|
||||||
|
|
||||||
|
// Room id on the bridged network
|
||||||
|
RoomID connector.RoomID
|
||||||
|
|
||||||
|
// Bridged room matrix id
|
||||||
|
MxRoomID string `gorm:"index:mxroomid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room mapping between Matrix rooms and private messages
|
||||||
|
type DbPmRoomMap struct {
|
||||||
|
gorm.Model
|
||||||
|
|
||||||
|
// User id and account name of the local end viewed on Matrix
|
||||||
|
MxUserID string
|
||||||
|
Protocol string
|
||||||
|
AccountName string
|
||||||
|
|
||||||
|
// User id to reach them
|
||||||
|
UserID connector.UserID
|
||||||
|
|
||||||
|
// Bridged room for PMs
|
||||||
|
MxRoomID string `gorm:"index:mxroomoid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
|
func dbGetMxRoom(protocol string, roomId connector.RoomID) (string, error) {
|
||||||
|
var room DbRoomMap
|
||||||
|
|
||||||
|
// Check if room exists in our mapping,
|
||||||
|
// If not create it
|
||||||
|
must_create := db.First(&room, DbRoomMap{
|
||||||
|
Protocol: protocol,
|
||||||
|
RoomID: roomId,
|
||||||
|
}).RecordNotFound()
|
||||||
|
if must_create {
|
||||||
|
alias := roomAlias(protocol, roomId)
|
||||||
|
// Lookup alias
|
||||||
|
mx_room_id, err := mxDirectoryRoom(fmt.Sprintf("#%s:%s", alias, config.MatrixDomain))
|
||||||
|
|
||||||
|
// If no alias found, create room
|
||||||
|
if err != nil {
|
||||||
|
name := fmt.Sprintf("%s (%s)", roomId, protocol)
|
||||||
|
|
||||||
|
mx_room_id, err = mxCreateRoom(name, alias, []string{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not create room for %s: %s", name, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
room = DbRoomMap{
|
||||||
|
Protocol: protocol,
|
||||||
|
RoomID: roomId,
|
||||||
|
MxRoomID: mx_room_id,
|
||||||
|
}
|
||||||
|
db.Create(&room)
|
||||||
|
}
|
||||||
|
log.Printf("Got room id: %s", room.MxRoomID)
|
||||||
|
|
||||||
|
return room.MxRoomID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbGetMxPmRoom(protocol string, them connector.UserID, themMxId string, usMxId string, usAccount string) (string, error) {
|
||||||
|
var room DbPmRoomMap
|
||||||
|
|
||||||
|
must_create := db.First(&room, DbPmRoomMap{
|
||||||
|
MxUserID: usMxId,
|
||||||
|
Protocol: protocol,
|
||||||
|
AccountName: usAccount,
|
||||||
|
UserID: them,
|
||||||
|
}).RecordNotFound()
|
||||||
|
if must_create {
|
||||||
|
name := fmt.Sprintf("%s (%s)", them, protocol)
|
||||||
|
|
||||||
|
mx_room_id, err := mxCreateDirectRoomAs(name, []string{usMxId}, themMxId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not create room for %s: %s", name, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mxRoomJoinAs(mx_room_id, themMxId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not join %s as %s", mx_room_id, themMxId)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
room = DbPmRoomMap{
|
||||||
|
MxUserID: usMxId,
|
||||||
|
Protocol: protocol,
|
||||||
|
AccountName: usAccount,
|
||||||
|
UserID: them,
|
||||||
|
MxRoomID: mx_room_id,
|
||||||
|
}
|
||||||
|
db.Create(&room)
|
||||||
|
}
|
||||||
|
log.Printf("Got PM room id: %s", room.MxRoomID)
|
||||||
|
|
||||||
|
return room.MxRoomID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbGetMxUser(protocol string, userId connector.UserID) (string, error) {
|
||||||
|
var user DbUserMap
|
||||||
|
|
||||||
|
must_create := db.First(&user, DbUserMap{
|
||||||
|
Protocol: protocol,
|
||||||
|
UserID: userId,
|
||||||
|
}).RecordNotFound()
|
||||||
|
if must_create {
|
||||||
|
username := userMxId(protocol, userId)
|
||||||
|
|
||||||
|
err := mxRegisterUser(username)
|
||||||
|
if err != nil {
|
||||||
|
if mxE, ok := err.(*mxlib.MxError); !ok || mxE.ErrCode != "M_USER_IN_USE" {
|
||||||
|
log.Printf("Could not register %s: %s", username, err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mxid := fmt.Sprintf("@%s:%s", username, config.MatrixDomain)
|
||||||
|
mxProfileDisplayname(mxid, fmt.Sprintf("%s (%s)", userId, protocol))
|
||||||
|
|
||||||
|
user = DbUserMap{
|
||||||
|
Protocol: protocol,
|
||||||
|
UserID: userId,
|
||||||
|
MxUserID: mxid,
|
||||||
|
}
|
||||||
|
db.Create(&user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.MxUserID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbIsPmRoom(mxRoomId string) *DbPmRoomMap {
|
||||||
|
var pm_room DbPmRoomMap
|
||||||
|
if db.First(&pm_room, DbPmRoomMap{MxRoomID: mxRoomId}).RecordNotFound() {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return &pm_room
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbIsPublicRoom(mxRoomId string) *DbRoomMap {
|
||||||
|
var room DbRoomMap
|
||||||
|
if db.First(&room, DbRoomMap{MxRoomID: mxRoomId}).RecordNotFound() {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return &room
|
||||||
|
}
|
||||||
|
}
|
255
appservice/matrix.go
Normal file
255
appservice/matrix.go
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
package appservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ezbrMxId() string {
|
||||||
|
return fmt.Sprintf("@%s:%s", registration.SenderLocalpart, config.MatrixDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
|
var httpClient *http.Client
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
tr := &http.Transport{
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
DisableCompression: true,
|
||||||
|
}
|
||||||
|
httpClient = &http.Client{Transport: tr}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxGetApiCall(endpoint string, response interface{}) error {
|
||||||
|
log.Debugf("Matrix GET request: %s\n", endpoint)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", config.Server + endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mxDoAndParse(req, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxPutApiCall(endpoint string, data interface{}, response interface{}) error {
|
||||||
|
body, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Matrix PUT request: %s %s\n", endpoint, string(body))
|
||||||
|
|
||||||
|
req, err := http.NewRequest("PUT", config.Server + endpoint, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return mxDoAndParse(req, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxPostApiCall(endpoint string, data interface{}, response interface{}) error {
|
||||||
|
body, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Matrix POST request: %s %s\n", endpoint, string(body))
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", config.Server + endpoint, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return mxDoAndParse(req, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxDoAndParse(req *http.Request, response interface{}) error {
|
||||||
|
req.Header.Add("Authorization", "Bearer " + registration.AsToken)
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
var e MxError
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&e)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("Response (%d): %#v\n", resp.StatusCode, e)
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Response: %#v\n", response)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
|
||||||
|
func mxRegisterUser(username string) error {
|
||||||
|
req := RegisterRequest{
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
var rep RegisterResponse
|
||||||
|
return mxPostApiCall("/_matrix/client/r0/register?kind=user", &req, &rep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxProfileDisplayname(userid string, displayname string) error {
|
||||||
|
req := ProfileDisplaynameRequest{
|
||||||
|
Displayname: displayname,
|
||||||
|
}
|
||||||
|
var rep struct{}
|
||||||
|
err := mxPutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/displayname?user_id=%s",
|
||||||
|
url.QueryEscape(userid), url.QueryEscape(userid)),
|
||||||
|
&req, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxDirectoryRoom(alias string) (string, error) {
|
||||||
|
var rep DirectoryRoomResponse
|
||||||
|
err := mxGetApiCall("/_matrix/client/r0/directory/room/" + url.QueryEscape(alias), &rep)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return rep.RoomId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxCreateRoom(name string, alias string, invite []string) (string, error) {
|
||||||
|
rq := CreateRoomRequest{
|
||||||
|
Preset: "private_chat",
|
||||||
|
RoomAliasName: alias,
|
||||||
|
Name: name,
|
||||||
|
Topic: "",
|
||||||
|
Invite: invite,
|
||||||
|
CreationContent: map[string]interface{} {
|
||||||
|
"m.federate": false,
|
||||||
|
},
|
||||||
|
PowerLevels: map[string]interface{} {
|
||||||
|
"invite": 100,
|
||||||
|
"events": map[string]interface{} {
|
||||||
|
"m.room.topic": 0,
|
||||||
|
"m.room.avatar": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var rep CreateRoomResponse
|
||||||
|
err := mxPostApiCall("/_matrix/client/r0/createRoom", &rq, &rep)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return rep.RoomId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxCreateDirectRoomAs(name string, invite []string, as_user string) (string, error) {
|
||||||
|
rq := CreateRoomNoAliasRequest{
|
||||||
|
Preset: "private_chat",
|
||||||
|
Name: name,
|
||||||
|
Topic: "",
|
||||||
|
Invite: invite,
|
||||||
|
CreationContent: map[string]interface{} {
|
||||||
|
"m.federate": false,
|
||||||
|
},
|
||||||
|
PowerLevels: map[string]interface{} {
|
||||||
|
"invite": 100,
|
||||||
|
},
|
||||||
|
IsDirect: true,
|
||||||
|
}
|
||||||
|
var rep CreateRoomResponse
|
||||||
|
err := mxPostApiCall("/_matrix/client/r0/createRoom?user_id=" + url.QueryEscape(as_user), &rq, &rep)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return rep.RoomId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxRoomInvite(room string, user string) error {
|
||||||
|
rq := RoomInviteRequest{
|
||||||
|
UserId: user,
|
||||||
|
}
|
||||||
|
var rep struct{}
|
||||||
|
err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/invite", &rq, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxRoomKick(room string, user string, reason string) error {
|
||||||
|
rq := RoomKickRequest{
|
||||||
|
UserId: user,
|
||||||
|
Reason: reason,
|
||||||
|
}
|
||||||
|
var rep struct{}
|
||||||
|
err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/kick", &rq, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxRoomJoinAs(room string, user string) error {
|
||||||
|
rq := struct{}{}
|
||||||
|
var rep RoomJoinResponse
|
||||||
|
err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/join?user_id=" + url.QueryEscape(user), &rq, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxRoomLeaveAs(room string, user string) error {
|
||||||
|
rq := struct{}{}
|
||||||
|
var rep struct{}
|
||||||
|
err := mxPostApiCall("/_matrix/client/r0/rooms/" + url.QueryEscape(room) + "/leave?user_id=" + url.QueryEscape(user), &rq, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxSendAs(room string, event_type string, content map[string]interface{}, user string) error {
|
||||||
|
txn_id := time.Now().UnixNano()
|
||||||
|
var rep RoomSendResponse
|
||||||
|
err := mxPutApiCall(fmt.Sprintf(
|
||||||
|
"/_matrix/client/r0/rooms/%s/send/%s/%d?user_id=%s",
|
||||||
|
url.QueryEscape(room), event_type, txn_id, url.QueryEscape(user)),
|
||||||
|
&content, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxSendMessageAs(room string, typ string, body string, user string) error {
|
||||||
|
content := map[string]interface{} {
|
||||||
|
"msgtype": typ,
|
||||||
|
"body": body,
|
||||||
|
}
|
||||||
|
return mxSendAs(room, "m.room.message", content, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxPutStateAs(room string, event_type string, key string, content map[string]interface{}, as_user string) error {
|
||||||
|
var rep RoomSendResponse
|
||||||
|
err := mxPutApiCall(fmt.Sprintf(
|
||||||
|
"/_matrix/client/r0/rooms/%s/state/%s/%s?user_id=%s",
|
||||||
|
url.QueryEscape(room), event_type, key, url.QueryEscape(as_user)),
|
||||||
|
&content, &rep)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxRoomNameAs(room string, name string, as_user string) error {
|
||||||
|
content := map[string]interface{} {
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
return mxPutStateAs(room, "m.room.name", "", content, as_user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mxRoomTopicAs(room string, topic string, as_user string) error {
|
||||||
|
content := map[string]interface{} {
|
||||||
|
"topic": topic,
|
||||||
|
}
|
||||||
|
return mxPutStateAs(room, "m.room.topic", "", content, as_user)
|
||||||
|
}
|
21
appservice/names.go
Normal file
21
appservice/names.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package appservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
|
)
|
||||||
|
|
||||||
|
func roomAlias(protocol string, id RoomID) string {
|
||||||
|
id2 := strings.ReplaceAll(string(id), "#", "")
|
||||||
|
id2 = strings.ReplaceAll(id2, "@", "__")
|
||||||
|
|
||||||
|
return fmt.Sprintf("_ezbr__%s__%s", id2, protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userMxId(protocol string, id UserID) string {
|
||||||
|
id2 := strings.ReplaceAll(string(id), "@", "__")
|
||||||
|
|
||||||
|
return fmt.Sprintf("_ezbr__%s__%s", id2, protocol)
|
||||||
|
}
|
160
appservice/server.go
Normal file
160
appservice/server.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package appservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
HttpBindAddr string
|
||||||
|
Server string
|
||||||
|
DbType string
|
||||||
|
DbPath string
|
||||||
|
MatrixDomain string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var registration *mxlib.Registration
|
||||||
|
var config *Config
|
||||||
|
|
||||||
|
func Start(r *mxlib.Registration, c *Config) (chan error, error) {
|
||||||
|
registration = r
|
||||||
|
config = c
|
||||||
|
|
||||||
|
err := InitDb()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mxRegisterUser(registration.SenderLocalpart)
|
||||||
|
if mxe, ok := err.(*mxlib.MxError); !ok || mxe.ErrCode != "M_USER_IN_USE" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = mxProfileDisplayname(ezbrMxId(), "Easybridge")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn)
|
||||||
|
router.HandleFunc("/transactions/{txnId}", handleTxn)
|
||||||
|
|
||||||
|
errch := make(chan error)
|
||||||
|
go func() {
|
||||||
|
log.Printf("Starting HTTP server on %s", config.HttpBindAddr)
|
||||||
|
err := http.ListenAndServe(config.HttpBindAddr, checkTokenAndLog(router))
|
||||||
|
if err != nil {
|
||||||
|
errch <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return errch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTokenAndLog(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
if strings.Join(r.Form["access_token"], "") != registration.HsToken {
|
||||||
|
http.Error(w, "Wrong or no token provided", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTxn(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == "PUT" {
|
||||||
|
var txn mxlib.Transaction
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&txn)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
log.Printf("JSON decode error: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Got transaction %#v\n", txn)
|
||||||
|
|
||||||
|
for i := range txn.Events {
|
||||||
|
handleTxnEvent(&txn.Events[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "{}\n")
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Expected PUT request", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTxnEvent(e *mxlib.Event) {
|
||||||
|
if e.Type == "m.room.message" {
|
||||||
|
ev := &connector.Event{
|
||||||
|
Type: connector.EVENT_MESSAGE,
|
||||||
|
Text: e.Content["body"].(string),
|
||||||
|
}
|
||||||
|
typ := e.Content["msgtype"].(string)
|
||||||
|
if typ == "m.emote" {
|
||||||
|
ev.Type = connector.EVENT_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up if this is a private message room
|
||||||
|
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
|
||||||
|
acct := FindAccount(pm_room.MxUserID, pm_room.AccountName)
|
||||||
|
if acct != nil && e.Sender == pm_room.MxUserID {
|
||||||
|
ev.Author = acct.Conn.User()
|
||||||
|
ev.Recipient = pm_room.UserID
|
||||||
|
acct.Conn.Send(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up if this is a regular room
|
||||||
|
if room := dbIsPublicRoom(e.RoomId); room != nil {
|
||||||
|
acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID)
|
||||||
|
if acct != nil {
|
||||||
|
ev.Author = acct.Conn.User()
|
||||||
|
ev.Room = room.RoomID
|
||||||
|
acct.Conn.Send(ev)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Could not find room account for %s %s %s", e.Sender, room.Protocol, room.RoomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if e.Type == "m.room.member" {
|
||||||
|
ms := e.Content["membership"].(string)
|
||||||
|
if ms == "leave" {
|
||||||
|
// If leaving a PM room, we must delete it
|
||||||
|
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
|
||||||
|
them_mx := userMxId(pm_room.Protocol, pm_room.UserID)
|
||||||
|
mxRoomLeaveAs(e.RoomId, them_mx)
|
||||||
|
db.Delete(pm_room)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If leaving a public room, leave from server as well
|
||||||
|
if room := dbIsPublicRoom(e.RoomId); room != nil {
|
||||||
|
acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID)
|
||||||
|
if acct != nil {
|
||||||
|
acct.Conn.Leave(room.RoomID)
|
||||||
|
// TODO: manage autojoin list, remove this room
|
||||||
|
} else {
|
||||||
|
log.Debugf("Could not find room account for %s %s %s", e.Sender, room.Protocol, room.RoomID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if e.Type == "m.room.topic" {
|
||||||
|
if room := dbIsPublicRoom(e.RoomId); room != nil {
|
||||||
|
acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID)
|
||||||
|
if acct != nil {
|
||||||
|
acct.Conn.SetRoomInfo(room.RoomID, &connector.RoomInfo{
|
||||||
|
Topic: e.Content["topic"].(string),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,29 +43,3 @@ func (c Configuration) GetBool(k string, deflt ...bool) (bool, error) {
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("Missing configuration key: %s", k)
|
return false, fmt.Errorf("Missing configuration key: %s", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type Protocol struct {
|
|
||||||
NewConnector func() Connector
|
|
||||||
Schema ConfigSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigSchema []*ConfigEntry
|
|
||||||
|
|
||||||
type ConfigEntry struct {
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
Default string
|
|
||||||
FixedValue string
|
|
||||||
Required bool
|
|
||||||
IsPassword bool
|
|
||||||
IsNumeric bool
|
|
||||||
IsBoolean bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var Protocols = map[string]Protocol{}
|
|
||||||
|
|
||||||
func Register(name string, protocol Protocol) {
|
|
||||||
Protocols[name] = protocol
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package connector
|
package connector
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A generic connector framework for instant messaging protocols.
|
A generic connector framework for instant messaging protocols.
|
||||||
|
|
||||||
|
@ -53,39 +49,19 @@ type Connector interface {
|
||||||
Join(roomId RoomID) error
|
Join(roomId RoomID) error
|
||||||
|
|
||||||
// Try to invite someone to a channel
|
// Try to invite someone to a channel
|
||||||
// Or if roomId == "", just try adding them as friends
|
|
||||||
Invite(user UserID, roomId RoomID) error
|
Invite(user UserID, roomId RoomID) error
|
||||||
|
|
||||||
// Leave a channel
|
// Leave a channel
|
||||||
Leave(roomId RoomID)
|
Leave(roomId RoomID)
|
||||||
|
|
||||||
// Search for users
|
// Send an event
|
||||||
SearchForUsers(query string) ([]UserSearchResult, error)
|
Send(event *Event) error
|
||||||
|
|
||||||
// Send an event. Returns the ID of the created remote message.
|
|
||||||
// This ID is used to deduplicate messages: if it comes back, it should have the same Id
|
|
||||||
// than the one returned here.
|
|
||||||
// For backends that do not implement IDs (e.g. IRC), an empty string is returned.
|
|
||||||
// (FIXME how to deduplicate IRC messages?)
|
|
||||||
// The event that is fed in this function may have its ID already set,
|
|
||||||
// in which case the backend is free to re-use the ID or select a new one.
|
|
||||||
Send(event *Event) (string, error)
|
|
||||||
|
|
||||||
// Used to send user commands directly
|
|
||||||
// (first use case: receive 2-factor authentication codes)
|
|
||||||
UserCommand(string)
|
|
||||||
|
|
||||||
// Close the connection
|
// Close the connection
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
// Called to save updated configuration parameters
|
|
||||||
SaveConfig(config Configuration)
|
|
||||||
|
|
||||||
// Called to notify user of stuff going on
|
|
||||||
SystemMessage(message string)
|
|
||||||
|
|
||||||
// Called when a room was joined (automatically or by call to Connector.Join)
|
// Called when a room was joined (automatically or by call to Connector.Join)
|
||||||
Joined(roomId RoomID)
|
Joined(roomId RoomID)
|
||||||
|
|
||||||
|
@ -103,17 +79,9 @@ type Handler interface {
|
||||||
// Called when an event occurs in a room
|
// Called when an event occurs in a room
|
||||||
// This must not be called for events authored by the user of the connection
|
// This must not be called for events authored by the user of the connection
|
||||||
Event(event *Event)
|
Event(event *Event)
|
||||||
|
|
||||||
// These two functions enable the connector to access a simple key/value
|
|
||||||
// database to cache some information in order not to generate useless events.
|
|
||||||
// The connector should function when they are not implemented,
|
|
||||||
// in which case CacheGet always returns ""
|
|
||||||
CachePut(key string, value string)
|
|
||||||
CacheGet(key string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventType int
|
type EventType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EVENT_JOIN EventType = iota
|
EVENT_JOIN EventType = iota
|
||||||
EVENT_LEAVE
|
EVENT_LEAVE
|
||||||
|
@ -122,76 +90,47 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type EventType `json:"type"`
|
Type EventType
|
||||||
|
|
||||||
// If non-empty, the event Id is used to deduplicate events in a channel
|
|
||||||
// This is usefull for backends that provide a backlog of channel messages
|
|
||||||
// when (re-)joining a room
|
|
||||||
Id string `json:"id"`
|
|
||||||
|
|
||||||
// UserID of the user that sent the event
|
// UserID of the user that sent the event
|
||||||
// If this is a direct message event, this event can only have been authored
|
// If this is a direct message event, this event can only have been authored
|
||||||
// by the user we are talking to (and not by ourself)
|
// by the user we are talking to (and not by ourself)
|
||||||
Author UserID `json:"author"`
|
Author UserID
|
||||||
|
|
||||||
// UserID of the targetted user in the case of a direct message,
|
// UserID of the targetted user in the case of a direct message,
|
||||||
// empty if targetting a room
|
// empty if targetting a room
|
||||||
Recipient UserID `json:"recipient"`
|
Recipient UserID
|
||||||
|
|
||||||
// RoomID of the room where the event happenned or of the targetted room,
|
// RoomID of the room where the event happenned or of the targetted room,
|
||||||
// or empty string if it happenned by direct message
|
// or empty string if it happenned by direct message
|
||||||
Room RoomID `json:"room"`
|
Room RoomID
|
||||||
|
|
||||||
// Message text or action text
|
// Message text or action text
|
||||||
Text string `json:"text"`
|
Text string
|
||||||
|
|
||||||
// Attached files such as images
|
// Attached files such as images
|
||||||
Attachments []SMediaObject `json:"attachments"`
|
Attachements map[string]MediaObject
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserInfo struct {
|
type UserInfo struct {
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string
|
||||||
|
Avatar MediaObject
|
||||||
// If non-empty, the Filename of the avatar object will be used by Easybridge
|
|
||||||
// to deduplicate the update events and prevent needless reuploads.
|
|
||||||
// Example strategy that works for the mattermost backend: use the update timestamp as fictious file name
|
|
||||||
Avatar SMediaObject `json:"avatar"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomInfo struct {
|
type RoomInfo struct {
|
||||||
Name string `json:"name"`
|
Name string
|
||||||
Topic string `json:"topic"`
|
Topic string
|
||||||
|
Picture MediaObject
|
||||||
// Same deduplication comment as for UserInfo.Avatar
|
|
||||||
Picture SMediaObject `json:"picture"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSearchResult struct {
|
|
||||||
ID UserID `json:"id"`
|
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaObject interface {
|
type MediaObject interface {
|
||||||
Filename() string
|
Size() int
|
||||||
Size() int64
|
MimeType() string
|
||||||
Mimetype() string
|
|
||||||
|
|
||||||
// Returns the size of an image if it is an image, otherwise nil
|
// AsBytes: must always be implemented
|
||||||
ImageSize() *ImageSize
|
AsBytes() ([]byte, error)
|
||||||
|
|
||||||
// Read: must always be implemented
|
// AsString: not mandatory, may return an empty string
|
||||||
Read() (io.ReadCloser, error)
|
// If so, AsBytes() is the only way to retrieve the object
|
||||||
|
AsURL() string
|
||||||
// URL(): not mandatory, may return an empty string
|
|
||||||
// If so, Read() is the only way to retrieve the object
|
|
||||||
URL() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type SMediaObject struct {
|
|
||||||
MediaObject
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageSize struct {
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
}
|
}
|
||||||
|
|
43
connector/external/config.go
vendored
43
connector/external/config.go
vendored
|
@ -1,43 +0,0 @@
|
||||||
package external
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MESSENGER_PROTOCOL = "Messenger"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register(MESSENGER_PROTOCOL, Protocol{
|
|
||||||
NewConnector: func() Connector {
|
|
||||||
return &External{
|
|
||||||
protocol: MESSENGER_PROTOCOL,
|
|
||||||
command: "./external/messenger.py",
|
|
||||||
debug: (os.Getenv("EASYBRIDGE_MESSENGER_DEBUG") == "true"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schema: ConfigSchema{
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "email",
|
|
||||||
Description: "Email address",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "password",
|
|
||||||
Description: "Password",
|
|
||||||
IsPassword: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "client_pickle",
|
|
||||||
Description: "Client pickle (alternative login method)",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "initial_backlog",
|
|
||||||
Description: "Maximum number of messages to load when joining a channel",
|
|
||||||
IsNumeric: true,
|
|
||||||
Default: "100",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
488
connector/external/external.go
vendored
488
connector/external/external.go
vendored
|
@ -1,488 +0,0 @@
|
||||||
package external
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Serialization protocol
|
|
||||||
|
|
||||||
type extMessage struct {
|
|
||||||
// Header: message type and identifier
|
|
||||||
MsgType string `json:"_type"`
|
|
||||||
MsgId uint64 `json:"_id"`
|
|
||||||
|
|
||||||
// Message fields
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
EventId string `json:"event_id"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
Room RoomID `json:"room"`
|
|
||||||
User UserID `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type extMessageWithData struct {
|
|
||||||
extMessage
|
|
||||||
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possible values for MsgType
|
|
||||||
const (
|
|
||||||
// ezbr -> external
|
|
||||||
CONFIGURE = "configure"
|
|
||||||
GET_USER = "get_user"
|
|
||||||
SET_USER_INFO = "set_user_info"
|
|
||||||
SET_ROOM_INFO = "set_room_info"
|
|
||||||
JOIN = "join"
|
|
||||||
INVITE = "invite"
|
|
||||||
LEAVE = "leave"
|
|
||||||
SEARCH = "search"
|
|
||||||
SEND = "send"
|
|
||||||
USER_COMMAND = "user_command"
|
|
||||||
CLOSE = "close"
|
|
||||||
|
|
||||||
// external -> ezbr
|
|
||||||
SAVE_CONFIG = "save_config"
|
|
||||||
SYSTEM_MESSAGE = "system_message"
|
|
||||||
JOINED = "joined"
|
|
||||||
LEFT = "left"
|
|
||||||
USER_INFO_UPDATED = "user_info_updated"
|
|
||||||
ROOM_INFO_UPDATED = "room_info_updated"
|
|
||||||
EVENT = "event"
|
|
||||||
CACHE_PUT = "cache_put"
|
|
||||||
CACHE_GET = "cache_get"
|
|
||||||
|
|
||||||
// reply messages
|
|
||||||
// ezbr -> external: all must wait for a reply!
|
|
||||||
// external -> ezbr: only CACHE_GET produces a reply
|
|
||||||
REP_OK = "rep_ok"
|
|
||||||
REP_SEARCH_RESULTS = "rep_search_results"
|
|
||||||
REP_ERROR = "rep_error"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type External struct {
|
|
||||||
handler Handler
|
|
||||||
|
|
||||||
protocol string
|
|
||||||
command string
|
|
||||||
debug bool
|
|
||||||
|
|
||||||
config Configuration
|
|
||||||
|
|
||||||
recvPipe io.ReadCloser
|
|
||||||
sendPipe io.WriteCloser
|
|
||||||
sendJson *json.Encoder
|
|
||||||
|
|
||||||
generation int
|
|
||||||
proc *exec.Cmd
|
|
||||||
|
|
||||||
handlerChan chan *extMessageWithData
|
|
||||||
counter uint64
|
|
||||||
inflightRequests map[uint64]chan *extMessageWithData
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) SetHandler(h Handler) {
|
|
||||||
ext.handler = h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Protocol() string {
|
|
||||||
return ext.protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Configure(c Configuration) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if ext.proc != nil {
|
|
||||||
ext.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
ext.inflightRequests = map[uint64]chan *extMessageWithData{}
|
|
||||||
|
|
||||||
ext.generation += 1
|
|
||||||
|
|
||||||
ext.handlerChan = make(chan *extMessageWithData, 1000)
|
|
||||||
go ext.handlerLoop(ext.generation)
|
|
||||||
|
|
||||||
err = ext.setupProc(ext.generation)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go ext.restartLoop(ext.generation)
|
|
||||||
|
|
||||||
_, err = ext.cmd(extMessage{
|
|
||||||
MsgType: CONFIGURE,
|
|
||||||
}, c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Process management and communication logic
|
|
||||||
|
|
||||||
func (ext *External) setupProc(generation int) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
ext.proc = exec.Command(ext.command)
|
|
||||||
|
|
||||||
ext.recvPipe, err = ext.proc.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ext.sendPipe, err = ext.proc.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
send := io.Writer(ext.sendPipe)
|
|
||||||
recv := io.Reader(ext.recvPipe)
|
|
||||||
if ext.debug {
|
|
||||||
recv = io.TeeReader(recv, os.Stderr)
|
|
||||||
send = io.MultiWriter(send, os.Stderr)
|
|
||||||
ext.proc.Stderr = os.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
ext.sendJson = json.NewEncoder(send)
|
|
||||||
|
|
||||||
err = ext.proc.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go ext.recvLoop(recv, generation)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) restartLoop(generation int) {
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
if ext.proc == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
ext.proc.Wait()
|
|
||||||
if ext.generation != generation {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.Printf("Process %s stopped, restarting.", ext.command)
|
|
||||||
log.Printf("Generation %d vs %d", ext.generation, generation)
|
|
||||||
err := ext.setupProc(generation)
|
|
||||||
if err != nil {
|
|
||||||
ext.proc = nil
|
|
||||||
log.Warnf("Unable to restart %s: %s", ext.command, err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Warnf("More than 3 attempts (%s); abandonning.", ext.command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *extMessageWithData) UnmarshalJSON(jj []byte) error {
|
|
||||||
var c extMessage
|
|
||||||
|
|
||||||
err := json.Unmarshal(jj, &c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*m = extMessageWithData{extMessage: c}
|
|
||||||
switch c.MsgType {
|
|
||||||
case SAVE_CONFIG:
|
|
||||||
var cf struct {
|
|
||||||
Data Configuration `json:"data"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(jj, &cf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.Data = cf.Data
|
|
||||||
return nil
|
|
||||||
case USER_INFO_UPDATED:
|
|
||||||
var ui struct {
|
|
||||||
Data UserInfo `json:"data"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(jj, &ui)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.Data = &ui.Data
|
|
||||||
return nil
|
|
||||||
case ROOM_INFO_UPDATED:
|
|
||||||
var ri struct {
|
|
||||||
Data RoomInfo `json:"data"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(jj, &ri)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.Data = &ri.Data
|
|
||||||
return nil
|
|
||||||
case EVENT:
|
|
||||||
var ev struct {
|
|
||||||
Data Event `json:"data"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(jj, &ev)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.Data = &ev.Data
|
|
||||||
return nil
|
|
||||||
case REP_SEARCH_RESULTS:
|
|
||||||
var sr struct {
|
|
||||||
Data []UserSearchResult `json:"data"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(jj, &sr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.Data = sr.Data
|
|
||||||
return nil
|
|
||||||
case SYSTEM_MESSAGE, JOINED, LEFT, CACHE_PUT, CACHE_GET, REP_OK, REP_ERROR:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Invalid message type for message from external program: '%s'", c.MsgType)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) recvLoop(from io.Reader, generation int) {
|
|
||||||
scanner := bufio.NewScanner(from)
|
|
||||||
for scanner.Scan() {
|
|
||||||
var msg extMessageWithData
|
|
||||||
err := json.Unmarshal(scanner.Bytes(), &msg)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Failed to decode from %s: %s. Skipping line.", ext.command, err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if scanner.Err() != nil {
|
|
||||||
log.Warnf("Failed to read from %s: %s. Stopping here.", ext.command, scanner.Err().Error())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("GOT MESSAGE: %#v %#v", msg, msg.Data)
|
|
||||||
if strings.HasPrefix(msg.MsgType, "rep_") {
|
|
||||||
func() {
|
|
||||||
ext.lock.Lock()
|
|
||||||
defer ext.lock.Unlock()
|
|
||||||
if ch, ok := ext.inflightRequests[msg.MsgId]; ok {
|
|
||||||
ch <- &msg
|
|
||||||
delete(ext.inflightRequests, msg.MsgId)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
ext.handlerChan <- &msg
|
|
||||||
}
|
|
||||||
|
|
||||||
if ext.generation != generation {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) handlerLoop(generation int) {
|
|
||||||
for ext.handlerChan != nil && ext.generation == generation {
|
|
||||||
select {
|
|
||||||
case msg := <-ext.handlerChan:
|
|
||||||
ext.handleCmd(msg)
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) cmd(msg extMessage, data interface{}) (*extMessageWithData, error) {
|
|
||||||
msg_id := atomic.AddUint64(&ext.counter, 1)
|
|
||||||
|
|
||||||
msg.MsgId = msg_id
|
|
||||||
|
|
||||||
fullMsg := extMessageWithData{
|
|
||||||
extMessage: msg,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan *extMessageWithData)
|
|
||||||
|
|
||||||
func() {
|
|
||||||
ext.lock.Lock()
|
|
||||||
defer ext.lock.Unlock()
|
|
||||||
ext.inflightRequests[msg_id] = ch
|
|
||||||
}()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
ext.lock.Lock()
|
|
||||||
defer ext.lock.Unlock()
|
|
||||||
delete(ext.inflightRequests, msg_id)
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := ext.sendJson.Encode(&fullMsg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case rep := <-ch:
|
|
||||||
if rep.MsgType == REP_ERROR {
|
|
||||||
return nil, fmt.Errorf("%s: %s", msg.MsgType, rep.Error)
|
|
||||||
} else {
|
|
||||||
return rep, nil
|
|
||||||
}
|
|
||||||
case <-time.After(30 * time.Second):
|
|
||||||
return nil, fmt.Errorf("(%s) timeout", msg.MsgType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Close() {
|
|
||||||
ext.generation += 1
|
|
||||||
|
|
||||||
ext.sendJson.Encode(&extMessage{
|
|
||||||
MsgType: CLOSE,
|
|
||||||
})
|
|
||||||
ext.proc.Process.Signal(os.Interrupt)
|
|
||||||
|
|
||||||
ext.recvPipe.Close()
|
|
||||||
ext.sendPipe.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
if ext.proc != nil {
|
|
||||||
log.Info("Sending SIGKILL to external process (did not terminate within 1 second)")
|
|
||||||
ext.proc.Process.Kill()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
ext.proc.Wait()
|
|
||||||
log.Info("External process exited")
|
|
||||||
|
|
||||||
ext.proc = nil
|
|
||||||
ext.recvPipe = nil
|
|
||||||
ext.sendPipe = nil
|
|
||||||
ext.sendJson = nil
|
|
||||||
ext.handlerChan = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Actual message handling :)
|
|
||||||
|
|
||||||
func (ext *External) handleCmd(msg *extMessageWithData) {
|
|
||||||
switch msg.MsgType {
|
|
||||||
case SAVE_CONFIG:
|
|
||||||
ext.handler.SaveConfig(msg.Data.(Configuration))
|
|
||||||
case SYSTEM_MESSAGE:
|
|
||||||
ext.handler.SystemMessage(msg.Value)
|
|
||||||
case JOINED:
|
|
||||||
ext.handler.Joined(msg.Room)
|
|
||||||
case LEFT:
|
|
||||||
ext.handler.Left(msg.Room)
|
|
||||||
case USER_INFO_UPDATED:
|
|
||||||
ext.handler.UserInfoUpdated(msg.User, msg.Data.(*UserInfo))
|
|
||||||
case ROOM_INFO_UPDATED:
|
|
||||||
ext.handler.RoomInfoUpdated(msg.Room, msg.User, msg.Data.(*RoomInfo))
|
|
||||||
case EVENT:
|
|
||||||
ext.handler.Event(msg.Data.(*Event))
|
|
||||||
case CACHE_PUT:
|
|
||||||
ext.handler.CachePut(msg.Key, msg.Value)
|
|
||||||
case CACHE_GET:
|
|
||||||
value := ext.handler.CacheGet(msg.Key)
|
|
||||||
ext.sendJson.Encode(&extMessage{
|
|
||||||
MsgType: REP_OK,
|
|
||||||
MsgId: msg.MsgId,
|
|
||||||
Key: msg.Key,
|
|
||||||
Value: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) User() UserID {
|
|
||||||
rep, err := ext.cmd(extMessage{
|
|
||||||
MsgType: GET_USER,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Unable to get user! %s", err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return rep.User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) SetUserInfo(info *UserInfo) error {
|
|
||||||
_, err := ext.cmd(extMessage{
|
|
||||||
MsgType: SET_USER_INFO,
|
|
||||||
}, info)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) SetRoomInfo(room RoomID, info *RoomInfo) error {
|
|
||||||
_, err := ext.cmd(extMessage{
|
|
||||||
MsgType: SET_ROOM_INFO,
|
|
||||||
Room: room,
|
|
||||||
}, info)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Join(room RoomID) error {
|
|
||||||
_, err := ext.cmd(extMessage{
|
|
||||||
MsgType: JOIN,
|
|
||||||
Room: room,
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Invite(user UserID, room RoomID) error {
|
|
||||||
_, err := ext.cmd(extMessage{
|
|
||||||
MsgType: INVITE,
|
|
||||||
User: user,
|
|
||||||
Room: room,
|
|
||||||
}, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Leave(room RoomID) {
|
|
||||||
_, err := ext.cmd(extMessage{
|
|
||||||
MsgType: LEAVE,
|
|
||||||
Room: room,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Could not leave %s: %s", room, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) SearchForUsers(query string) ([]UserSearchResult, error) {
|
|
||||||
rep, err := ext.cmd(extMessage{
|
|
||||||
MsgType: SEARCH,
|
|
||||||
}, query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if rep.MsgType != REP_SEARCH_RESULTS {
|
|
||||||
return nil, fmt.Errorf("Invalid result type from external: %s", rep.MsgType)
|
|
||||||
}
|
|
||||||
return rep.Data.([]UserSearchResult), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) Send(event *Event) (string, error) {
|
|
||||||
rep, err := ext.cmd(extMessage{
|
|
||||||
MsgType: SEND,
|
|
||||||
}, event)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return rep.EventId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ext *External) UserCommand(cm string) {
|
|
||||||
ext.cmd(extMessage{
|
|
||||||
MsgType: USER_COMMAND,
|
|
||||||
Value: cm,
|
|
||||||
}, nil)
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const IRC_PROTOCOL = "IRC"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register(IRC_PROTOCOL, Protocol{
|
|
||||||
NewConnector: func() Connector { return &IRC{} },
|
|
||||||
Schema: ConfigSchema{
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "nick",
|
|
||||||
Description: "Nickname",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "server",
|
|
||||||
Description: "Server",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "port",
|
|
||||||
Description: "Port",
|
|
||||||
IsNumeric: true,
|
|
||||||
Default: "6667",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "ssl",
|
|
||||||
Description: "Use SSL",
|
|
||||||
IsBoolean: true,
|
|
||||||
Default: "false",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "server_pass",
|
|
||||||
Description: "Server password (authenticate with PASS command)",
|
|
||||||
IsPassword: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "sasl_user",
|
|
||||||
Description: "Username for SASL authentication",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "sasl_pass",
|
|
||||||
Description: "Password for SASL authentication",
|
|
||||||
IsPassword: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,10 +1,10 @@
|
||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"time"
|
||||||
_ "os"
|
_ "os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"fmt"
|
||||||
|
|
||||||
"github.com/lrstanley/girc"
|
"github.com/lrstanley/girc"
|
||||||
|
|
||||||
|
@ -25,16 +25,14 @@ type IRC struct {
|
||||||
name string
|
name string
|
||||||
server string
|
server string
|
||||||
conn *girc.Client
|
conn *girc.Client
|
||||||
|
|
||||||
joinedRooms map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) SetHandler(h Handler) {
|
func (irc *IRC) SetHandler(h Handler) {
|
||||||
irc.handler = h
|
irc.handler = h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) Protocol() string {
|
func(irc *IRC) Protocol() string {
|
||||||
return IRC_PROTOCOL
|
return "irc"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) Configure(c Configuration) error {
|
func (irc *IRC) Configure(c Configuration) error {
|
||||||
|
@ -64,25 +62,11 @@ func (irc *IRC) Configure(c Configuration) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
server_pass, _ := c.GetString("server_pass", "")
|
|
||||||
sasl_user, _ := c.GetString("sasl_user", "")
|
|
||||||
sasl_pass, _ := c.GetString("sasl_pass", "")
|
|
||||||
|
|
||||||
var sasl girc.SASLMech
|
|
||||||
if sasl_user != "" && sasl_pass != "" {
|
|
||||||
sasl = &girc.SASLPlain{
|
|
||||||
User: sasl_user,
|
|
||||||
Pass: sasl_pass,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client := girc.New(girc.Config{
|
client := girc.New(girc.Config{
|
||||||
Server: irc.server,
|
Server: irc.server,
|
||||||
ServerPass: server_pass,
|
|
||||||
Port: port,
|
Port: port,
|
||||||
Nick: irc.nick,
|
Nick: irc.nick,
|
||||||
User: irc.nick,
|
User: irc.nick,
|
||||||
SASL: sasl,
|
|
||||||
//Out: os.Stderr,
|
//Out: os.Stderr,
|
||||||
SSL: ssl,
|
SSL: ssl,
|
||||||
})
|
})
|
||||||
|
@ -97,13 +81,11 @@ func (irc *IRC) Configure(c Configuration) error {
|
||||||
client.Handlers.Add(girc.TOPIC, irc.ircTopic)
|
client.Handlers.Add(girc.TOPIC, irc.ircTopic)
|
||||||
client.Handlers.Add(girc.RPL_TOPIC, irc.ircRplTopic)
|
client.Handlers.Add(girc.RPL_TOPIC, irc.ircRplTopic)
|
||||||
|
|
||||||
irc.joinedRooms = make(map[string]bool)
|
|
||||||
|
|
||||||
irc.conn = client
|
irc.conn = client
|
||||||
go irc.connectLoop(client)
|
go irc.connectLoop(client)
|
||||||
|
|
||||||
for i := 0; i < 42; i++ {
|
for i := 0; i < 42; i++ {
|
||||||
time.Sleep(time.Duration(1) * time.Second)
|
time.Sleep(time.Duration(1)*time.Second)
|
||||||
if irc.conn != client {
|
if irc.conn != client {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -120,10 +102,7 @@ func (irc *IRC) User() UserID {
|
||||||
|
|
||||||
func (irc *IRC) checkRoomId(id RoomID) (string, error) {
|
func (irc *IRC) checkRoomId(id RoomID) (string, error) {
|
||||||
x := strings.Split(string(id), "@")
|
x := strings.Split(string(id), "@")
|
||||||
if len(x) == 1 {
|
if len(x) != 2 || x[1] != irc.server || x[0][0] != '#' {
|
||||||
return "", fmt.Errorf("Please write whole room ID with server: %s@%s", id, irc.server)
|
|
||||||
}
|
|
||||||
if x[0][0] != '#' || len(x) != 2 || x[1] != irc.server {
|
|
||||||
return "", fmt.Errorf("Invalid room ID: %s", id)
|
return "", fmt.Errorf("Invalid room ID: %s", id)
|
||||||
}
|
}
|
||||||
return x[0], nil
|
return x[0], nil
|
||||||
|
@ -131,10 +110,7 @@ func (irc *IRC) checkRoomId(id RoomID) (string, error) {
|
||||||
|
|
||||||
func (irc *IRC) checkUserId(id UserID) (string, error) {
|
func (irc *IRC) checkUserId(id UserID) (string, error) {
|
||||||
x := strings.Split(string(id), "@")
|
x := strings.Split(string(id), "@")
|
||||||
if len(x) == 1 {
|
if len(x) != 2 || x[1] != irc.server || x[0][0] == '#' {
|
||||||
return "", fmt.Errorf("Please write whole user ID with server: %s@%s", id, irc.server)
|
|
||||||
}
|
|
||||||
if x[0][0] == '#' || len(x) != 2 || x[1] != irc.server {
|
|
||||||
return "", fmt.Errorf("Invalid user ID: %s", id)
|
return "", fmt.Errorf("Invalid user ID: %s", id)
|
||||||
}
|
}
|
||||||
return x[0], nil
|
return x[0], nil
|
||||||
|
@ -145,32 +121,24 @@ func (irc *IRC) SetUserInfo(info *UserInfo) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
|
func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
|
||||||
if irc.conn == nil {
|
|
||||||
return fmt.Errorf("Not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := irc.checkRoomId(roomId)
|
ch, err := irc.checkRoomId(roomId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.Topic != "" {
|
|
||||||
irc.conn.Cmd.Topic(ch, info.Topic)
|
|
||||||
}
|
|
||||||
if info.Name != "" && info.Name != ch {
|
if info.Name != "" && info.Name != ch {
|
||||||
return fmt.Errorf("May not change IRC room name to other than %s", ch)
|
return fmt.Errorf("May not change IRC room name to other than %s", ch)
|
||||||
}
|
}
|
||||||
if info.Picture.MediaObject != nil {
|
if info.Picture != nil {
|
||||||
return fmt.Errorf("Room picture not supported on IRC")
|
return fmt.Errorf("Room picture not supported on IRC")
|
||||||
}
|
}
|
||||||
|
if info.Topic != "" {
|
||||||
|
irc.conn.Cmd.Topic(ch, info.Topic)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) Join(roomId RoomID) error {
|
func (irc *IRC) Join(roomId RoomID) error {
|
||||||
if irc.conn == nil {
|
|
||||||
return fmt.Errorf("Not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := irc.checkRoomId(roomId)
|
ch, err := irc.checkRoomId(roomId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -181,20 +149,12 @@ func (irc *IRC) Join(roomId RoomID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) Invite(userId UserID, roomId RoomID) error {
|
func (irc *IRC) Invite(userId UserID, roomId RoomID) error {
|
||||||
if irc.conn == nil {
|
ch, err := irc.checkRoomId(roomId)
|
||||||
return fmt.Errorf("Not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
who, err := irc.checkUserId(userId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if roomId == "" {
|
who, err := irc.checkUserId(userId)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := irc.checkRoomId(roomId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -204,10 +164,6 @@ func (irc *IRC) Invite(userId UserID, roomId RoomID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) Leave(roomId RoomID) {
|
func (irc *IRC) Leave(roomId RoomID) {
|
||||||
if irc.conn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := irc.checkRoomId(roomId)
|
ch, err := irc.checkRoomId(roomId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -216,16 +172,7 @@ func (irc *IRC) Leave(roomId RoomID) {
|
||||||
irc.conn.Cmd.Part(ch)
|
irc.conn.Cmd.Part(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) SearchForUsers(query string) ([]UserSearchResult, error) {
|
func (irc *IRC) Send(event *Event) error {
|
||||||
// TODO
|
|
||||||
return nil, fmt.Errorf("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (irc *IRC) Send(event *Event) (string, error) {
|
|
||||||
if irc.conn == nil {
|
|
||||||
return "", fmt.Errorf("Not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround girc bug
|
// Workaround girc bug
|
||||||
if event.Text[0] == ':' {
|
if event.Text[0] == ':' {
|
||||||
event.Text = " " + event.Text
|
event.Text = " " + event.Text
|
||||||
|
@ -235,30 +182,22 @@ func (irc *IRC) Send(event *Event) (string, error) {
|
||||||
if event.Room != "" {
|
if event.Room != "" {
|
||||||
ch, err := irc.checkRoomId(event.Room)
|
ch, err := irc.checkRoomId(event.Room)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
dest = ch
|
dest = ch
|
||||||
} else if event.Recipient != "" {
|
} else if event.Recipient != "" {
|
||||||
ui, err := irc.checkUserId(event.Recipient)
|
ui, err := irc.checkUserId(event.Recipient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return err
|
||||||
}
|
}
|
||||||
dest = ui
|
dest = ui
|
||||||
} else {
|
} else {
|
||||||
return "", fmt.Errorf("Invalid target")
|
return fmt.Errorf("Invalid target")
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Attachments != nil && len(event.Attachments) > 0 {
|
if event.Attachements != nil && len(event.Attachements) > 0 {
|
||||||
for _, at := range event.Attachments {
|
|
||||||
url := at.URL()
|
|
||||||
if url == "" {
|
|
||||||
// TODO find a way to send them using some hosing of some kind
|
// TODO find a way to send them using some hosing of some kind
|
||||||
return "", fmt.Errorf("Attachment without URL sent to IRC")
|
return fmt.Errorf("Attachements not supported on IRC")
|
||||||
} else {
|
|
||||||
irc.conn.Cmd.Message(dest, fmt.Sprintf("%s (%s, %dkb)",
|
|
||||||
url, at.Mimetype(), at.Size()/1024))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Type == EVENT_MESSAGE {
|
if event.Type == EVENT_MESSAGE {
|
||||||
|
@ -266,22 +205,15 @@ func (irc *IRC) Send(event *Event) (string, error) {
|
||||||
} else if event.Type == EVENT_ACTION {
|
} else if event.Type == EVENT_ACTION {
|
||||||
irc.conn.Cmd.Action(dest, event.Text)
|
irc.conn.Cmd.Action(dest, event.Text)
|
||||||
} else {
|
} else {
|
||||||
return "", fmt.Errorf("Invalid event type")
|
return fmt.Errorf("Invalid event type")
|
||||||
}
|
}
|
||||||
return "", nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func (irc *IRC) UserCommand(cm string) {
|
|
||||||
irc.handler.SystemMessage("Command not supported.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) Close() {
|
func (irc *IRC) Close() {
|
||||||
conn := irc.conn
|
conn := irc.conn
|
||||||
irc.conn = nil
|
irc.conn = nil
|
||||||
irc.connected = false
|
|
||||||
if conn != nil {
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) connectLoop(c *girc.Client) {
|
func (irc *IRC) connectLoop(c *girc.Client) {
|
||||||
|
@ -292,7 +224,8 @@ func (irc *IRC) connectLoop(c *girc.Client) {
|
||||||
}
|
}
|
||||||
if err := c.Connect(); err != nil {
|
if err := c.Connect(); err != nil {
|
||||||
irc.connected = false
|
irc.connected = false
|
||||||
irc.handler.SystemMessage(fmt.Sprintf("IRC failed to connect / disconnected (%s), reconnecting in %ds", err, irc.timeout))
|
fmt.Printf("IRC failed to connect / disconnected: %s\n", err)
|
||||||
|
fmt.Printf("Retrying in %ds\n", irc.timeout)
|
||||||
time.Sleep(time.Duration(irc.timeout) * time.Second)
|
time.Sleep(time.Duration(irc.timeout) * time.Second)
|
||||||
irc.timeout *= 2
|
irc.timeout *= 2
|
||||||
if irc.timeout > 600 {
|
if irc.timeout > 600 {
|
||||||
|
@ -305,15 +238,9 @@ func (irc *IRC) connectLoop(c *girc.Client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) ircConnected(c *girc.Client, e girc.Event) {
|
func (irc *IRC) ircConnected(c *girc.Client, e girc.Event) {
|
||||||
irc.handler.SystemMessage("Connected to IRC.")
|
fmt.Printf("ircConnected ^^^^\n")
|
||||||
irc.timeout = 10
|
irc.timeout = 10
|
||||||
irc.connected = true
|
irc.connected = true
|
||||||
|
|
||||||
for room, joined := range irc.joinedRooms {
|
|
||||||
if joined {
|
|
||||||
irc.conn.Cmd.Join(room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (irc *IRC) ircPrivmsg(c *girc.Client, e girc.Event) {
|
func (irc *IRC) ircPrivmsg(c *girc.Client, e girc.Event) {
|
||||||
|
@ -335,7 +262,6 @@ func (irc *IRC) ircJoin(c *girc.Client, e girc.Event) {
|
||||||
room := RoomID(e.Params[0] + "@" + irc.server)
|
room := RoomID(e.Params[0] + "@" + irc.server)
|
||||||
if e.Source.Name == irc.nick {
|
if e.Source.Name == irc.nick {
|
||||||
irc.handler.Joined(room)
|
irc.handler.Joined(room)
|
||||||
irc.joinedRooms[e.Params[0]] = true
|
|
||||||
} else {
|
} else {
|
||||||
user := UserID(e.Source.Name + "@" + irc.server)
|
user := UserID(e.Source.Name + "@" + irc.server)
|
||||||
ev := &Event{
|
ev := &Event{
|
||||||
|
@ -354,7 +280,6 @@ func (irc *IRC) ircPart(c *girc.Client, e girc.Event) {
|
||||||
room := RoomID(e.Params[0] + "@" + irc.server)
|
room := RoomID(e.Params[0] + "@" + irc.server)
|
||||||
if e.Source.Name == irc.nick {
|
if e.Source.Name == irc.nick {
|
||||||
irc.handler.Left(room)
|
irc.handler.Left(room)
|
||||||
delete(irc.joinedRooms, e.Params[0])
|
|
||||||
} else {
|
} else {
|
||||||
user := UserID(e.Source.Name + "@" + irc.server)
|
user := UserID(e.Source.Name + "@" + irc.server)
|
||||||
ev := &Event{
|
ev := &Event{
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
package connector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
S_EVENT_JOIN = "join"
|
|
||||||
S_EVENT_LEAVE = "leave"
|
|
||||||
S_EVENT_MESSAGE = "message"
|
|
||||||
S_EVENT_ACTION = "action"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t EventType) MarshalText() ([]byte, error) {
|
|
||||||
switch t {
|
|
||||||
case EVENT_JOIN:
|
|
||||||
return []byte(S_EVENT_JOIN), nil
|
|
||||||
case EVENT_LEAVE:
|
|
||||||
return []byte(S_EVENT_LEAVE), nil
|
|
||||||
case EVENT_MESSAGE:
|
|
||||||
return []byte(S_EVENT_MESSAGE), nil
|
|
||||||
case EVENT_ACTION:
|
|
||||||
return []byte(S_EVENT_ACTION), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Invalid event type: %d", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *EventType) UnmarshalText(text []byte) error {
|
|
||||||
switch string(text) {
|
|
||||||
case S_EVENT_JOIN:
|
|
||||||
*t = EVENT_JOIN
|
|
||||||
return nil
|
|
||||||
case S_EVENT_LEAVE:
|
|
||||||
*t = EVENT_LEAVE
|
|
||||||
return nil
|
|
||||||
case S_EVENT_MESSAGE:
|
|
||||||
*t = EVENT_MESSAGE
|
|
||||||
return nil
|
|
||||||
case S_EVENT_ACTION:
|
|
||||||
*t = EVENT_ACTION
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Invalid event type: %s", string(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type MediaObjectJSON struct {
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Mimetype string `json:"mime_type"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
ImageSize *ImageSize `json:"image_size"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mo SMediaObject) MarshalJSON() ([]byte, error) {
|
|
||||||
if mo.MediaObject == nil {
|
|
||||||
return []byte("null"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mod := MediaObjectJSON{
|
|
||||||
Filename: mo.Filename(),
|
|
||||||
Mimetype: mo.Mimetype(),
|
|
||||||
Size: mo.Size(),
|
|
||||||
ImageSize: mo.ImageSize(),
|
|
||||||
URL: mo.URL(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if mod.URL == "" {
|
|
||||||
// If we don't have a URL, the only way is to pass the blob itself
|
|
||||||
rd, err := mo.Read()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rd.Close()
|
|
||||||
buf := bytes.NewBuffer([]byte{})
|
|
||||||
_, err = io.Copy(buf, rd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mod.Data = base64.StdEncoding.EncodeToString(buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(&mod)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mo *SMediaObject) UnmarshalJSON(jdata []byte) error {
|
|
||||||
if string(jdata) == "null" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var d MediaObjectJSON
|
|
||||||
err := json.Unmarshal(jdata, &d)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.URL != "" {
|
|
||||||
*mo = SMediaObject{&LazyBlobMediaObject{
|
|
||||||
ObjectFilename: d.Filename,
|
|
||||||
ObjectMimetype: d.Mimetype,
|
|
||||||
ObjectImageSize: d.ImageSize,
|
|
||||||
GetFn: func(o *LazyBlobMediaObject) error {
|
|
||||||
resp, err := http.Get(d.URL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if o.ObjectMimetype == "" {
|
|
||||||
o.ObjectMimetype = strings.Join(resp.Header["Content-Type"], "")
|
|
||||||
}
|
|
||||||
o.ObjectData, err = ioutil.ReadAll(resp.Body)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := base64.StdEncoding.DecodeString(d.Data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*mo = SMediaObject{&BlobMediaObject{
|
|
||||||
ObjectFilename: d.Filename,
|
|
||||||
ObjectMimetype: d.Mimetype,
|
|
||||||
ObjectImageSize: d.ImageSize,
|
|
||||||
ObjectData: bytes,
|
|
||||||
}}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package mattermost
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MATTERMOST_PROTOCOL = "Mattermost"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register(MATTERMOST_PROTOCOL, Protocol{
|
|
||||||
NewConnector: func() Connector { return &Mattermost{} },
|
|
||||||
Schema: ConfigSchema{
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "server",
|
|
||||||
Description: "Server",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "username",
|
|
||||||
Description: "Username",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "password",
|
|
||||||
Description: "Password",
|
|
||||||
IsPassword: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "token",
|
|
||||||
Description: "Authentification token (replaces password if set)",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "teams",
|
|
||||||
Description: "Comma-separated list of teams to follow",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "no_tls",
|
|
||||||
Description: "Disable SSL/TLS",
|
|
||||||
IsBoolean: true,
|
|
||||||
Default: "false",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "initial_backlog",
|
|
||||||
Description: "Maximum number of messages to load when joining a channel",
|
|
||||||
IsNumeric: true,
|
|
||||||
Default: "1000",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "initial_members",
|
|
||||||
Description: "Maximum number of members to load when joining a channel",
|
|
||||||
IsNumeric: true,
|
|
||||||
Default: "100",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,676 +0,0 @@
|
||||||
package mattermost
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
_ "os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/matterclient"
|
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User id format: nickname@server
|
|
||||||
// Room id format: room_name@team@server
|
|
||||||
|
|
||||||
type Mattermost struct {
|
|
||||||
handler Handler
|
|
||||||
|
|
||||||
server string
|
|
||||||
username string
|
|
||||||
teams map[string]bool
|
|
||||||
|
|
||||||
initial_members int // How many room members (maximum) to load when first joining a channel
|
|
||||||
initial_backlog int // How many previous messages (maximum) to load when first joining a channel
|
|
||||||
|
|
||||||
conn *matterclient.MMClient
|
|
||||||
handlerStopChan chan struct{}
|
|
||||||
|
|
||||||
caches mmCaches
|
|
||||||
}
|
|
||||||
|
|
||||||
type mmCaches struct {
|
|
||||||
sync.Mutex
|
|
||||||
|
|
||||||
mmusers map[string]string // map mm username to mm user id
|
|
||||||
sentjoined map[string]bool // map username/room name to bool
|
|
||||||
displayname map[UserID]string // map username to last displayname
|
|
||||||
initsynced map[string]bool // chans for which init sync has been done
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) SetHandler(h Handler) {
|
|
||||||
mm.handler = h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Protocol() string {
|
|
||||||
return MATTERMOST_PROTOCOL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Configure(c Configuration) error {
|
|
||||||
if mm.conn != nil {
|
|
||||||
mm.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reinitialize shared data structures
|
|
||||||
mm.handlerStopChan = make(chan struct{})
|
|
||||||
|
|
||||||
mm.caches.mmusers = make(map[string]string)
|
|
||||||
mm.caches.sentjoined = make(map[string]bool)
|
|
||||||
mm.caches.displayname = make(map[UserID]string)
|
|
||||||
mm.caches.initsynced = make(map[string]bool)
|
|
||||||
|
|
||||||
// Read config
|
|
||||||
var err error
|
|
||||||
|
|
||||||
mm.server, err = c.GetString("server")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.username, err = c.GetString("username")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.initial_members, err = c.GetInt("initial_members", 100)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.initial_backlog, err = c.GetInt("initial_backlog", 1000)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
teams, err := c.GetString("teams")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mm.teams = map[string]bool{}
|
|
||||||
anyteam := ""
|
|
||||||
for _, team := range strings.Split(teams, ",") {
|
|
||||||
anyteam = strings.TrimSpace(team)
|
|
||||||
mm.teams[anyteam] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
notls, err := c.GetBool("no_tls", false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
password, _ := c.GetString("password", "")
|
|
||||||
token, _ := c.GetString("token", "")
|
|
||||||
if token != "" {
|
|
||||||
password = "token=" + token
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to log in
|
|
||||||
mm.conn = matterclient.New(mm.username, password, anyteam, mm.server)
|
|
||||||
mm.conn.Credentials.NoTLS = notls
|
|
||||||
err = mm.conn.Login()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to start listening for messages
|
|
||||||
// Everytime the listener reconnects, mm.handleConnected does a sync of room status
|
|
||||||
mm.conn.OnWsConnect = mm.handleConnected
|
|
||||||
go mm.conn.WsReceiver()
|
|
||||||
go mm.conn.StatusLoop()
|
|
||||||
go mm.handleLoop(mm.conn.MessageChan, mm.handlerStopChan)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) User() UserID {
|
|
||||||
return UserID(mm.username + "@" + mm.server)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) getTeamIdByName(name string) string {
|
|
||||||
for _, team := range mm.conn.OtherTeams {
|
|
||||||
if team.Team.Name == name {
|
|
||||||
return team.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) checkRoomId(id RoomID) (string, error) {
|
|
||||||
x := strings.Split(string(id), "@")
|
|
||||||
if len(x) == 1 {
|
|
||||||
return "", fmt.Errorf("Please write whole room ID with team and server: %s@<team>@%s", id, mm.server)
|
|
||||||
}
|
|
||||||
if len(x) == 2 {
|
|
||||||
return x[0], nil
|
|
||||||
}
|
|
||||||
if len(x) != 3 || x[2] != mm.server {
|
|
||||||
return "", fmt.Errorf("Invalid room ID: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
team_id := mm.getTeamIdByName(x[1])
|
|
||||||
if team_id == "" {
|
|
||||||
return "", fmt.Errorf("Team not found: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
ch_id := mm.conn.GetChannelId(x[0], team_id)
|
|
||||||
if ch_id == "" {
|
|
||||||
return "", fmt.Errorf("Channel not found: %s", id)
|
|
||||||
}
|
|
||||||
return ch_id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) reverseRoomId(id string) (bool, RoomID) {
|
|
||||||
team := mm.conn.GetChannelTeamId(id)
|
|
||||||
if team == "" {
|
|
||||||
return true, RoomID(fmt.Sprintf("%s@%s", id, mm.server))
|
|
||||||
} else {
|
|
||||||
teamName := mm.conn.GetTeamName(team)
|
|
||||||
if u, ok := mm.teams[teamName]; ok && u {
|
|
||||||
name := mm.conn.GetChannelName(id)
|
|
||||||
return true, RoomID(fmt.Sprintf("%s@%s@%s", name, teamName, mm.server))
|
|
||||||
} else {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) checkUserId(id UserID) (string, error) {
|
|
||||||
x := strings.Split(string(id), "@")
|
|
||||||
if len(x) == 1 {
|
|
||||||
return "", fmt.Errorf("Please write whole user ID with server: %s@%s", id, mm.server)
|
|
||||||
}
|
|
||||||
if len(x) != 2 || x[1] != mm.server {
|
|
||||||
return "", fmt.Errorf("Invalid user ID: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.caches.Lock()
|
|
||||||
defer mm.caches.Unlock()
|
|
||||||
|
|
||||||
if user_id, ok := mm.caches.mmusers[x[0]]; ok {
|
|
||||||
return user_id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u, resp := mm.conn.Client.GetUserByUsername(x[0], "")
|
|
||||||
if u == nil || resp.Error != nil {
|
|
||||||
return "", fmt.Errorf("Not found: %s (%s)", x[0], resp.Error)
|
|
||||||
}
|
|
||||||
mm.caches.mmusers[x[0]] = u.Id
|
|
||||||
|
|
||||||
return u.Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) SetUserInfo(info *UserInfo) error {
|
|
||||||
return fmt.Errorf("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
|
|
||||||
ch, err := mm.checkRoomId(roomId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Topic != "" {
|
|
||||||
mm.conn.UpdateChannelHeader(ch, info.Topic)
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Picture.MediaObject != nil {
|
|
||||||
err = fmt.Errorf("Not supported: channel picture on mattermost")
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Name != "" {
|
|
||||||
err = fmt.Errorf("Not supported: channel name on mattermost")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Join(roomId RoomID) error {
|
|
||||||
ch, err := mm.checkRoomId(roomId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mm.conn.JoinChannel(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Invite(userId UserID, roomId RoomID) error {
|
|
||||||
if roomId == "" {
|
|
||||||
_, err := mm.checkUserId(userId)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Not supported: invite on mattermost")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Leave(roomId RoomID) {
|
|
||||||
// Not supported? TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) SearchForUsers(query string) ([]UserSearchResult, error) {
|
|
||||||
query = strings.ToLower(query)
|
|
||||||
ret := []UserSearchResult{}
|
|
||||||
for _, user := range mm.conn.Users {
|
|
||||||
if strings.Contains(strings.ToLower(user.Username), query) ||
|
|
||||||
strings.Contains(strings.ToLower(user.Nickname), query) ||
|
|
||||||
strings.Contains(strings.ToLower(user.GetDisplayName(model.SHOW_NICKNAME_FULLNAME)), query) {
|
|
||||||
ret = append(ret, UserSearchResult{
|
|
||||||
ID: UserID(fmt.Sprintf("%s@%s", user.Username, mm.server)),
|
|
||||||
DisplayName: user.GetDisplayName(model.SHOW_NICKNAME_FULLNAME),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Send(event *Event) (string, error) {
|
|
||||||
post := &model.Post{
|
|
||||||
Message: event.Text,
|
|
||||||
}
|
|
||||||
if event.Type == EVENT_ACTION {
|
|
||||||
post.Type = "me"
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Room != "" {
|
|
||||||
ch, err := mm.checkRoomId(event.Room)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
post.ChannelId = ch
|
|
||||||
} else if event.Recipient != "" {
|
|
||||||
ui, err := mm.checkUserId(event.Recipient)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, resp := mm.conn.Client.CreateDirectChannel(mm.conn.User.Id, ui)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return "", resp.Error
|
|
||||||
}
|
|
||||||
channelName := model.GetDMNameFromIds(ui, mm.conn.User.Id)
|
|
||||||
|
|
||||||
err = mm.conn.UpdateChannels()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
post.ChannelId = mm.conn.GetChannelId(channelName, "")
|
|
||||||
} else {
|
|
||||||
return "", fmt.Errorf("Invalid target")
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Attachments != nil {
|
|
||||||
post.FileIds = []string{}
|
|
||||||
for _, file := range event.Attachments {
|
|
||||||
rdr, err := file.Read()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer rdr.Close()
|
|
||||||
data, err := ioutil.ReadAll(rdr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
up_file, err := mm.conn.UploadFile(data, post.ChannelId, file.Filename())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("UploadFile error: %s", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
post.FileIds = append(post.FileIds, up_file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
created_post, resp := mm.conn.Client.CreatePost(post)
|
|
||||||
if resp.Error != nil {
|
|
||||||
log.Warnf("CreatePost error: %s", resp.Error)
|
|
||||||
return "", resp.Error
|
|
||||||
}
|
|
||||||
return created_post.Id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) UserCommand(cm string) {
|
|
||||||
mm.handler.SystemMessage("Command not supported.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) Close() {
|
|
||||||
if mm.conn != nil {
|
|
||||||
mm.conn.WsQuit = true
|
|
||||||
}
|
|
||||||
if mm.handlerStopChan != nil {
|
|
||||||
close(mm.handlerStopChan)
|
|
||||||
mm.handlerStopChan = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) handleConnected() {
|
|
||||||
mm.handler.SystemMessage(fmt.Sprintf("(Re-)connected to mattermost (%s@%s), doing channel sync", mm.username, mm.server))
|
|
||||||
|
|
||||||
// Initial channel sync
|
|
||||||
chans := mm.conn.GetChannels()
|
|
||||||
doneCh := make(map[string]bool)
|
|
||||||
for _, ch := range chans {
|
|
||||||
if _, ok := doneCh[ch.Id]; !ok {
|
|
||||||
doneCh[ch.Id] = true
|
|
||||||
go mm.syncChannel(ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) syncChannel(ch *model.Channel) {
|
|
||||||
// The first time we see a chan, sync everything (member list, names, profile pictures)
|
|
||||||
must_initsync := func() bool {
|
|
||||||
mm.caches.Lock()
|
|
||||||
defer mm.caches.Unlock()
|
|
||||||
|
|
||||||
if x, ok := mm.caches.initsynced[ch.Id]; ok && x {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mm.caches.initsynced[ch.Id] = true
|
|
||||||
return true
|
|
||||||
}()
|
|
||||||
|
|
||||||
if must_initsync {
|
|
||||||
mm.initSyncChannel(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following times, only sync missing messages
|
|
||||||
mm.backlogChannel(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) initSyncChannel(ch *model.Channel) {
|
|
||||||
if len(strings.Split(ch.Name, "__")) == 2 {
|
|
||||||
// DM channel
|
|
||||||
// Update remote user info
|
|
||||||
users := strings.Split(ch.Name, "__")
|
|
||||||
for _, uid := range users {
|
|
||||||
if uid != mm.conn.User.Id {
|
|
||||||
user := mm.conn.GetUser(uid)
|
|
||||||
if user != nil {
|
|
||||||
mm.updateUserInfo(user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
interested, id := mm.reverseRoomId(ch.Id)
|
|
||||||
if !interested {
|
|
||||||
// Skip channels that are not in teams we want to bridge
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mm.handler.Joined(id)
|
|
||||||
|
|
||||||
// Update room info
|
|
||||||
room_info := &RoomInfo{
|
|
||||||
Name: ch.DisplayName,
|
|
||||||
Topic: ch.Header,
|
|
||||||
}
|
|
||||||
for _, t := range mm.conn.OtherTeams {
|
|
||||||
if t.Id == ch.TeamId {
|
|
||||||
if t.Team.DisplayName != "" {
|
|
||||||
room_info.Name = t.Team.DisplayName + " / " + room_info.Name
|
|
||||||
} else {
|
|
||||||
room_info.Name = t.Team.Name + " / " + room_info.Name
|
|
||||||
}
|
|
||||||
if t.Team.LastTeamIconUpdate > 0 {
|
|
||||||
room_info.Picture = SMediaObject{&LazyBlobMediaObject{
|
|
||||||
ObjectFilename: fmt.Sprintf("%s-%d",
|
|
||||||
t.Team.Name,
|
|
||||||
t.Team.LastTeamIconUpdate),
|
|
||||||
GetFn: func(o *LazyBlobMediaObject) error {
|
|
||||||
team_img, resp := mm.conn.Client.GetTeamIcon(t.Id, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
log.Warnf("Could not get team image: %s", resp.Error.Error())
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
o.ObjectData = team_img
|
|
||||||
o.ObjectMimetype = http.DetectContentType(team_img)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mm.handler.RoomInfoUpdated(id, UserID(""), room_info)
|
|
||||||
|
|
||||||
// Update member list
|
|
||||||
// TODO (when this will be slow, i.e. hundreds of members): do only a diff
|
|
||||||
members, resp := mm.conn.Client.GetChannelMembers(ch.Id, 0, mm.initial_members, "")
|
|
||||||
if resp.Error == nil {
|
|
||||||
for _, mem := range *members {
|
|
||||||
if mem.UserId == mm.conn.User.Id {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
user := mm.conn.GetUser(mem.UserId)
|
|
||||||
if user != nil {
|
|
||||||
mm.ensureJoined(user, id)
|
|
||||||
mm.updateUserInfo(user)
|
|
||||||
} else {
|
|
||||||
log.Warnf("Could not find joined user: %s", mem.UserId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Warnf("Could not get channel members: %s", resp.Error.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) backlogChannel(ch *model.Channel) {
|
|
||||||
// Read backlog
|
|
||||||
last_seen_post := mm.handler.CacheGet(fmt.Sprintf("last_seen_%s", ch.Id))
|
|
||||||
if last_seen_post != "" {
|
|
||||||
const NUM_PER_PAGE = 100
|
|
||||||
page := 0
|
|
||||||
backlogs := []*model.PostList{}
|
|
||||||
for {
|
|
||||||
backlog, resp := mm.conn.Client.GetPostsAfter(ch.Id, last_seen_post, page, NUM_PER_PAGE, "")
|
|
||||||
if resp.Error == nil {
|
|
||||||
backlogs = append(backlogs, backlog)
|
|
||||||
if len(backlog.Order) == NUM_PER_PAGE {
|
|
||||||
page += 1
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := 0; i < len(backlogs); i++ {
|
|
||||||
mm.processBacklog(ch, backlogs[i])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backlog, resp := mm.conn.Client.GetPostsForChannel(ch.Id, 0, mm.initial_backlog, "")
|
|
||||||
if resp.Error == nil {
|
|
||||||
mm.processBacklog(ch, backlog)
|
|
||||||
} else {
|
|
||||||
log.Warnf("Could not get channel backlog: %s", resp.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) processBacklog(ch *model.Channel, backlog *model.PostList) {
|
|
||||||
for i := 0; i < len(backlog.Order); i++ {
|
|
||||||
post_id := backlog.Order[len(backlog.Order)-i-1]
|
|
||||||
post := backlog.Posts[post_id]
|
|
||||||
post_time := time.Unix(post.CreateAt/1000, 0)
|
|
||||||
post.Message = fmt.Sprintf("[%s] %s",
|
|
||||||
post_time.Format("2006-01-02 15:04 MST"), post.Message)
|
|
||||||
mm.handlePost(ch.Name, post, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) handleLoop(msgCh chan *matterclient.Message, quitCh chan struct{}) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quitCh:
|
|
||||||
break
|
|
||||||
case msg := <-msgCh:
|
|
||||||
log.Tracef("Mattermost: %#v\n", msg)
|
|
||||||
log.Tracef("Mattermost raw: %#v\n", msg.Raw)
|
|
||||||
err := mm.handlePosted(msg.Raw)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Mattermost error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) updateUserInfo(user *model.User) {
|
|
||||||
userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
|
|
||||||
userDisp := user.GetDisplayName(model.SHOW_NICKNAME_FULLNAME)
|
|
||||||
|
|
||||||
mm.caches.Lock()
|
|
||||||
defer mm.caches.Unlock()
|
|
||||||
|
|
||||||
if lastdn, ok := mm.caches.displayname[userId]; !ok || lastdn != userDisp {
|
|
||||||
ui := &UserInfo{
|
|
||||||
DisplayName: userDisp,
|
|
||||||
}
|
|
||||||
if user.LastPictureUpdate > 0 {
|
|
||||||
ui.Avatar = SMediaObject{&LazyBlobMediaObject{
|
|
||||||
ObjectFilename: fmt.Sprintf("%s-%d",
|
|
||||||
user.Username,
|
|
||||||
user.LastPictureUpdate),
|
|
||||||
GetFn: func(o *LazyBlobMediaObject) error {
|
|
||||||
img, resp := mm.conn.Client.GetProfileImage(user.Id, "")
|
|
||||||
if resp.Error != nil {
|
|
||||||
log.Warnf("Could not get profile picture: %s", resp.Error.Error())
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
o.ObjectData = img
|
|
||||||
o.ObjectMimetype = http.DetectContentType(img)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
mm.handler.UserInfoUpdated(userId, ui)
|
|
||||||
mm.caches.displayname[userId] = userDisp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) ensureJoined(user *model.User, roomId RoomID) {
|
|
||||||
userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
|
|
||||||
cache_key := fmt.Sprintf("%s / %s", userId, roomId)
|
|
||||||
|
|
||||||
mm.caches.Lock()
|
|
||||||
defer mm.caches.Unlock()
|
|
||||||
|
|
||||||
if _, ok := mm.caches.sentjoined[cache_key]; !ok {
|
|
||||||
mm.handler.Event(&Event{
|
|
||||||
Author: userId,
|
|
||||||
Room: roomId,
|
|
||||||
Type: EVENT_JOIN,
|
|
||||||
})
|
|
||||||
mm.caches.sentjoined[cache_key] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) handlePosted(msg *model.WebSocketEvent) error {
|
|
||||||
channel_name, ok := msg.Data["channel_name"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
post_str := msg.Data["post"].(string)
|
|
||||||
var post model.Post
|
|
||||||
err := json.Unmarshal([]byte(post_str), &post)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mm.handlePost(channel_name, &post, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *Mattermost) handlePost(channel_name string, post *model.Post, only_messages bool) error {
|
|
||||||
// Skip self messages
|
|
||||||
if post.UserId == mm.conn.User.Id {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find sending user
|
|
||||||
user := mm.conn.GetUser(post.UserId)
|
|
||||||
if user == nil {
|
|
||||||
return fmt.Errorf("Invalid user")
|
|
||||||
}
|
|
||||||
userId := UserID(fmt.Sprintf("%s@%s", user.Username, mm.server))
|
|
||||||
mm.updateUserInfo(user)
|
|
||||||
|
|
||||||
// Build message event
|
|
||||||
msg_ev := &Event{
|
|
||||||
Id: post.Id,
|
|
||||||
Author: userId,
|
|
||||||
Text: post.Message,
|
|
||||||
Type: EVENT_MESSAGE,
|
|
||||||
}
|
|
||||||
if post.Type == "me" {
|
|
||||||
msg_ev.Type = EVENT_ACTION
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle files
|
|
||||||
if post.Metadata != nil && post.Metadata.Files != nil {
|
|
||||||
msg_ev.Attachments = []SMediaObject{}
|
|
||||||
for _, file := range post.Metadata.Files {
|
|
||||||
media_object := &LazyBlobMediaObject{
|
|
||||||
ObjectFilename: file.Name,
|
|
||||||
ObjectMimetype: file.MimeType,
|
|
||||||
GetFn: func(o *LazyBlobMediaObject) error {
|
|
||||||
blob, resp := mm.conn.Client.GetFile(file.Id)
|
|
||||||
if resp.Error != nil {
|
|
||||||
return resp.Error
|
|
||||||
}
|
|
||||||
o.ObjectData = blob
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if file.Width > 0 {
|
|
||||||
media_object.ObjectImageSize = &ImageSize{
|
|
||||||
Width: file.Width,
|
|
||||||
Height: file.Height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg_ev.Attachments = append(msg_ev.Attachments, SMediaObject{media_object})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch as PM or as room message
|
|
||||||
if len(strings.Split(channel_name, "__")) == 2 {
|
|
||||||
// Private message, no need to find room id
|
|
||||||
if user.Id == mm.conn.User.Id {
|
|
||||||
// Skip self sent messages
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.handler.Event(msg_ev)
|
|
||||||
mm.handler.CachePut(fmt.Sprintf("last_seen_%s", post.ChannelId), post.Id)
|
|
||||||
} else {
|
|
||||||
interested, roomId := mm.reverseRoomId(post.ChannelId)
|
|
||||||
if !interested {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if roomId == "" {
|
|
||||||
return fmt.Errorf("Invalid channel id")
|
|
||||||
}
|
|
||||||
|
|
||||||
mm.ensureJoined(user, roomId)
|
|
||||||
|
|
||||||
if post.Type == "system_header_change" {
|
|
||||||
if !only_messages {
|
|
||||||
new_header := post.Props["new_header"].(string)
|
|
||||||
mm.handler.RoomInfoUpdated(roomId, userId, &RoomInfo{
|
|
||||||
Topic: new_header,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if post.Type == "" || post.Type == "me" {
|
|
||||||
msg_ev.Room = roomId
|
|
||||||
mm.handler.Event(msg_ev)
|
|
||||||
mm.handler.CachePut(fmt.Sprintf("last_seen_%s", post.ChannelId), post.Id)
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Unhandled post type: %s", post.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,147 +0,0 @@
|
||||||
package connector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileMediaObject struct {
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileMediaObject) Filename() string {
|
|
||||||
return filepath.Base(m.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileMediaObject) Size() int64 {
|
|
||||||
fi, err := os.Stat(m.Path)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return fi.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileMediaObject) Mimetype() string {
|
|
||||||
f, err := os.Open(m.Path)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
buffer := make([]byte, 512)
|
|
||||||
_, err = f.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.DetectContentType(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileMediaObject) ImageSize() *ImageSize {
|
|
||||||
// TODO but not really usefull
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileMediaObject) Read() (io.ReadCloser, error) {
|
|
||||||
return os.Open(m.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *FileMediaObject) URL() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type BlobMediaObject struct {
|
|
||||||
ObjectFilename string
|
|
||||||
ObjectMimetype string
|
|
||||||
ObjectImageSize *ImageSize
|
|
||||||
ObjectData []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *BlobMediaObject) Filename() string {
|
|
||||||
return m.ObjectFilename
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *BlobMediaObject) Size() int64 {
|
|
||||||
return int64(len(m.ObjectData))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *BlobMediaObject) Mimetype() string {
|
|
||||||
return m.ObjectMimetype
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *BlobMediaObject) ImageSize() *ImageSize {
|
|
||||||
return m.ObjectImageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *BlobMediaObject) Read() (io.ReadCloser, error) {
|
|
||||||
return nullCloseReader{bytes.NewBuffer(m.ObjectData)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *BlobMediaObject) URL() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type nullCloseReader struct {
|
|
||||||
io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ncr nullCloseReader) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type LazyBlobMediaObject struct {
|
|
||||||
ObjectFilename string
|
|
||||||
ObjectMimetype string
|
|
||||||
ObjectImageSize *ImageSize
|
|
||||||
ObjectData []byte
|
|
||||||
|
|
||||||
GetFn func(o *LazyBlobMediaObject) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LazyBlobMediaObject) Filename() string {
|
|
||||||
return m.ObjectFilename
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LazyBlobMediaObject) Size() int64 {
|
|
||||||
if m.ObjectData == nil {
|
|
||||||
m.GetFn(m)
|
|
||||||
}
|
|
||||||
return int64(len(m.ObjectData))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LazyBlobMediaObject) Mimetype() string {
|
|
||||||
if m.ObjectData == nil {
|
|
||||||
m.GetFn(m)
|
|
||||||
}
|
|
||||||
return m.ObjectMimetype
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LazyBlobMediaObject) ImageSize() *ImageSize {
|
|
||||||
if m.ObjectData == nil {
|
|
||||||
m.GetFn(m)
|
|
||||||
}
|
|
||||||
return m.ObjectImageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LazyBlobMediaObject) Read() (io.ReadCloser, error) {
|
|
||||||
if m.ObjectData == nil {
|
|
||||||
err := m.GetFn(m)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullCloseReader{bytes.NewBuffer(m.ObjectData)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *LazyBlobMediaObject) URL() string {
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const XMPP_PROTOCOL = "XMPP"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register(XMPP_PROTOCOL, Protocol{
|
|
||||||
NewConnector: func() Connector { return &XMPP{} },
|
|
||||||
Schema: ConfigSchema{
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "jid",
|
|
||||||
Description: "JID",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "password",
|
|
||||||
Description: "Password",
|
|
||||||
Required: true,
|
|
||||||
IsPassword: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "nickname",
|
|
||||||
Description: "Nickname in MUCs",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "port",
|
|
||||||
Description: "Port",
|
|
||||||
IsNumeric: true,
|
|
||||||
Default: "5222",
|
|
||||||
},
|
|
||||||
&ConfigEntry{
|
|
||||||
Name: "ssl",
|
|
||||||
Description: "Use SSL",
|
|
||||||
IsBoolean: true,
|
|
||||||
Default: "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
51
connector/xmpp/iq.go
Normal file
51
connector/xmpp/iq.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscoQuery struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/disco#items query"`
|
||||||
|
Items []Node `xml:"item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
XMLName xml.Name `xml:"item"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PubSub struct {
|
||||||
|
XMLName xml.Name `xml:"http://jabber.org/protocol/pubsub pubsub"`
|
||||||
|
Subscribe []Subscribe `xml:"subscribe"`
|
||||||
|
Subscription []Subscription `xml:"subscription"`
|
||||||
|
Subscriptions []Subscription `xml:"subscriptions"`
|
||||||
|
Publish *Publish `xml:"publish"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscribe struct {
|
||||||
|
XMLName xml.Name `xml:"subscribe"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
XMLName xml.Name `xml:"subscription"`
|
||||||
|
Jid string `xml:"jid,attr"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
SubID string `xml:"subid,attr"`
|
||||||
|
Subscription string `xml:"subscription,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Publish struct {
|
||||||
|
XMLName xml.Name `xml:"publish"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
Item []Item `xml:"item"`
|
||||||
|
Items []Item `xml:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
XMLName xml.Name `xml:"publish"`
|
||||||
|
Id string `xml:"id,attr"`
|
||||||
|
Data string `xml:",innerxml"`
|
||||||
|
}
|
|
@ -1,15 +1,16 @@
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
//"os"
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
gxmpp "github.com/matterbridge/go-xmpp"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
//gxmpp "github.com/mattn/go-xmpp"
|
||||||
|
gxmpp "git.deuxfleurs.fr/lx/go-xmpp"
|
||||||
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
)
|
)
|
||||||
|
@ -36,22 +37,15 @@ type XMPP struct {
|
||||||
|
|
||||||
conn *gxmpp.Client
|
conn *gxmpp.Client
|
||||||
|
|
||||||
stateLock sync.Mutex
|
isMUC map[string]bool
|
||||||
muc map[RoomID]*mucInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
type mucInfo struct {
|
|
||||||
joined bool
|
|
||||||
pendingJoins map[UserID]string
|
|
||||||
pendingLeaves map[UserID]struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) SetHandler(h Handler) {
|
func (xm *XMPP) SetHandler(h Handler) {
|
||||||
xm.handler = h
|
xm.handler = h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) Protocol() string {
|
func(xm *XMPP) Protocol() string {
|
||||||
return XMPP_PROTOCOL
|
return "xmpp"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) Configure(c Configuration) error {
|
func (xm *XMPP) Configure(c Configuration) error {
|
||||||
|
@ -62,6 +56,11 @@ func (xm *XMPP) Configure(c Configuration) error {
|
||||||
// Parse and validate configuration
|
// Parse and validate configuration
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
xm.server, err = c.GetString("server")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
xm.port, err = c.GetInt("port", 5222)
|
xm.port, err = c.GetInt("port", 5222)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -80,10 +79,11 @@ func (xm *XMPP) Configure(c Configuration) error {
|
||||||
if len(jid_parts) != 2 {
|
if len(jid_parts) != 2 {
|
||||||
return fmt.Errorf("Invalid JID: %s", xm.jid)
|
return fmt.Errorf("Invalid JID: %s", xm.jid)
|
||||||
}
|
}
|
||||||
xm.server = jid_parts[1]
|
if jid_parts[1] != xm.server {
|
||||||
|
return fmt.Errorf("JID %s not on server %s", xm.jid, xm.server)
|
||||||
|
}
|
||||||
xm.jid_localpart = jid_parts[0]
|
xm.jid_localpart = jid_parts[0]
|
||||||
|
xm.nickname = xm.jid_localpart
|
||||||
xm.nickname, _ = c.GetString("nickname", xm.jid_localpart)
|
|
||||||
|
|
||||||
xm.password, err = c.GetString("password")
|
xm.password, err = c.GetString("password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,13 +91,15 @@ func (xm *XMPP) Configure(c Configuration) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to connect
|
// Try to connect
|
||||||
xm.muc = make(map[RoomID]*mucInfo)
|
if xm.isMUC == nil {
|
||||||
|
xm.isMUC = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
xm.connectorLoopNum += 1
|
xm.connectorLoopNum += 1
|
||||||
go xm.connectLoop(xm.connectorLoopNum)
|
go xm.connectLoop(xm.connectorLoopNum)
|
||||||
|
|
||||||
for i := 0; i < 42; i++ {
|
for i := 0; i < 42; i++ {
|
||||||
time.Sleep(time.Duration(1) * time.Second)
|
time.Sleep(time.Duration(1)*time.Second)
|
||||||
if xm.connected {
|
if xm.connected {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -112,7 +114,7 @@ func (xm *XMPP) connectLoop(num int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc := &tls.Config{
|
tc := &tls.Config{
|
||||||
ServerName: xm.server,
|
ServerName: strings.Split(xm.jid, "@")[1],
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
}
|
}
|
||||||
options := gxmpp.Options{
|
options := gxmpp.Options{
|
||||||
|
@ -128,7 +130,8 @@ func (xm *XMPP) connectLoop(num int) {
|
||||||
xm.conn, err = options.NewClient()
|
xm.conn, err = options.NewClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xm.connected = false
|
xm.connected = false
|
||||||
xm.handler.SystemMessage(fmt.Sprintf("XMPP failed to connect (%s). Retrying in %ds", err, xm.timeout))
|
fmt.Printf("XMPP failed to connect / disconnected: %s\n", err)
|
||||||
|
fmt.Printf("Retrying in %ds\n", xm.timeout)
|
||||||
time.Sleep(time.Duration(xm.timeout) * time.Second)
|
time.Sleep(time.Duration(xm.timeout) * time.Second)
|
||||||
xm.timeout *= 2
|
xm.timeout *= 2
|
||||||
if xm.timeout > 600 {
|
if xm.timeout > 600 {
|
||||||
|
@ -137,22 +140,12 @@ func (xm *XMPP) connectLoop(num int) {
|
||||||
} else {
|
} else {
|
||||||
xm.connected = true
|
xm.connected = true
|
||||||
xm.timeout = 10
|
xm.timeout = 10
|
||||||
|
|
||||||
for muc, mucInfo := range xm.muc {
|
|
||||||
if mucInfo.joined {
|
|
||||||
_, err := xm.conn.JoinMUCNoHistory(string(muc), xm.nickname)
|
|
||||||
if err != nil {
|
|
||||||
xm.handler.SystemMessage(fmt.Sprintf("Could not rejoin MUC %s after reconnection: %s", muc, err))
|
|
||||||
xm.handler.Left(RoomID(muc))
|
|
||||||
mucInfo.joined = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = xm.handleXMPP()
|
err = xm.handleXMPP()
|
||||||
|
if err != nil {
|
||||||
xm.connected = false
|
xm.connected = false
|
||||||
xm.handler.SystemMessage(fmt.Sprintf("XMPP disconnected (%s), reconnecting)", err))
|
fmt.Printf("XMPP disconnected: %s\n", err)
|
||||||
|
fmt.Printf("Reconnecting.\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,7 +159,7 @@ func (xm *XMPP) xmppKeepAlive() chan bool {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if err := xm.conn.PingC2S("", ""); err != nil {
|
if err := xm.conn.PingC2S("", ""); err != nil {
|
||||||
log.Debugf("PING failed %#v\n", err)
|
log.Printf("PING failed %#v\n", err)
|
||||||
}
|
}
|
||||||
case <-done:
|
case <-done:
|
||||||
return
|
return
|
||||||
|
@ -186,23 +179,15 @@ func (xm *XMPP) handleXMPP() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("XMPP: %#v\n", m)
|
|
||||||
|
|
||||||
xm.handleXMPPStance(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xm *XMPP) handleXMPPStance(m interface{}) {
|
|
||||||
xm.stateLock.Lock()
|
|
||||||
defer xm.stateLock.Unlock()
|
|
||||||
|
|
||||||
switch v := m.(type) {
|
switch v := m.(type) {
|
||||||
case gxmpp.Chat:
|
case gxmpp.Chat:
|
||||||
|
fmt.Printf("XMPP chat: %#v\n", v)
|
||||||
remote_sp := strings.Split(v.Remote, "/")
|
remote_sp := strings.Split(v.Remote, "/")
|
||||||
|
|
||||||
// Skip self-sent events
|
// Skip self-sent events
|
||||||
if v.Remote == xm.jid || (v.Type == "groupchat" && len(remote_sp) == 2 && remote_sp[1] == xm.nickname) {
|
if v.Remote == xm.jid || (v.Type == "groupchat" && len(remote_sp) == 2 && remote_sp[1] == xm.nickname) {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If empty text, make sure we joined the room
|
// If empty text, make sure we joined the room
|
||||||
|
@ -215,12 +200,8 @@ func (xm *XMPP) handleXMPPStance(m interface{}) {
|
||||||
if v.Subject != "" && v.Type == "groupchat" {
|
if v.Subject != "" && v.Type == "groupchat" {
|
||||||
author := UserID("")
|
author := UserID("")
|
||||||
if len(remote_sp) == 2 {
|
if len(remote_sp) == 2 {
|
||||||
if remote_sp[1] == xm.nickname {
|
|
||||||
author = xm.User()
|
|
||||||
} else {
|
|
||||||
author = UserID(remote_sp[1] + "@" + remote_sp[0])
|
author = UserID(remote_sp[1] + "@" + remote_sp[0])
|
||||||
}
|
}
|
||||||
}
|
|
||||||
xm.handler.RoomInfoUpdated(RoomID(remote_sp[0]), author, &RoomInfo{
|
xm.handler.RoomInfoUpdated(RoomID(remote_sp[0]), author, &RoomInfo{
|
||||||
Topic: v.Subject,
|
Topic: v.Subject,
|
||||||
})
|
})
|
||||||
|
@ -243,66 +224,89 @@ func (xm *XMPP) handleXMPPStance(m interface{}) {
|
||||||
xm.handler.Event(event)
|
xm.handler.Event(event)
|
||||||
}
|
}
|
||||||
if v.Type == "groupchat" && len(remote_sp) == 2 {
|
if v.Type == "groupchat" && len(remote_sp) == 2 {
|
||||||
// First flush pending leaves and joins
|
event.Room = RoomID(remote_sp[0])
|
||||||
room_id := RoomID(remote_sp[0])
|
|
||||||
if muc, ok := xm.muc[room_id]; ok {
|
|
||||||
muc.flushLeavesJoins(room_id, xm.handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now send event
|
|
||||||
event.Room = room_id
|
|
||||||
event.Author = UserID(remote_sp[1] + "@" + remote_sp[0])
|
event.Author = UserID(remote_sp[1] + "@" + remote_sp[0])
|
||||||
event.Id = v.ID
|
|
||||||
xm.handler.Event(event)
|
xm.handler.Event(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case gxmpp.Presence:
|
case gxmpp.Presence:
|
||||||
|
fmt.Printf("XMPP presence: %#v\n", v)
|
||||||
|
|
||||||
remote := strings.Split(v.From, "/")
|
remote := strings.Split(v.From, "/")
|
||||||
room := RoomID(remote[0])
|
if ismuc, ok := xm.isMUC[remote[0]]; ok && ismuc {
|
||||||
if mucInfo, ok := xm.muc[room]; ok {
|
|
||||||
// skip presence with no user and self-presence
|
// skip presence with no user and self-presence
|
||||||
if len(remote) < 2 || remote[1] == xm.nickname {
|
if len(remote) < 2 || remote[1] == xm.nickname {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
user := UserID(remote[1] + "@" + remote[0])
|
user := UserID(remote[1] + "@" + remote[0])
|
||||||
if v.Type != "unavailable" {
|
event := &Event{
|
||||||
if _, ok := mucInfo.pendingLeaves[user]; ok {
|
|
||||||
delete(mucInfo.pendingLeaves, user)
|
|
||||||
} else {
|
|
||||||
mucInfo.pendingJoins[user] = remote[1]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, ok := mucInfo.pendingJoins[user]; ok {
|
|
||||||
delete(mucInfo.pendingJoins, user)
|
|
||||||
} else {
|
|
||||||
mucInfo.pendingLeaves[user] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muc *mucInfo) flushLeavesJoins(room RoomID, handler Handler) {
|
|
||||||
for user, display_name := range muc.pendingJoins {
|
|
||||||
handler.Event(&Event{
|
|
||||||
Type: EVENT_JOIN,
|
Type: EVENT_JOIN,
|
||||||
Room: room,
|
Room: RoomID(remote[0]),
|
||||||
Author: user,
|
Author: user,
|
||||||
|
}
|
||||||
|
if v.Type == "unavailable" {
|
||||||
|
event.Type = EVENT_LEAVE
|
||||||
|
}
|
||||||
|
xm.handler.Event(event)
|
||||||
|
xm.handler.UserInfoUpdated(user, &UserInfo{
|
||||||
|
DisplayName: remote[1],
|
||||||
})
|
})
|
||||||
handler.UserInfoUpdated(user, &UserInfo{
|
} else {
|
||||||
DisplayName: display_name,
|
// Send discovery query
|
||||||
|
iq, err := xml.Marshal(&DiscoQuery{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("XML marshall error: %s\n", err)
|
||||||
|
} else {
|
||||||
|
xm.conn.SendIQ(gxmpp.IQ{
|
||||||
|
Type: "get",
|
||||||
|
To: remote[0],
|
||||||
|
ID: "items1",
|
||||||
|
Query: iq,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for user, _ := range muc.pendingLeaves {
|
}
|
||||||
handler.Event(&Event{
|
case gxmpp.IQ:
|
||||||
Type: EVENT_LEAVE,
|
fmt.Printf("XMPP iq: from=%s to=%s id=%s type=%s\n", v.From, v.To, v.ID, v.Type)
|
||||||
Room: room,
|
if len(v.Query) > 0 {
|
||||||
Author: user,
|
fmt.Printf("Query data: %s\n", string(v.Query))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Type == "result" && v.ID == "items1" {
|
||||||
|
var q DiscoQuery
|
||||||
|
err := xml.Unmarshal(v.Query, &q)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("XML unmarshall error: %s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range q.Items {
|
||||||
|
if item.Node == "urn:xmpp:avatar:metadata" || item.Node == "urn:xmpp:avatar:data" {
|
||||||
|
sub := &PubSub{
|
||||||
|
Subscribe: []Subscribe{
|
||||||
|
Subscribe{
|
||||||
|
Jid: xm.jid,
|
||||||
|
Node: item.Node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
iq, err := xml.Marshal(sub)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("XML marshall error: %s\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("IQ AVATAR SUB: %s\n", iq)
|
||||||
|
xm.conn.SendIQ(gxmpp.IQ{
|
||||||
|
To: v.From,
|
||||||
|
Type: "set",
|
||||||
|
ID: "sub1",
|
||||||
|
Query: iq,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
muc.pendingJoins = make(map[UserID]string)
|
}
|
||||||
muc.pendingLeaves = make(map[UserID]struct{})
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) User() UserID {
|
func (xm *XMPP) User() UserID {
|
||||||
|
@ -315,17 +319,14 @@ func (xm *XMPP) SetUserInfo(info *UserInfo) error {
|
||||||
|
|
||||||
func (xm *XMPP) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
|
func (xm *XMPP) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
|
||||||
if info.Topic != "" {
|
if info.Topic != "" {
|
||||||
_, err := xm.conn.Send(gxmpp.Chat{
|
xm.conn.Send(gxmpp.Chat{
|
||||||
Type: "groupchat",
|
Type: "groupchat",
|
||||||
Remote: string(roomId),
|
Remote: string(roomId),
|
||||||
Subject: info.Topic,
|
Subject: info.Topic,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.Picture.MediaObject != nil {
|
if info.Picture != nil {
|
||||||
// TODO
|
// TODO
|
||||||
return fmt.Errorf("Room picture change not implemented on xmpp")
|
return fmt.Errorf("Room picture change not implemented on xmpp")
|
||||||
}
|
}
|
||||||
|
@ -338,107 +339,46 @@ func (xm *XMPP) SetRoomInfo(roomId RoomID, info *RoomInfo) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) Join(roomId RoomID) error {
|
func (xm *XMPP) Join(roomId RoomID) error {
|
||||||
xm.stateLock.Lock()
|
xm.isMUC[string(roomId)] = true
|
||||||
defer xm.stateLock.Unlock()
|
|
||||||
|
|
||||||
xm.muc[roomId] = &mucInfo{
|
fmt.Printf("Join %s with nick %s\n", roomId, xm.nickname)
|
||||||
pendingJoins: make(map[UserID]string),
|
|
||||||
pendingLeaves: make(map[UserID]struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("Join %s with nick %s\n", roomId, xm.nickname)
|
|
||||||
_, err := xm.conn.JoinMUCNoHistory(string(roomId), xm.nickname)
|
_, err := xm.conn.JoinMUCNoHistory(string(roomId), xm.nickname)
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
xm.muc[roomId].joined = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) Invite(userId UserID, roomId RoomID) error {
|
func (xm *XMPP) Invite(userId UserID, roomId RoomID) error {
|
||||||
if roomId == "" {
|
|
||||||
xm.conn.RequestSubscription(string(userId))
|
|
||||||
xm.conn.ApproveSubscription(string(userId))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// TODO
|
// TODO
|
||||||
return fmt.Errorf("Not implemented")
|
return fmt.Errorf("Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) Leave(roomId RoomID) {
|
func (xm *XMPP) Leave(roomId RoomID) {
|
||||||
xm.stateLock.Lock()
|
|
||||||
defer xm.stateLock.Unlock()
|
|
||||||
|
|
||||||
xm.conn.LeaveMUC(string(roomId))
|
xm.conn.LeaveMUC(string(roomId))
|
||||||
|
|
||||||
if muc, ok := xm.muc[roomId]; ok {
|
|
||||||
muc.joined = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) SearchForUsers(query string) ([]UserSearchResult, error) {
|
func (xm *XMPP) Send(event *Event) error {
|
||||||
// TODO: search roster
|
fmt.Printf("xm *XMPP Send %#v\n", event)
|
||||||
return nil, fmt.Errorf("Not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xm *XMPP) Send(event *Event) (string, error) {
|
|
||||||
xm.stateLock.Lock()
|
|
||||||
defer xm.stateLock.Unlock()
|
|
||||||
|
|
||||||
if event.Attachments != nil && len(event.Attachments) > 0 {
|
|
||||||
for _, at := range event.Attachments {
|
|
||||||
url := at.URL()
|
|
||||||
if url == "" {
|
|
||||||
// TODO find a way to send them using some hosing of some kind
|
|
||||||
return "", fmt.Errorf("Attachment without URL sent to XMPP")
|
|
||||||
} else {
|
|
||||||
event.Text += fmt.Sprintf("\n%s (%s, %dkb)",
|
|
||||||
url, at.Mimetype(), at.Size()/1024)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Id == "" {
|
|
||||||
event.Id = xid.New().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("xm *XMPP Send %#v\n", event)
|
|
||||||
if len(event.Recipient) > 0 {
|
if len(event.Recipient) > 0 {
|
||||||
_, err := xm.conn.Send(gxmpp.Chat{
|
xm.conn.Send(gxmpp.Chat{
|
||||||
Type: "chat",
|
Type: "chat",
|
||||||
Remote: string(event.Recipient),
|
Remote: string(event.Recipient),
|
||||||
Text: event.Text,
|
Text: event.Text,
|
||||||
})
|
})
|
||||||
return event.Id, err
|
return nil
|
||||||
} else if len(event.Room) > 0 {
|
} else if len(event.Room) > 0 {
|
||||||
if muc, ok := xm.muc[event.Room]; ok {
|
xm.conn.Send(gxmpp.Chat{
|
||||||
muc.flushLeavesJoins(event.Room, xm.handler)
|
|
||||||
}
|
|
||||||
_, err := xm.conn.Send(gxmpp.Chat{
|
|
||||||
Type: "groupchat",
|
Type: "groupchat",
|
||||||
Remote: string(event.Room),
|
Remote: string(event.Room),
|
||||||
Text: event.Text,
|
Text: event.Text,
|
||||||
ID: event.Id,
|
|
||||||
})
|
})
|
||||||
return event.Id, err
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return "", fmt.Errorf("Invalid event")
|
return fmt.Errorf("Invalid event")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xm *XMPP) UserCommand(cmd string) {
|
|
||||||
xm.handler.SystemMessage("Command not supported.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xm *XMPP) Close() {
|
func (xm *XMPP) Close() {
|
||||||
xm.stateLock.Lock()
|
|
||||||
defer xm.stateLock.Unlock()
|
|
||||||
|
|
||||||
if xm.conn != nil {
|
|
||||||
xm.conn.Close()
|
xm.conn.Close()
|
||||||
}
|
|
||||||
xm.conn = nil
|
xm.conn = nil
|
||||||
xm.connectorLoopNum += 1
|
xm.connectorLoopNum += 1
|
||||||
xm.connected = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
320
db.go
320
db.go
|
@ -1,320 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/crypto/blake2b"
|
|
||||||
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var db *gorm.DB
|
|
||||||
|
|
||||||
func InitDb() error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
db, err = gorm.Open(config.DbType, config.DbPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db.AutoMigrate(&DbAccountConfig{})
|
|
||||||
|
|
||||||
db.AutoMigrate(&DbJoinedRoom{})
|
|
||||||
db.Model(&DbJoinedRoom{}).AddIndex("idx_joined_room_user_protocol_account", "mx_user_id", "protocol", "account_name")
|
|
||||||
|
|
||||||
db.AutoMigrate(&DbUserMap{})
|
|
||||||
db.Model(&DbUserMap{}).AddIndex("idx_user_map_protocol_user", "protocol", "user_id")
|
|
||||||
|
|
||||||
db.AutoMigrate(&DbRoomMap{})
|
|
||||||
db.Model(&DbRoomMap{}).AddIndex("idx_room_map_protocol_room", "protocol", "room_id")
|
|
||||||
|
|
||||||
db.AutoMigrate(&DbPmRoomMap{})
|
|
||||||
db.Model(&DbPmRoomMap{}).AddIndex("idx_pm_room_map_protocol_user_account_user", "protocol", "mx_user_id", "account_name", "user_id")
|
|
||||||
|
|
||||||
db.AutoMigrate(&DbKv{})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account configuration
|
|
||||||
type DbAccountConfig struct {
|
|
||||||
gorm.Model
|
|
||||||
|
|
||||||
MxUserID string `gorm:"index"`
|
|
||||||
Name string
|
|
||||||
Protocol string
|
|
||||||
Config string
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of joined channels to be re-joined on reconnect
|
|
||||||
type DbJoinedRoom struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement:true"`
|
|
||||||
|
|
||||||
// User id and account name
|
|
||||||
MxUserID string
|
|
||||||
Protocol string
|
|
||||||
AccountName string
|
|
||||||
|
|
||||||
// Room ID
|
|
||||||
RoomID connector.RoomID
|
|
||||||
}
|
|
||||||
|
|
||||||
// User mapping between protocol user IDs and puppeted matrix ids
|
|
||||||
type DbUserMap struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement:true"`
|
|
||||||
|
|
||||||
// Protocol and user ID on the bridged network
|
|
||||||
Protocol string
|
|
||||||
UserID connector.UserID
|
|
||||||
|
|
||||||
// Puppetted Matrix ID
|
|
||||||
MxUserID string `gorm:"index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room mapping between Matrix rooms and outside rooms
|
|
||||||
type DbRoomMap struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement:true"`
|
|
||||||
|
|
||||||
// Network protocol and room ID on the bridged network
|
|
||||||
Protocol string
|
|
||||||
RoomID connector.RoomID
|
|
||||||
|
|
||||||
// Bridged room matrix id
|
|
||||||
MxRoomID string `gorm:"index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Room mapping between Matrix rooms and private messages
|
|
||||||
type DbPmRoomMap struct {
|
|
||||||
ID uint `gorm:"primaryKey;autoIncrement:true"`
|
|
||||||
|
|
||||||
// User id and account name of the local end viewed on Matrix
|
|
||||||
MxUserID string
|
|
||||||
Protocol string
|
|
||||||
AccountName string
|
|
||||||
|
|
||||||
// User id to reach them on the bridged network
|
|
||||||
UserID connector.UserID
|
|
||||||
|
|
||||||
// Bridged room for PMs
|
|
||||||
MxRoomID string `gorm:"index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key-value store for various things
|
|
||||||
type DbKv struct {
|
|
||||||
Key string `gorm:"primaryKey"`
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Simple locking mechanism
|
|
||||||
// Slot keys are strings that identify the object we are acting upon
|
|
||||||
// They define which lock to lock for a certain operation
|
|
||||||
|
|
||||||
var dbLocks [256]sync.Mutex
|
|
||||||
|
|
||||||
func dbLockSlot(key string) {
|
|
||||||
slot := blake2b.Sum512([]byte(key))[0]
|
|
||||||
dbLocks[slot].Lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbUnlockSlot(key string) {
|
|
||||||
slot := blake2b.Sum512([]byte(key))[0]
|
|
||||||
dbLocks[slot].Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Key-value store supporting atomic test-and-set
|
|
||||||
|
|
||||||
func dbKvGet(key string) string {
|
|
||||||
var entry DbKv
|
|
||||||
if db.Where(&DbKv{Key: key}).First(&entry).RecordNotFound() {
|
|
||||||
return ""
|
|
||||||
} else {
|
|
||||||
return entry.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbKvPut(key string, value string) {
|
|
||||||
dbLockSlot(key)
|
|
||||||
defer dbUnlockSlot(key)
|
|
||||||
|
|
||||||
dbKvPutLocked(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variant of dbKvPut that does not take a lock,
|
|
||||||
// use this if the slot is already locked
|
|
||||||
func dbKvPutLocked(key string, value string) {
|
|
||||||
var entry DbKv
|
|
||||||
db.Where(&DbKv{Key: key}).Assign(&DbKv{Value: value}).FirstOrCreate(&entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbKvTestAndSet(key string, value string) bool {
|
|
||||||
dbLockSlot(key)
|
|
||||||
defer dbUnlockSlot(key)
|
|
||||||
|
|
||||||
// True if value was changed, false if was already set
|
|
||||||
if dbKvGet(key) == value {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
dbKvPutLocked(key, value)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func dbGetMxRoom(protocol string, roomId connector.RoomID) (string, error) {
|
|
||||||
slot_key := fmt.Sprintf("room:%s/%s", protocol, roomId)
|
|
||||||
|
|
||||||
dbLockSlot(slot_key)
|
|
||||||
defer dbUnlockSlot(slot_key)
|
|
||||||
|
|
||||||
// Check if room exists in our mapping,
|
|
||||||
// If not create it
|
|
||||||
var room DbRoomMap
|
|
||||||
must_create := db.First(&room, DbRoomMap{
|
|
||||||
Protocol: protocol,
|
|
||||||
RoomID: roomId,
|
|
||||||
}).RecordNotFound()
|
|
||||||
|
|
||||||
if must_create {
|
|
||||||
alias := roomAlias(protocol, roomId)
|
|
||||||
|
|
||||||
// Delete previous alias if it existed
|
|
||||||
prev_full_alias := fmt.Sprintf("#%s:%s", alias, config.MatrixDomain)
|
|
||||||
mx_room_id, err := mx.DirectoryRoom(prev_full_alias)
|
|
||||||
if err == nil {
|
|
||||||
mx.DirectoryDeleteRoom(prev_full_alias)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create room
|
|
||||||
name := fmt.Sprintf("%s (%s)", roomId, protocol)
|
|
||||||
|
|
||||||
mx_room_id, err = mx.CreateRoom(name, alias, []string{})
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Could not create room for %s: %s", name, err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
room = DbRoomMap{
|
|
||||||
Protocol: protocol,
|
|
||||||
RoomID: roomId,
|
|
||||||
MxRoomID: mx_room_id,
|
|
||||||
}
|
|
||||||
db.Create(&room)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("%s -> %s", slot_key, room.MxRoomID)
|
|
||||||
|
|
||||||
return room.MxRoomID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbGetMxPmRoom(protocol string, them connector.UserID, themMxId string, usMxId string, usAccount string) (string, error) {
|
|
||||||
map_key := &DbPmRoomMap{
|
|
||||||
MxUserID: usMxId,
|
|
||||||
Protocol: protocol,
|
|
||||||
AccountName: usAccount,
|
|
||||||
UserID: them,
|
|
||||||
}
|
|
||||||
slot_key := fmt.Sprintf("pmroom:%s/%s/%s/%s", protocol, usMxId, usAccount, them)
|
|
||||||
|
|
||||||
dbLockSlot(slot_key)
|
|
||||||
defer dbUnlockSlot(slot_key)
|
|
||||||
|
|
||||||
var room DbPmRoomMap
|
|
||||||
must_create := db.First(&room, map_key).RecordNotFound()
|
|
||||||
|
|
||||||
if must_create {
|
|
||||||
name := fmt.Sprintf("%s (%s)", them, protocol)
|
|
||||||
|
|
||||||
mx_room_id, err := mx.CreateDirectRoomAs([]string{usMxId}, themMxId)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Could not create room for %s: %s", name, err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
room = DbPmRoomMap{
|
|
||||||
MxUserID: usMxId,
|
|
||||||
Protocol: protocol,
|
|
||||||
AccountName: usAccount,
|
|
||||||
UserID: them,
|
|
||||||
MxRoomID: mx_room_id,
|
|
||||||
}
|
|
||||||
db.Create(&room)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("%s -> %s", slot_key, room.MxRoomID)
|
|
||||||
|
|
||||||
return room.MxRoomID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbDeletePmRoom(room *DbPmRoomMap) {
|
|
||||||
if room.ID != 0 {
|
|
||||||
db.Delete(room)
|
|
||||||
} else {
|
|
||||||
log.Warnf("In dbDeletePmRoom: %#v (not deleting since primary key is zero)", room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbGetMxUser(protocol string, userId connector.UserID) (string, error) {
|
|
||||||
slot_key := fmt.Sprintf("user:%s/%s", protocol, userId)
|
|
||||||
|
|
||||||
dbLockSlot(slot_key)
|
|
||||||
defer dbUnlockSlot(slot_key)
|
|
||||||
|
|
||||||
var user DbUserMap
|
|
||||||
|
|
||||||
must_create := db.First(&user, DbUserMap{
|
|
||||||
Protocol: protocol,
|
|
||||||
UserID: userId,
|
|
||||||
}).RecordNotFound()
|
|
||||||
if must_create {
|
|
||||||
username := userMxId(protocol, userId)
|
|
||||||
|
|
||||||
err := mx.RegisterUser(username)
|
|
||||||
if err != nil {
|
|
||||||
if mxE, ok := err.(*mxlib.MxError); !ok || mxE.ErrCode != "M_USER_IN_USE" {
|
|
||||||
log.Warnf("Could not register %s: %s", username, err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mxid := fmt.Sprintf("@%s:%s", username, config.MatrixDomain)
|
|
||||||
mx.ProfileDisplayname(mxid, fmt.Sprintf("%s (%s)", userId, protocol))
|
|
||||||
|
|
||||||
user = DbUserMap{
|
|
||||||
Protocol: protocol,
|
|
||||||
UserID: userId,
|
|
||||||
MxUserID: mxid,
|
|
||||||
}
|
|
||||||
db.Create(&user)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("%s -> %s", slot_key, user.MxUserID)
|
|
||||||
|
|
||||||
return user.MxUserID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbIsPmRoom(mxRoomId string) *DbPmRoomMap {
|
|
||||||
var pm_room DbPmRoomMap
|
|
||||||
if db.First(&pm_room, DbPmRoomMap{MxRoomID: mxRoomId}).RecordNotFound() {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return &pm_room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbIsPublicRoom(mxRoomId string) *DbRoomMap {
|
|
||||||
var room DbRoomMap
|
|
||||||
if db.First(&room, DbRoomMap{MxRoomID: mxRoomId}).RecordNotFound() {
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return &room
|
|
||||||
}
|
|
||||||
}
|
|
BIN
easybridge.jpg
BIN
easybridge.jpg
Binary file not shown.
Before Width: | Height: | Size: 39 KiB |
652
external/messenger.py
vendored
652
external/messenger.py
vendored
|
@ -1,652 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import signal
|
|
||||||
import threading
|
|
||||||
import queue
|
|
||||||
import pickle
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from urllib.parse import unquote as UrlUnquote
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import getpass
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
import fbchat
|
|
||||||
from fbchat.models import *
|
|
||||||
|
|
||||||
# ---- MESSAGE TYPES ----
|
|
||||||
|
|
||||||
# ezbr -> external
|
|
||||||
CONFIGURE = "configure"
|
|
||||||
GET_USER = "get_user"
|
|
||||||
SET_USER_INFO = "set_user_info"
|
|
||||||
SET_ROOM_INFO = "set_room_info"
|
|
||||||
JOIN = "join"
|
|
||||||
INVITE = "invite"
|
|
||||||
LEAVE = "leave"
|
|
||||||
SEARCH = "search"
|
|
||||||
SEND = "send"
|
|
||||||
USER_COMMAND = "user_command"
|
|
||||||
CLOSE = "close"
|
|
||||||
|
|
||||||
# external -> ezbr
|
|
||||||
SAVE_CONFIG = "save_config"
|
|
||||||
SYSTEM_MESSAGE = "system_message"
|
|
||||||
JOINED = "joined"
|
|
||||||
LEFT = "left"
|
|
||||||
USER_INFO_UPDATED = "user_info_updated"
|
|
||||||
ROOM_INFO_UPDATED = "room_info_updated"
|
|
||||||
EVENT = "event"
|
|
||||||
CACHE_PUT = "cache_put"
|
|
||||||
CACHE_GET = "cache_get"
|
|
||||||
|
|
||||||
# reply messages
|
|
||||||
# ezbr -> external: all must wait for a reply!
|
|
||||||
# external -> ezbr: only CACHE_GET produces a reply
|
|
||||||
REP_OK = "rep_ok"
|
|
||||||
REP_SEARCH_RESULTS = "rep_search_results"
|
|
||||||
REP_ERROR = "rep_error"
|
|
||||||
|
|
||||||
# Event types
|
|
||||||
EVENT_JOIN = "join"
|
|
||||||
EVENT_LEAVE = "leave"
|
|
||||||
EVENT_MESSAGE = "message"
|
|
||||||
EVENT_ACTION = "action"
|
|
||||||
|
|
||||||
|
|
||||||
def mediaObjectOfURL(url):
|
|
||||||
return {
|
|
||||||
"filename": url.split("?")[0].split("/")[-1],
|
|
||||||
"url": url,
|
|
||||||
}
|
|
||||||
|
|
||||||
def stripFbLinkPrefix(url):
|
|
||||||
PREFIX = "https://l.facebook.com/l.php?u="
|
|
||||||
if url[:len(PREFIX)] == PREFIX:
|
|
||||||
return UrlUnquote(url[len(PREFIX):].split('&')[0])
|
|
||||||
else:
|
|
||||||
return url
|
|
||||||
|
|
||||||
# ---- MESSENGER CLIENT CLASS THAT HANDLES EVENTS ----
|
|
||||||
|
|
||||||
class MessengerBridgeClient(fbchat.Client):
|
|
||||||
def __init__(self, bridge, *args, **kwargs):
|
|
||||||
self.bridge = bridge
|
|
||||||
super(MessengerBridgeClient, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def setBridge(self, bridge):
|
|
||||||
self.bridge = bridge
|
|
||||||
|
|
||||||
## Redirect all interesting events to Bridge
|
|
||||||
def onMessage(self, *args, **kwargs):
|
|
||||||
self.bridge.onMessage(*args, **kwargs)
|
|
||||||
def onPeopleAdded(self, *args, **kwargs):
|
|
||||||
self.bridge.onPeopleAdded(*args, **kwargs)
|
|
||||||
def onPersonRemoved(self, *args, **kwargs):
|
|
||||||
self.bridge.onPersonRemoved(*args, **kwargs)
|
|
||||||
def onTitleChange(self, *args, **kwargs):
|
|
||||||
self.bridge.onTitleChange(*args, **kwargs)
|
|
||||||
def on2FACode(self, *args, **kwargs):
|
|
||||||
return self.bridge.on2FACode(*args, **kwargs)
|
|
||||||
|
|
||||||
# ---- SEPARATE THREADS FOR INITIAL SYNC & CLIENT LISTEN ----
|
|
||||||
|
|
||||||
class LoginThread(threading.Thread):
|
|
||||||
def __init__(self, bridge, *args, **kwargs):
|
|
||||||
super(LoginThread, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.bridge = bridge
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.bridge.processLogin()
|
|
||||||
|
|
||||||
class SyncerThread(threading.Thread):
|
|
||||||
def __init__(self, bridge, thread_queue, *args, **kwargs):
|
|
||||||
super(SyncerThread, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.bridge = bridge
|
|
||||||
self.thread_queue = thread_queue
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while True:
|
|
||||||
thread = self.thread_queue.get(block=True)
|
|
||||||
sys.stderr.write("(python) fb thread: {}\n".format(thread))
|
|
||||||
self.bridge.setup_joined_thread(thread)
|
|
||||||
|
|
||||||
|
|
||||||
class ClientListenThread(threading.Thread):
|
|
||||||
def __init__(self, client, *args, **kwargs):
|
|
||||||
super(ClientListenThread, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.client = client
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
sys.stderr.write("(python messenger) Start client.listen()\n")
|
|
||||||
self.client.listen()
|
|
||||||
|
|
||||||
|
|
||||||
# ---- MAIN LOOP THAT HANDLES REQUESTS FROM BRIDGE ----
|
|
||||||
|
|
||||||
class MessengerBridge:
|
|
||||||
def __init__(self):
|
|
||||||
self.init_backlog_length = 100
|
|
||||||
|
|
||||||
self.config = None
|
|
||||||
self.login_in_progress = None
|
|
||||||
|
|
||||||
# We cache maps between two kinds of identifiers:
|
|
||||||
# - facebook uids of users
|
|
||||||
# - identifiers for the bridge, which are the username when defined (otherwise equal to above)
|
|
||||||
# Generally speaking, the first is referred to as uid whereas the second is just id
|
|
||||||
# THESE MAPS SHOULD NOT BE USED DIRECTLY, instead functions getUserId, getUserIdFromUid and revUserId should be used
|
|
||||||
self.uid_map = {} # map from fb user uid to bridge id
|
|
||||||
self.rev_uid = {} # map fro bridge id to fb user uid
|
|
||||||
|
|
||||||
# caches the room we (the user of the bridge) have joined (map keys = room uid)
|
|
||||||
self.my_joined_rooms = {}
|
|
||||||
|
|
||||||
# caches for the people that are in rooms so that we don't send JOINED every time (map keys = "<userId>--<threadId>")
|
|
||||||
self.others_joined_map = {}
|
|
||||||
|
|
||||||
# queue for thread syncing
|
|
||||||
self.sync_thread_queue = queue.Queue(100)
|
|
||||||
|
|
||||||
def getUserId(self, user):
|
|
||||||
retval = None
|
|
||||||
if user.url is not None and not "?" in user.url:
|
|
||||||
retval = user.url.split("/")[-1]
|
|
||||||
else:
|
|
||||||
retval = user.uid
|
|
||||||
|
|
||||||
if user.uid not in self.uid_map:
|
|
||||||
self.uid_map[user.uid] = retval
|
|
||||||
self.rev_uid[retval] = user.uid
|
|
||||||
|
|
||||||
user_info = {
|
|
||||||
"display_name": user.name,
|
|
||||||
}
|
|
||||||
if user.photo is not None:
|
|
||||||
user_info["avatar"] = mediaObjectOfURL(user.photo)
|
|
||||||
self.write({
|
|
||||||
"_type": USER_INFO_UPDATED,
|
|
||||||
"user": retval,
|
|
||||||
"data": user_info,
|
|
||||||
})
|
|
||||||
|
|
||||||
return retval
|
|
||||||
|
|
||||||
def getUserIdFromUid(self, uid):
|
|
||||||
if uid in self.uid_map:
|
|
||||||
return self.uid_map[uid]
|
|
||||||
else:
|
|
||||||
user = self.client.fetchUserInfo(uid)[uid]
|
|
||||||
return self.getUserId(user)
|
|
||||||
|
|
||||||
def revUserId(self, user_id):
|
|
||||||
if user_id not in self.rev_uid:
|
|
||||||
for user in self.client.searchForUsers(user_id):
|
|
||||||
self.getUserId(user)
|
|
||||||
|
|
||||||
if user_id not in self.rev_uid:
|
|
||||||
raise ValueError("User not found: {}".format(user_id))
|
|
||||||
|
|
||||||
return self.rev_uid[user_id]
|
|
||||||
|
|
||||||
def getUserShortName(self, user):
|
|
||||||
if user.first_name != None:
|
|
||||||
return user.first_name
|
|
||||||
else:
|
|
||||||
return user.name
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.client = None
|
|
||||||
self.keep_running = True
|
|
||||||
self.cache_gets = {}
|
|
||||||
self.num = 0
|
|
||||||
self.my_user_id = ""
|
|
||||||
|
|
||||||
while self.keep_running:
|
|
||||||
try:
|
|
||||||
line = sys.stdin.readline()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.stderr.write("(python messenger) shutting down")
|
|
||||||
self.close()
|
|
||||||
break
|
|
||||||
|
|
||||||
sys.stderr.write("(python) reading {}\n".format(line.strip()))
|
|
||||||
cmd = json.loads(line)
|
|
||||||
|
|
||||||
try:
|
|
||||||
rep = self.handle_cmd(cmd)
|
|
||||||
if rep is None:
|
|
||||||
rep = {}
|
|
||||||
if "_type" not in rep:
|
|
||||||
rep["_type"] = REP_OK
|
|
||||||
except Exception as e:
|
|
||||||
sys.stderr.write("(python) {}\n".format(traceback.format_exc()))
|
|
||||||
rep = {
|
|
||||||
"_type": REP_ERROR,
|
|
||||||
"error": "{}".format(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
rep["_id"] = cmd["_id"]
|
|
||||||
self.write(rep)
|
|
||||||
|
|
||||||
def write(self, msg):
|
|
||||||
msgstr = json.dumps(msg)
|
|
||||||
sys.stderr.write("(python) writing {}\n".format(msgstr))
|
|
||||||
sys.stdout.write(msgstr + "\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def system_message(self, msg):
|
|
||||||
self.write({
|
|
||||||
"_type": SYSTEM_MESSAGE,
|
|
||||||
"value": msg,
|
|
||||||
})
|
|
||||||
|
|
||||||
def handle_cmd(self, cmd):
|
|
||||||
ty = cmd["_type"]
|
|
||||||
if ty == CONFIGURE:
|
|
||||||
if self.login_in_progress is None:
|
|
||||||
self.config = cmd["data"]
|
|
||||||
self.login_in_progress = queue.Queue(1)
|
|
||||||
LoginThread(self).start()
|
|
||||||
else:
|
|
||||||
return {"_type": REP_ERROR, "error": "Already logging in (CONFIGURE sent twice)"}
|
|
||||||
|
|
||||||
elif ty == CLOSE:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
elif ty == GET_USER:
|
|
||||||
return {"_type": REP_OK, "user": self.my_user_id}
|
|
||||||
|
|
||||||
elif ty == JOIN:
|
|
||||||
if self.client is None:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.ensure_i_joined(cmd["room"])
|
|
||||||
|
|
||||||
elif ty == LEAVE:
|
|
||||||
thread_id = cmd["room"]
|
|
||||||
self.client.removeUserFromGroup(self.client.uid, thread_id)
|
|
||||||
if thread_id in self.my_joined_rooms:
|
|
||||||
del self.my_joined_rooms[thread_id]
|
|
||||||
|
|
||||||
elif ty == INVITE:
|
|
||||||
if cmd["room"] != "":
|
|
||||||
uid = self.revUserId(cmd["user"])
|
|
||||||
self.client.addUsersToGroup([uid], cmd["room"])
|
|
||||||
|
|
||||||
elif ty == SEARCH:
|
|
||||||
users = self.client.searchForUsers(cmd["data"])
|
|
||||||
rep = []
|
|
||||||
for user in users:
|
|
||||||
rep.append({
|
|
||||||
"id": self.getUserId(user),
|
|
||||||
"display_name": user.name,
|
|
||||||
})
|
|
||||||
return {"_type": REP_SEARCH_RESULTS, "data": rep}
|
|
||||||
|
|
||||||
elif ty == SEND:
|
|
||||||
event = cmd["data"]
|
|
||||||
if event["type"] in [EVENT_MESSAGE, EVENT_ACTION]:
|
|
||||||
attachments = []
|
|
||||||
if "attachments" in event and isinstance(event["attachments"], list):
|
|
||||||
for at in event["attachments"]:
|
|
||||||
if "url" in at:
|
|
||||||
attachments.append(at["url"])
|
|
||||||
else:
|
|
||||||
# TODO
|
|
||||||
sys.stdout.write("Unhandled: attachment without URL")
|
|
||||||
|
|
||||||
msg = Message(event["text"])
|
|
||||||
if event["type"] == EVENT_ACTION:
|
|
||||||
msg.text = "* " + event["text"]
|
|
||||||
|
|
||||||
if event["room"] != "":
|
|
||||||
if len(attachments) > 0:
|
|
||||||
msg_id = self.client.sendRemoteFiles(attachments, message=msg, thread_id=event["room"], thread_type=ThreadType.GROUP)
|
|
||||||
else:
|
|
||||||
msg_id = self.client.send(msg, thread_id=event["room"], thread_type=ThreadType.GROUP)
|
|
||||||
elif event["recipient"] != "":
|
|
||||||
uid = self.revUserId(event["recipient"])
|
|
||||||
sys.stderr.write("(python) Sending to {}\n".format(uid))
|
|
||||||
if len(attachments) > 0:
|
|
||||||
msg_id = self.client.sendRemoteFiles(attachments, message=msg, thread_id=uid, thread_type=ThreadType.USER)
|
|
||||||
else:
|
|
||||||
msg_id = self.client.send(msg, thread_id=uid, thread_type=ThreadType.USER)
|
|
||||||
else:
|
|
||||||
return {"_type": REP_ERROR, "error": "Invalid message"}
|
|
||||||
|
|
||||||
return {"_type": REP_OK, "event_id": msg_id}
|
|
||||||
|
|
||||||
elif ty == REP_OK and cmd["_id"] in self.cache_gets:
|
|
||||||
self.cache_gets[cmd["_id"]].put(cmd["value"])
|
|
||||||
|
|
||||||
elif ty == USER_COMMAND:
|
|
||||||
self.handleUserCommand(cmd["value"])
|
|
||||||
|
|
||||||
else:
|
|
||||||
return {"_type": REP_ERROR, "error": "Not implemented"}
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.keep_running = False
|
|
||||||
self.client.stopListening()
|
|
||||||
|
|
||||||
def cache_get(self, key):
|
|
||||||
self.num += 1
|
|
||||||
num = self.num
|
|
||||||
q = queue.Queue(1)
|
|
||||||
self.cache_gets[num] = q
|
|
||||||
self.write({"_type": CACHE_GET, "_id": num, "key": key})
|
|
||||||
try:
|
|
||||||
rep = q.get(block=True, timeout=30)
|
|
||||||
except queue.Empty:
|
|
||||||
rep = ""
|
|
||||||
del self.cache_gets[num]
|
|
||||||
return rep
|
|
||||||
|
|
||||||
def cache_put(self, key, value):
|
|
||||||
self.write({"_type": CACHE_PUT, "key": key, "value": value})
|
|
||||||
|
|
||||||
# ---- Process login (called from separate thread) ----
|
|
||||||
|
|
||||||
def processLogin(self):
|
|
||||||
self.init_backlog_length = int(self.config["initial_backlog"])
|
|
||||||
|
|
||||||
has_pickle = "client_pickle" in self.config and len(self.config["client_pickle"]) > 0
|
|
||||||
if has_pickle:
|
|
||||||
data = base64.b64decode(self.config["client_pickle"])
|
|
||||||
data = zlib.decompress(data)
|
|
||||||
self.client = pickle.loads(data)
|
|
||||||
else:
|
|
||||||
email, password = self.config["email"], self.config["password"]
|
|
||||||
self.client = MessengerBridgeClient(bridge=self, email=email, password=password, max_tries=1)
|
|
||||||
|
|
||||||
if not self.client.isLoggedIn():
|
|
||||||
self.system_message("Unable to login (invalid pickle? dunno)")
|
|
||||||
else:
|
|
||||||
self.system_message("Login complete, will now sync threads.")
|
|
||||||
|
|
||||||
if not has_pickle:
|
|
||||||
self.client.setBridge(None)
|
|
||||||
data = pickle.dumps(self.client)
|
|
||||||
data = zlib.compress(data)
|
|
||||||
self.config["client_pickle"] = base64.b64encode(data).decode('ascii')
|
|
||||||
self.write({"_type": SAVE_CONFIG, "data": self.config})
|
|
||||||
|
|
||||||
self.client.setBridge(self)
|
|
||||||
|
|
||||||
self.my_user_id = self.getUserIdFromUid(self.client.uid)
|
|
||||||
|
|
||||||
threads = self.client.fetchThreadList(limit=10)
|
|
||||||
# ensure we have a correct mapping for bridged user IDs to fb uids
|
|
||||||
# (this should be fast)
|
|
||||||
for thread in threads:
|
|
||||||
if thread.type == ThreadType.USER:
|
|
||||||
self.getUserId(thread)
|
|
||||||
|
|
||||||
SyncerThread(self, self.sync_thread_queue).start()
|
|
||||||
for thread in reversed(threads):
|
|
||||||
self.sync_thread_queue.put(thread)
|
|
||||||
|
|
||||||
ClientListenThread(self.client).start()
|
|
||||||
|
|
||||||
self.login_in_progress = None
|
|
||||||
|
|
||||||
# ---- Info sync ----
|
|
||||||
|
|
||||||
def ensure_i_joined(self, thread_id):
|
|
||||||
if thread_id not in self.my_joined_rooms:
|
|
||||||
self.my_joined_rooms[thread_id] = True
|
|
||||||
|
|
||||||
thread = self.client.fetchThreadInfo(thread_id)[thread_id]
|
|
||||||
self.sync_thread_queue.put(thread)
|
|
||||||
|
|
||||||
def setup_joined_thread(self, thread):
|
|
||||||
sys.stderr.write("(python) setup_joined_thread {}".format(thread))
|
|
||||||
if thread.type == ThreadType.GROUP:
|
|
||||||
members = self.client.fetchAllUsersFromThreads([thread])
|
|
||||||
|
|
||||||
self.write({
|
|
||||||
"_type": JOINED,
|
|
||||||
"room": thread.uid,
|
|
||||||
})
|
|
||||||
|
|
||||||
self.send_room_info(thread, members)
|
|
||||||
self.send_room_members(thread, members)
|
|
||||||
|
|
||||||
self.backlog_room(thread)
|
|
||||||
|
|
||||||
|
|
||||||
def send_room_info(self, thread, members):
|
|
||||||
members.sort(key=lambda m: m.uid)
|
|
||||||
|
|
||||||
room_info = {}
|
|
||||||
if thread.name is not None:
|
|
||||||
room_info["name"] = thread.name
|
|
||||||
else:
|
|
||||||
who = [m for m in members if m.uid != self.client.uid]
|
|
||||||
if len(who) > 3:
|
|
||||||
room_info["name"] = ", ".join([self.getUserShortName(m) for m in who[:3]] + ["..."])
|
|
||||||
else:
|
|
||||||
room_info["name"] = ", ".join([self.getUserShortName(m) for m in who])
|
|
||||||
|
|
||||||
if thread.photo is not None:
|
|
||||||
room_info["picture"] = mediaObjectOfURL(thread.photo)
|
|
||||||
else:
|
|
||||||
for m in members:
|
|
||||||
if m.uid != self.client.uid and m.photo is not None:
|
|
||||||
room_info["picture"] = mediaObjectOfURL(m.photo)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.write({
|
|
||||||
"_type": ROOM_INFO_UPDATED,
|
|
||||||
"room": thread.uid,
|
|
||||||
"data": room_info,
|
|
||||||
})
|
|
||||||
|
|
||||||
def send_room_members(self, thread, members):
|
|
||||||
for member in members:
|
|
||||||
sys.stderr.write("(python) fb thread member: {}\n".format(member))
|
|
||||||
self.ensureJoined(self.getUserId(member), thread.uid)
|
|
||||||
|
|
||||||
def backlog_room(self, thread):
|
|
||||||
prev_last_seen = self.cache_get("last_seen_%s"%thread.uid)
|
|
||||||
if prev_last_seen == "":
|
|
||||||
prev_last_seen = None
|
|
||||||
|
|
||||||
messages = []
|
|
||||||
found = False
|
|
||||||
while not found:
|
|
||||||
before = None
|
|
||||||
if len(messages) > 0:
|
|
||||||
before = messages[-1].timestamp
|
|
||||||
page = self.client.fetchThreadMessages(thread.uid, before=before, limit=20)
|
|
||||||
for m in page:
|
|
||||||
if m.uid == prev_last_seen or len(messages) > self.init_backlog_length:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
messages.append(m)
|
|
||||||
|
|
||||||
for m in reversed(messages):
|
|
||||||
if m.text is None:
|
|
||||||
m.text = ""
|
|
||||||
m.text = "[{}] {}".format(
|
|
||||||
time.strftime("%Y-%m-%d %H:%M %Z", time.localtime(float(m.timestamp)/1000)).strip(),
|
|
||||||
m.text)
|
|
||||||
self.onMessage(thread_id=thread.uid,
|
|
||||||
thread_type=thread.type,
|
|
||||||
message_object=m)
|
|
||||||
|
|
||||||
def ensureJoined(self, userId, room):
|
|
||||||
key = "{}--{}".format(userId, room)
|
|
||||||
if not key in self.others_joined_map:
|
|
||||||
self.write({
|
|
||||||
"_type": EVENT,
|
|
||||||
"data": {
|
|
||||||
"type": EVENT_JOIN,
|
|
||||||
"author": userId,
|
|
||||||
"room": room,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
self.others_joined_map[key] = True
|
|
||||||
|
|
||||||
# ---- Event handlers ----
|
|
||||||
|
|
||||||
def onMessage(self, thread_id, thread_type, message_object, **kwargs):
|
|
||||||
self.ensure_i_joined(thread_id)
|
|
||||||
|
|
||||||
if message_object.author == self.client.uid:
|
|
||||||
# Ignore our own messages
|
|
||||||
return
|
|
||||||
|
|
||||||
sys.stderr.write("(python messenger) Got message: {}\n".format(message_object))
|
|
||||||
|
|
||||||
author = self.getUserIdFromUid(message_object.author)
|
|
||||||
|
|
||||||
event = {
|
|
||||||
"id": message_object.uid,
|
|
||||||
"type": EVENT_MESSAGE,
|
|
||||||
"author": author,
|
|
||||||
"text": message_object.text,
|
|
||||||
"attachments": []
|
|
||||||
}
|
|
||||||
if event["text"] is None:
|
|
||||||
event["text"] = ""
|
|
||||||
|
|
||||||
for at in message_object.attachments:
|
|
||||||
if isinstance(at, ImageAttachment):
|
|
||||||
try:
|
|
||||||
full_url = self.client.fetchImageUrl(at.uid)
|
|
||||||
except:
|
|
||||||
time.sleep(1)
|
|
||||||
full_url = self.client.fetchImageUrl(at.uid)
|
|
||||||
event["attachments"].append({
|
|
||||||
"filename": full_url.split("?")[0].split("/")[-1],
|
|
||||||
"url": full_url,
|
|
||||||
"image_size": {
|
|
||||||
"width": at.width,
|
|
||||||
"height": at.height,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
elif isinstance(at, FileAttachment):
|
|
||||||
url = stripFbLinkPrefix(at.url)
|
|
||||||
event["attachments"].append({
|
|
||||||
"filename": at.name,
|
|
||||||
"url": url,
|
|
||||||
})
|
|
||||||
elif isinstance(at, AudioAttachment):
|
|
||||||
url = stripFbLinkPrefix(at.url)
|
|
||||||
event["attachments"].append({
|
|
||||||
"filename": at.filename,
|
|
||||||
"url": url,
|
|
||||||
})
|
|
||||||
elif isinstance(at, ShareAttachment):
|
|
||||||
event["text"] += "\n{}\n{}".format(at.description, at.url)
|
|
||||||
else:
|
|
||||||
event["text"] += "\nUnhandled attachment: {}".format(at)
|
|
||||||
|
|
||||||
if isinstance(message_object.sticker, Sticker):
|
|
||||||
stk = message_object.sticker
|
|
||||||
event["attachments"].append({
|
|
||||||
"filename": stk.label,
|
|
||||||
"url": stk.url,
|
|
||||||
"image_size": {
|
|
||||||
"width": stk.width,
|
|
||||||
"height": stk.height,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if thread_type == ThreadType.GROUP:
|
|
||||||
event["room"] = thread_id
|
|
||||||
self.ensureJoined(author, thread_id)
|
|
||||||
|
|
||||||
if event["text"] != "" or len(event["attachments"]) > 0:
|
|
||||||
self.write({"_type": EVENT, "data": event})
|
|
||||||
|
|
||||||
self.cache_put("last_seen_%s"%thread_id, message_object.uid)
|
|
||||||
|
|
||||||
def onPeopleAdded(self, added_ids, thread_id, *args, **kwargs):
|
|
||||||
for user_id in added_ids:
|
|
||||||
if user_id == self.client.uid:
|
|
||||||
self.ensure_i_joined(thread_id)
|
|
||||||
else:
|
|
||||||
self.ensureJoined(self.getUserIdFromUid(user_id), thread_id)
|
|
||||||
|
|
||||||
def onPersonRemoved(self, removed_id, thread_id, *args, **kwargs):
|
|
||||||
if removed_id == self.client.uid:
|
|
||||||
self.write({
|
|
||||||
"_type": LEFT,
|
|
||||||
"room": thread_id,
|
|
||||||
})
|
|
||||||
if thread_id in self.my_joined_rooms:
|
|
||||||
del self.my_joined_rooms[thread_id]
|
|
||||||
else:
|
|
||||||
userId = self.getUserIdFromUid(removed_id),
|
|
||||||
self.write({
|
|
||||||
"_type": EVENT,
|
|
||||||
"data": {
|
|
||||||
"type": EVENT_JOIN,
|
|
||||||
"author": userId,
|
|
||||||
"room": thread_id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
map_key = "{}--{}".format(userId, thread_id)
|
|
||||||
if map_key in self.others_joined_map:
|
|
||||||
del self.others_joined_map[map_key]
|
|
||||||
|
|
||||||
def onTitleChange(self, author_id, new_title, thread_id, thread_type, *args, **kwargs):
|
|
||||||
self.ensure_i_joined(thread_id)
|
|
||||||
if thread_type == ThreadType.GROUP:
|
|
||||||
self.write({
|
|
||||||
"_type": ROOM_INFO_UPDATED,
|
|
||||||
"room": thread_id,
|
|
||||||
"data": {"name": new_title},
|
|
||||||
})
|
|
||||||
|
|
||||||
def on2FACode(self, *args, **kwargs):
|
|
||||||
if self.login_in_progress is None:
|
|
||||||
self.system_message("Facebook messenger requests 2 factor authentication, but we have a bug so that won't work.")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
self.system_message("Facebook messenger requests 2 factor authentication. Enter it by saying: cmd messenger 2fa <your code> (replace messenger by your account name if you have several messenger accounts)")
|
|
||||||
uc = self.login_in_progress.get(block=True)
|
|
||||||
return uc["2fa_code"]
|
|
||||||
|
|
||||||
def handleUserCommand(self, cmd):
|
|
||||||
cmd = cmd.split(' ')
|
|
||||||
if cmd[0] == "2fa":
|
|
||||||
if self.login_in_progress is not None:
|
|
||||||
self.login_in_progress.put({"2fa_code": cmd[1]})
|
|
||||||
else:
|
|
||||||
self.system_message("2FA code not required at this point.")
|
|
||||||
else:
|
|
||||||
self.system_message("Invalid user command.")
|
|
||||||
|
|
||||||
# ---- CLI ----
|
|
||||||
|
|
||||||
def createClientPickle():
|
|
||||||
email = input("Email address of Facebook account: ")
|
|
||||||
password = getpass.getpass()
|
|
||||||
client = MessengerBridgeClient(email, password, max_tries=1)
|
|
||||||
if not client.isLoggedIn():
|
|
||||||
print("Could not log in (why???)")
|
|
||||||
print("Still creating pickle though, maybe it will work after login was authorized?")
|
|
||||||
print("")
|
|
||||||
data = pickle.dumps(client)
|
|
||||||
data = zlib.compress(data)
|
|
||||||
data = base64.b64encode(data).decode('ascii')
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if "create_client_pickle" in sys.argv:
|
|
||||||
createClientPickle()
|
|
||||||
else:
|
|
||||||
bridge = MessengerBridge()
|
|
||||||
bridge.run()
|
|
||||||
|
|
61
go.mod
61
go.mod
|
@ -3,58 +3,15 @@ module git.deuxfleurs.fr/Deuxfleurs/easybridge
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/42wim/matterbridge v1.18.3
|
git.deuxfleurs.fr/lx/go-xmpp v0.0.0-20200217161715-21c9a1d8b8fd
|
||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
git.deuxfleurs.fr/lx/gxmpp v0.0.0-20200217161715-21c9a1d8b8fd
|
||||||
github.com/bwmarrin/discordgo v0.20.2 // indirect
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204 // indirect
|
github.com/jinzhu/gorm v1.9.12
|
||||||
github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec // indirect
|
|
||||||
github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8 // indirect
|
|
||||||
github.com/go-ldap/ldap v3.0.3+incompatible // indirect
|
|
||||||
github.com/go-ldap/ldap/v3 v3.1.7
|
|
||||||
github.com/go-redis/redis v6.15.9+incompatible // indirect
|
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible // indirect
|
|
||||||
github.com/google/go-cmp v0.5.2 // indirect
|
|
||||||
github.com/google/uuid v1.1.2 // indirect
|
|
||||||
github.com/gorilla/mux v1.8.0
|
|
||||||
github.com/gorilla/sessions v1.2.1
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
|
||||||
github.com/jinzhu/gorm v1.9.16
|
|
||||||
github.com/jinzhu/now v1.1.1 // indirect
|
|
||||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect
|
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/labstack/echo v3.3.10+incompatible // indirect
|
|
||||||
github.com/lib/pq v1.8.0 // indirect
|
|
||||||
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7
|
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7
|
||||||
github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050
|
github.com/matterbridge/go-xmpp v0.0.0-20180131083630-7ec2b8b7def6
|
||||||
github.com/matterbridge/gomatrix v0.0.0-20191026211822-6fc7accd00ca // indirect
|
github.com/mattn/go-gtk v0.0.0-20191030024613-af2e013261f5
|
||||||
github.com/mattermost/logr v1.0.13 // indirect
|
github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f // indirect
|
||||||
github.com/mattermost/mattermost-server v5.11.1+incompatible
|
|
||||||
github.com/mattermost/mattermost-server/v5 v5.27.0
|
|
||||||
github.com/mattn/go-xmpp v0.0.0-20200128155807-a86b6abcb3ad
|
github.com/mattn/go-xmpp v0.0.0-20200128155807-a86b6abcb3ad
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/nelsam/hel/v2 v2.3.3 // indirect
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
github.com/nicksnyder/go-i18n v1.10.1 // indirect
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
|
||||||
github.com/nlopes/slack v0.6.0 // indirect
|
|
||||||
github.com/nxadm/tail v1.4.5 // indirect
|
|
||||||
github.com/onsi/ginkgo v1.14.1 // indirect
|
|
||||||
github.com/onsi/gomega v1.10.2 // indirect
|
|
||||||
github.com/pborman/uuid v1.2.1 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
|
||||||
github.com/poy/onpar v1.0.1 // indirect
|
|
||||||
github.com/rs/xid v1.2.1
|
|
||||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.7.0
|
|
||||||
github.com/stretchr/objx v0.3.0 // indirect
|
|
||||||
go.uber.org/multierr v1.6.0 // indirect
|
|
||||||
go.uber.org/zap v1.16.0 // indirect
|
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
|
|
||||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
|
||||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d // indirect
|
|
||||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.3.0
|
|
||||||
honnef.co/go/tools v0.0.1-2020.1.5 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
134
main.go
134
main.go
|
@ -1,39 +1,41 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"fmt"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
_"strings"
|
||||||
"syscall"
|
_ "time"
|
||||||
|
_ "fmt"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/appservice"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/xmpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ConfigAccount struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Rooms []string `json:"rooms"`
|
||||||
|
Config map[string]string `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
LogLevel string `json:"log_level"`
|
HttpBindAddr string`json:"http_bind_addr"`
|
||||||
|
|
||||||
Registration string `json:"registration"`
|
Registration string `json:"registration"`
|
||||||
ASBindAddr string `json:"appservice_bind_addr"`
|
|
||||||
Server string `json:"homeserver_url"`
|
Server string `json:"homeserver_url"`
|
||||||
MatrixDomain string `json:"matrix_domain"`
|
|
||||||
NameFormat string `json:"name_format"`
|
|
||||||
|
|
||||||
WebBindAddr string `json:"web_bind_addr"`
|
|
||||||
WebURL string `json:"web_url"`
|
|
||||||
SessionKey string `json:"web_session_key"`
|
|
||||||
|
|
||||||
DbType string `json:"db_type"`
|
DbType string `json:"db_type"`
|
||||||
DbPath string `json:"db_path"`
|
DbPath string `json:"db_path"`
|
||||||
|
MatrixDomain string `json:"matrix_domain"`
|
||||||
AvatarFile string `json:"easybridge_avatar"`
|
Accounts map[string]map[string]ConfigAccount `json:"accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var configFlag = flag.String("config", "./config.json", "Configuration file path")
|
var configFlag = flag.String("config", "./config.json", "Configuration file path")
|
||||||
|
@ -42,20 +44,13 @@ var config *ConfigFile
|
||||||
var registration *mxlib.Registration
|
var registration *mxlib.Registration
|
||||||
|
|
||||||
func readConfig() ConfigFile {
|
func readConfig() ConfigFile {
|
||||||
defaultKey := make([]byte, 32)
|
|
||||||
rand.Read(defaultKey)
|
|
||||||
|
|
||||||
config_file := ConfigFile{
|
config_file := ConfigFile{
|
||||||
LogLevel: "info",
|
HttpBindAddr: "0.0.0.0:8321",
|
||||||
ASBindAddr: "0.0.0.0:8321",
|
|
||||||
WebBindAddr: "0.0.0.0:8281",
|
|
||||||
Registration: "./registration.yaml",
|
Registration: "./registration.yaml",
|
||||||
Server: "http://localhost:8008",
|
Server: "http://localhost:8008",
|
||||||
NameFormat: "{}_ezbr_",
|
|
||||||
DbType: "sqlite3",
|
DbType: "sqlite3",
|
||||||
DbPath: "easybridge.db",
|
DbPath: "easybridge.db",
|
||||||
AvatarFile: "./easybridge.jpg",
|
Accounts: map[string]map[string]ConfigAccount{},
|
||||||
SessionKey: hex.EncodeToString(defaultKey),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := os.Stat(*configFlag)
|
_, err := os.Stat(*configFlag)
|
||||||
|
@ -106,18 +101,17 @@ func readRegistration(file string) mxlib.Registration {
|
||||||
AsToken: hex.EncodeToString(rnd[:32]),
|
AsToken: hex.EncodeToString(rnd[:32]),
|
||||||
HsToken: hex.EncodeToString(rnd[32:]),
|
HsToken: hex.EncodeToString(rnd[32:]),
|
||||||
SenderLocalpart: "_ezbr_",
|
SenderLocalpart: "_ezbr_",
|
||||||
RateLimited: false,
|
|
||||||
Namespaces: mxlib.RegistrationNamespaceSet{
|
Namespaces: mxlib.RegistrationNamespaceSet{
|
||||||
Users: []mxlib.RegistrationNamespace{
|
Users: []mxlib.RegistrationNamespace{
|
||||||
mxlib.RegistrationNamespace{
|
mxlib.RegistrationNamespace{
|
||||||
Exclusive: true,
|
Exclusive: true,
|
||||||
Regex: "@.*_ezbr_",
|
Regex: "@_ezbr_.*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Aliases: []mxlib.RegistrationNamespace{
|
Aliases: []mxlib.RegistrationNamespace{
|
||||||
mxlib.RegistrationNamespace{
|
mxlib.RegistrationNamespace{
|
||||||
Exclusive: true,
|
Exclusive: true,
|
||||||
Regex: "#.*_ezbr_",
|
Regex: "#_ezbr_.*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Rooms: []mxlib.RegistrationNamespace{},
|
Rooms: []mxlib.RegistrationNamespace{},
|
||||||
|
@ -160,51 +154,71 @@ func readRegistration(file string) mxlib.Registration {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Read configuration
|
|
||||||
config_file := readConfig()
|
config_file := readConfig()
|
||||||
config = &config_file
|
config = &config_file
|
||||||
|
|
||||||
log_level, err := log.ParseLevel(config.LogLevel)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
log.SetLevel(log_level)
|
|
||||||
|
|
||||||
reg_file := readRegistration(config.Registration)
|
reg_file := readRegistration(config.Registration)
|
||||||
registration = ®_file
|
registration = ®_file
|
||||||
|
|
||||||
// Create context and handlers for errors and signals
|
as_config := &appservice.Config{
|
||||||
ctx, stop_all := context.WithCancel(context.Background())
|
HttpBindAddr: config.HttpBindAddr,
|
||||||
errch := make(chan error)
|
Server: config.Server,
|
||||||
sigch := make(chan os.Signal)
|
DbType: config.DbType,
|
||||||
signal.Notify(sigch, os.Interrupt, syscall.SIGTERM)
|
DbPath: config.DbPath,
|
||||||
defer func() {
|
MatrixDomain: config.MatrixDomain,
|
||||||
signal.Stop(sigch)
|
}
|
||||||
stop_all()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Start appservice and web server
|
errch, err := appservice.Start(registration, as_config)
|
||||||
_, err = StartAppService(errch, ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = StartWeb(errch, ctx)
|
for user, accounts := range config.Accounts {
|
||||||
|
for name, params := range accounts {
|
||||||
// Wait for an error somewhere or interrupt signal
|
var conn connector.Connector
|
||||||
select {
|
switch params.Protocol {
|
||||||
case err = <-errch:
|
case "irc":
|
||||||
log.Error(err)
|
conn = &irc.IRC{}
|
||||||
stop_all()
|
case "xmpp":
|
||||||
case sig := <-sigch:
|
conn = &xmpp.XMPP{}
|
||||||
log.Warnf("Got signal: %s", sig.String())
|
default:
|
||||||
stop_all()
|
log.Fatalf("Invalid protocol %s", params.Protocol)
|
||||||
case <-ctx.Done():
|
}
|
||||||
|
account := &appservice.Account{
|
||||||
|
MatrixUser: fmt.Sprintf("@%s:%s", user, config.MatrixDomain),
|
||||||
|
AccountName: name,
|
||||||
|
Protocol: params.Protocol,
|
||||||
|
Conn: conn,
|
||||||
|
JoinedRooms: map[connector.RoomID]bool{},
|
||||||
|
}
|
||||||
|
conn.SetHandler(account)
|
||||||
|
appservice.AddAccount(account)
|
||||||
|
go connectAndJoin(conn, params)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Closing all account connections...")
|
err = <-errch
|
||||||
CloseAllAccountsForShutdown()
|
if err != nil {
|
||||||
log.Info("Exiting.")
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectAndJoin(conn connector.Connector, params ConfigAccount) {
|
||||||
|
log.Printf("Connecting to %s", params.Protocol)
|
||||||
|
err := conn.Configure(params.Config)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not connect to %s: %s", params.Protocol, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Connected to %s, now joining %#v", params.Protocol, params.Rooms)
|
||||||
|
for _, room := range params.Rooms {
|
||||||
|
err := conn.Join(connector.RoomID(room))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not join %s: %s", room, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
29
mxlib/api.go
29
mxlib/api.go
|
@ -26,29 +26,10 @@ type Event struct {
|
||||||
OriginServerTs int `json:"origin_server_ts"`
|
OriginServerTs int `json:"origin_server_ts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasswordLoginRequest struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Identifier map[string]string `json:"identifier"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
DeviceID string `json:"device_id"`
|
|
||||||
InitialDeviceDisplayNAme string `json:"initial_device_display_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginResponse struct {
|
|
||||||
UserID string `json:"user_id"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
Auth RegisterRequestAuth `json:"auth"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegisterRequestAuth struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegisterResponse struct {
|
type RegisterResponse struct {
|
||||||
UserId string `json:"user_id"`
|
UserId string `json:"user_id"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
|
@ -69,8 +50,9 @@ type CreateRoomRequest struct {
|
||||||
PowerLevels map[string]interface{} `json:"power_level_content_override"`
|
PowerLevels map[string]interface{} `json:"power_level_content_override"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateDirectRoomRequest struct {
|
type CreateRoomNoAliasRequest struct {
|
||||||
Preset string `json:"preset"`
|
Preset string `json:"preset"`
|
||||||
|
Name string `json:"name"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Invite []string `json:"invite"`
|
Invite []string `json:"invite"`
|
||||||
CreationContent map[string]interface{} `json:"creation_content"`
|
CreationContent map[string]interface{} `json:"creation_content"`
|
||||||
|
@ -103,10 +85,3 @@ type RoomSendResponse struct {
|
||||||
EventId string `json:"event_id"`
|
EventId string `json:"event_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadResponse struct {
|
|
||||||
ContentUri string `json:"content_uri"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfileAvatarUrl struct {
|
|
||||||
AvatarUrl string `json:"avatar_url"`
|
|
||||||
}
|
|
||||||
|
|
404
mxlib/client.go
404
mxlib/client.go
|
@ -1,404 +0,0 @@
|
||||||
package mxlib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
Server string
|
|
||||||
Token string
|
|
||||||
httpClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(server string, token string) *Client {
|
|
||||||
tr := &http.Transport{
|
|
||||||
MaxIdleConns: 10,
|
|
||||||
IdleConnTimeout: 30 * time.Second,
|
|
||||||
DisableCompression: true,
|
|
||||||
}
|
|
||||||
return &Client{
|
|
||||||
Server: server,
|
|
||||||
Token: token,
|
|
||||||
httpClient: &http.Client{Transport: tr},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) GetApiCall(endpoint string, response interface{}) error {
|
|
||||||
log.Debugf("Matrix GET request: %s\n", endpoint)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", mx.Server+endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mx.DoAndParse(req, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) PutApiCall(endpoint string, data interface{}, response interface{}) error {
|
|
||||||
body, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Matrix PUT request: %s\n", endpoint)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", mx.Server+endpoint, bytes.NewBuffer(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
|
|
||||||
return mx.DoAndParse(req, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) PostApiCall(endpoint string, data interface{}, response interface{}) error {
|
|
||||||
body, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Matrix POST request: %s\n", endpoint)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", mx.Server+endpoint, bytes.NewBuffer(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
|
|
||||||
return mx.DoAndParse(req, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) DeleteApiCall(endpoint string, response interface{}) error {
|
|
||||||
log.Debugf("Matrix DELETE request: %s\n", endpoint)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", mx.Server+endpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mx.DoAndParse(req, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) DoAndParse(req *http.Request, response interface{}) error {
|
|
||||||
if mx.Token != "" {
|
|
||||||
req.Header.Add("Authorization", "Bearer "+mx.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := mx.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
var e MxError
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Debugf("Response: %d %#v\n", resp.StatusCode, e)
|
|
||||||
return &e
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Response: 200 OK")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func (mx *Client) PasswordLogin(username string, password string, device_id string, device_name string) (string, error) {
|
|
||||||
req := PasswordLoginRequest{
|
|
||||||
Type: "m.login.password",
|
|
||||||
Identifier: map[string]string{
|
|
||||||
"type": "m.id.user",
|
|
||||||
"user": username,
|
|
||||||
},
|
|
||||||
Password: password,
|
|
||||||
DeviceID: device_id,
|
|
||||||
InitialDeviceDisplayNAme: device_name,
|
|
||||||
}
|
|
||||||
var rep LoginResponse
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/login", &req, &rep)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if mx.Token == "" {
|
|
||||||
mx.Token = rep.AccessToken
|
|
||||||
}
|
|
||||||
return rep.UserID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RegisterUser(username string) error {
|
|
||||||
req := RegisterRequest{
|
|
||||||
Auth: RegisterRequestAuth{
|
|
||||||
Type: "m.login.application_service",
|
|
||||||
},
|
|
||||||
Type: "m.login.application_service",
|
|
||||||
Username: username,
|
|
||||||
}
|
|
||||||
var rep RegisterResponse
|
|
||||||
return mx.PostApiCall("/_matrix/client/r0/register?kind=user", &req, &rep)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) ProfileDisplayname(userid string, displayname string) error {
|
|
||||||
req := ProfileDisplaynameRequest{
|
|
||||||
Displayname: displayname,
|
|
||||||
}
|
|
||||||
var rep struct{}
|
|
||||||
err := mx.PutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/displayname?user_id=%s",
|
|
||||||
url.QueryEscape(userid), url.QueryEscape(userid)),
|
|
||||||
&req, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) ProfileAvatar(userid string, m connector.MediaObject) error {
|
|
||||||
var mxc *MediaObject
|
|
||||||
if mxm, ok := m.(*MediaObject); ok {
|
|
||||||
mxc = mxm
|
|
||||||
} else {
|
|
||||||
mxm, err := mx.UploadMedia(m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mxc = mxm
|
|
||||||
}
|
|
||||||
|
|
||||||
req := ProfileAvatarUrl{
|
|
||||||
AvatarUrl: mxc.MxcUri(),
|
|
||||||
}
|
|
||||||
var rep struct{}
|
|
||||||
err := mx.PutApiCall(fmt.Sprintf("/_matrix/client/r0/profile/%s/avatar_url?user_id=%s",
|
|
||||||
url.QueryEscape(userid), url.QueryEscape(userid)),
|
|
||||||
&req, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) DirectoryRoom(alias string) (string, error) {
|
|
||||||
var rep DirectoryRoomResponse
|
|
||||||
err := mx.GetApiCall("/_matrix/client/r0/directory/room/"+url.QueryEscape(alias), &rep)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return rep.RoomId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) DirectoryDeleteRoom(alias string) error {
|
|
||||||
var rep struct{}
|
|
||||||
err := mx.DeleteApiCall("/_matrix/client/r0/directory/room/"+url.QueryEscape(alias), &rep)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) CreateRoom(name string, alias string, invite []string) (string, error) {
|
|
||||||
rq := CreateRoomRequest{
|
|
||||||
Preset: "private_chat",
|
|
||||||
RoomAliasName: alias,
|
|
||||||
Name: name,
|
|
||||||
Topic: "",
|
|
||||||
Invite: invite,
|
|
||||||
CreationContent: map[string]interface{}{
|
|
||||||
"m.federate": false,
|
|
||||||
},
|
|
||||||
PowerLevels: map[string]interface{}{
|
|
||||||
"invite": 100,
|
|
||||||
"events": map[string]interface{}{
|
|
||||||
"m.room.topic": 0,
|
|
||||||
"m.room.avatar": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
var rep CreateRoomResponse
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/createRoom", &rq, &rep)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return rep.RoomId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) CreateDirectRoomAs(invite []string, as_user string) (string, error) {
|
|
||||||
rq := CreateDirectRoomRequest{
|
|
||||||
Preset: "private_chat",
|
|
||||||
Topic: "",
|
|
||||||
Invite: invite,
|
|
||||||
CreationContent: map[string]interface{}{
|
|
||||||
"m.federate": false,
|
|
||||||
},
|
|
||||||
PowerLevels: map[string]interface{}{
|
|
||||||
"invite": 100,
|
|
||||||
},
|
|
||||||
IsDirect: true,
|
|
||||||
}
|
|
||||||
var rep CreateRoomResponse
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/createRoom?user_id="+url.QueryEscape(as_user), &rq, &rep)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return rep.RoomId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomInvite(room string, user string) error {
|
|
||||||
rq := RoomInviteRequest{
|
|
||||||
UserId: user,
|
|
||||||
}
|
|
||||||
var rep struct{}
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/invite", &rq, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomKick(room string, user string, reason string) error {
|
|
||||||
rq := RoomKickRequest{
|
|
||||||
UserId: user,
|
|
||||||
Reason: reason,
|
|
||||||
}
|
|
||||||
var rep struct{}
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/kick", &rq, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomJoinAs(room string, user string) error {
|
|
||||||
rq := struct{}{}
|
|
||||||
var rep RoomJoinResponse
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/join?user_id="+url.QueryEscape(user), &rq, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomLeaveAs(room string, user string) error {
|
|
||||||
rq := struct{}{}
|
|
||||||
var rep struct{}
|
|
||||||
err := mx.PostApiCall("/_matrix/client/r0/rooms/"+url.QueryEscape(room)+"/leave?user_id="+url.QueryEscape(user), &rq, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) SendAs(room string, event_type string, content map[string]interface{}, user string) error {
|
|
||||||
txn_id := time.Now().UnixNano()
|
|
||||||
var rep RoomSendResponse
|
|
||||||
err := mx.PutApiCall(fmt.Sprintf(
|
|
||||||
"/_matrix/client/r0/rooms/%s/send/%s/%d?user_id=%s",
|
|
||||||
url.QueryEscape(room), event_type, txn_id, url.QueryEscape(user)),
|
|
||||||
&content, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) SendMessageAs(room string, typ string, body string, user string) error {
|
|
||||||
content := map[string]interface{}{
|
|
||||||
"msgtype": typ,
|
|
||||||
"body": body,
|
|
||||||
}
|
|
||||||
return mx.SendAs(room, "m.room.message", content, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) PutStateAs(room string, event_type string, key string, content map[string]interface{}, as_user string) error {
|
|
||||||
var rep RoomSendResponse
|
|
||||||
err := mx.PutApiCall(fmt.Sprintf(
|
|
||||||
"/_matrix/client/r0/rooms/%s/state/%s/%s?user_id=%s",
|
|
||||||
url.QueryEscape(room), event_type, key, url.QueryEscape(as_user)),
|
|
||||||
&content, &rep)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomNameAs(room string, name string, as_user string) error {
|
|
||||||
content := map[string]interface{}{
|
|
||||||
"name": name,
|
|
||||||
}
|
|
||||||
return mx.PutStateAs(room, "m.room.name", "", content, as_user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomAvatarAs(room string, pic connector.MediaObject, as_user string) error {
|
|
||||||
mo, err := mx.UploadMedia(pic)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
content := map[string]interface{}{
|
|
||||||
"url": mo.MxcUri(),
|
|
||||||
"info": map[string]interface{}{
|
|
||||||
"mimetype": mo.Mimetype(),
|
|
||||||
"size": mo.Size(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return mx.PutStateAs(room, "m.room.avatar", "", content, as_user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) RoomTopicAs(room string, topic string, as_user string) error {
|
|
||||||
content := map[string]interface{}{
|
|
||||||
"topic": topic,
|
|
||||||
}
|
|
||||||
return mx.PutStateAs(room, "m.room.topic", "", content, as_user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) UploadMedia(m connector.MediaObject) (*MediaObject, error) {
|
|
||||||
// Return early if this is already a Matrix media object
|
|
||||||
if mxm, ok := m.(*MediaObject); ok {
|
|
||||||
return mxm, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := m.Read()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST",
|
|
||||||
mx.Server+"/_matrix/media/r0/upload?filename="+url.QueryEscape(m.Filename()),
|
|
||||||
reader)
|
|
||||||
req.Header.Add("Content-Type", m.Mimetype())
|
|
||||||
req.ContentLength = m.Size() // TODO: this wasn't specified as mandatory in the matrix client/server spec, do a PR to fix this
|
|
||||||
|
|
||||||
var resp UploadResponse
|
|
||||||
err = mx.DoAndParse(req, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mxc := strings.Split(strings.Replace(resp.ContentUri, "mxc://", "", 1), "/")
|
|
||||||
if len(mxc) != 2 {
|
|
||||||
return nil, fmt.Errorf("Invalid mxc:// returned: %s", resp.ContentUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &MediaObject{
|
|
||||||
mxClient: mx,
|
|
||||||
filename: m.Filename(),
|
|
||||||
size: m.Size(),
|
|
||||||
mimetype: m.Mimetype(),
|
|
||||||
imageSize: m.ImageSize(),
|
|
||||||
MxcServer: mxc[0],
|
|
||||||
MxcMediaId: mxc[1],
|
|
||||||
}
|
|
||||||
return media, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mx *Client) ParseMediaInfo(content map[string]interface{}) *MediaObject {
|
|
||||||
// Content is an event content of type m.file or m.image
|
|
||||||
info := content["info"].(map[string]interface{})
|
|
||||||
mxc := strings.Split(strings.Replace(content["url"].(string), "mxc://", "", 1), "/")
|
|
||||||
media := &MediaObject{
|
|
||||||
mxClient: mx,
|
|
||||||
filename: content["body"].(string),
|
|
||||||
size: int64(info["size"].(float64)),
|
|
||||||
mimetype: info["mimetype"].(string),
|
|
||||||
MxcServer: mxc[0],
|
|
||||||
MxcMediaId: mxc[1],
|
|
||||||
}
|
|
||||||
if content["msgtype"].(string) == "m.image" {
|
|
||||||
media.imageSize = &connector.ImageSize{
|
|
||||||
Width: int(info["w"].(float64)),
|
|
||||||
Height: int(info["h"].(float64)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return media
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package mxlib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MediaObject struct {
|
|
||||||
mxClient *Client
|
|
||||||
filename string
|
|
||||||
size int64
|
|
||||||
mimetype string
|
|
||||||
imageSize *connector.ImageSize
|
|
||||||
MxcServer string
|
|
||||||
MxcMediaId string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) Filename() string {
|
|
||||||
return m.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) Size() int64 {
|
|
||||||
return m.size
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) Mimetype() string {
|
|
||||||
return m.mimetype
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) ImageSize() *connector.ImageSize {
|
|
||||||
return m.imageSize
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) Read() (io.ReadCloser, error) {
|
|
||||||
req, err := http.NewRequest("GET", m.URL(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Authorization", "Bearer "+m.mxClient.Token)
|
|
||||||
|
|
||||||
resp, err := m.mxClient.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) URL() string {
|
|
||||||
return fmt.Sprintf("%s/_matrix/media/r0/download/%s/%s/%s",
|
|
||||||
m.mxClient.Server, m.MxcServer, m.MxcMediaId, url.QueryEscape(m.filename))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MediaObject) MxcUri() string {
|
|
||||||
return fmt.Sprintf("mxc://%s/%s", m.MxcServer, m.MxcMediaId)
|
|
||||||
}
|
|
|
@ -10,7 +10,6 @@ type Registration struct {
|
||||||
AsToken string `yaml:"as_token"`
|
AsToken string `yaml:"as_token"`
|
||||||
HsToken string `yaml:"hs_token"`
|
HsToken string `yaml:"hs_token"`
|
||||||
SenderLocalpart string `yaml:"sender_localpart"`
|
SenderLocalpart string `yaml:"sender_localpart"`
|
||||||
RateLimited bool `yaml:"rate_limited"`
|
|
||||||
Namespaces RegistrationNamespaceSet `yaml:"namespaces"`
|
Namespaces RegistrationNamespaceSet `yaml:"namespaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
362
server.go
362
server.go
|
@ -1,362 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var mx *mxlib.Client
|
|
||||||
|
|
||||||
func StartAppService(errch chan error, ctx context.Context) (*http.Server, error) {
|
|
||||||
mx = mxlib.NewClient(config.Server, registration.AsToken)
|
|
||||||
|
|
||||||
err := InitDb()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbKvGet("ezbr_initialized") != "yes" {
|
|
||||||
err = mx.RegisterUser(registration.SenderLocalpart)
|
|
||||||
if mxe, ok := err.(*mxlib.MxError); !ok || mxe.ErrCode != "M_USER_IN_USE" {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, st := os.Stat(config.AvatarFile)
|
|
||||||
if !os.IsNotExist(st) {
|
|
||||||
err = mx.ProfileAvatar(ezbrMxId(), &connector.FileMediaObject{
|
|
||||||
Path: config.AvatarFile,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mx.ProfileDisplayname(ezbrMxId(), fmt.Sprintf("Easybridge (%s)", EASYBRIDGE_SYSTEM_PROTOCOL))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dbKvPut("ezbr_initialized", "yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
router := mux.NewRouter()
|
|
||||||
router.HandleFunc("/_matrix/app/v1/transactions/{txnId}", handleTxn)
|
|
||||||
router.HandleFunc("/transactions/{txnId}", handleTxn)
|
|
||||||
|
|
||||||
log.Printf("Starting HTTP server on %s", config.ASBindAddr)
|
|
||||||
http_server := &http.Server{
|
|
||||||
Addr: config.ASBindAddr,
|
|
||||||
Handler: checkTokenAndLog(router),
|
|
||||||
BaseContext: func(net.Listener) context.Context {
|
|
||||||
return ctx
|
|
||||||
},
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
err := http_server.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
errch <- err
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Notify users that Easybridge has restarted
|
|
||||||
go func() {
|
|
||||||
var users []DbAccountConfig
|
|
||||||
db.Model(&DbAccountConfig{}).Select("mx_user_id").Group("mx_user_id").Find(&users)
|
|
||||||
for _, u := range users {
|
|
||||||
ezbrSystemSendf(u.MxUserID,
|
|
||||||
"Easybridge has restarted, please visit %s or open configuration widget to reconnect to your accounts.",
|
|
||||||
config.WebURL)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return http_server, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkTokenAndLog(handler http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r.ParseForm()
|
|
||||||
if strings.Join(r.Form["access_token"], "") != registration.HsToken {
|
|
||||||
http.Error(w, "Wrong or no token provided", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, strings.Split(r.URL.String(), "?")[0])
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleTxn(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == "PUT" {
|
|
||||||
var txn mxlib.Transaction
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&txn)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
log.Warnf("JSON decode error: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Processing transaction %s (%d events)", mux.Vars(r)["txnId"], len(txn.Events))
|
|
||||||
log.Tracef("Transaction content: %#v\n", txn)
|
|
||||||
|
|
||||||
for i := range txn.Events {
|
|
||||||
ev := &txn.Events[i]
|
|
||||||
if isBridgedIdentifier(ev.Sender) {
|
|
||||||
// Don't do anything with ezbr events that come back to us
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = handleTxnEvent(ev)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSend(ev.Sender, fmt.Sprintf("Could not process %s (from %s): %s", ev.Type, ev.Sender, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "{}\n")
|
|
||||||
} else {
|
|
||||||
http.Error(w, "Expected PUT request", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleTxnEvent(e *mxlib.Event) error {
|
|
||||||
if e.Type == "m.room.message" {
|
|
||||||
e_body, ok := e.Content["body"].(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Invalid m.room.message event, body is not defined: %#v", e)
|
|
||||||
}
|
|
||||||
typ, ok := e.Content["msgtype"].(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Invalid m.room.message event, msgtype is not defined: %#v", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
ev := &connector.Event{
|
|
||||||
Type: connector.EVENT_MESSAGE,
|
|
||||||
Text: e_body,
|
|
||||||
Id: e.EventId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if typ == "m.emote" {
|
|
||||||
ev.Type = connector.EVENT_MESSAGE
|
|
||||||
} else if typ == "m.file" || typ == "m.image" {
|
|
||||||
ev.Text = ""
|
|
||||||
ev.Attachments = []connector.SMediaObject{
|
|
||||||
connector.SMediaObject{mx.ParseMediaInfo(e.Content)},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
|
|
||||||
if pm_room.Protocol == EASYBRIDGE_SYSTEM_PROTOCOL {
|
|
||||||
handleSystemMessage(e.Sender, e_body)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If this is a private message room
|
|
||||||
acct := FindAccount(pm_room.MxUserID, pm_room.AccountName)
|
|
||||||
if acct == nil {
|
|
||||||
return fmt.Errorf("Not connected to %s", pm_room.AccountName)
|
|
||||||
} else if e.Sender == pm_room.MxUserID {
|
|
||||||
ev.Author = acct.Conn.User()
|
|
||||||
ev.Recipient = pm_room.UserID
|
|
||||||
_, err := acct.Conn.Send(ev)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if room := dbIsPublicRoom(e.RoomId); room != nil {
|
|
||||||
// If this is a regular room
|
|
||||||
acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID)
|
|
||||||
if acct == nil {
|
|
||||||
mx.RoomKick(e.RoomId, e.Sender, fmt.Sprintf("Not present in %s on %s, please talk with Easybridge to rejoin", room.RoomID, room.Protocol))
|
|
||||||
return fmt.Errorf("not joined %s on %s", room.RoomID, room.Protocol)
|
|
||||||
} else {
|
|
||||||
ev.Author = acct.Conn.User()
|
|
||||||
ev.Room = room.RoomID
|
|
||||||
|
|
||||||
// use room id as lock slot key, see account.go in eventInternal
|
|
||||||
dbLockSlot(e.RoomId)
|
|
||||||
defer dbUnlockSlot(e.RoomId)
|
|
||||||
|
|
||||||
created_ev_id, err := acct.Conn.Send(ev)
|
|
||||||
if err == nil && created_ev_id != "" {
|
|
||||||
cache_key := fmt.Sprintf("%s/event_seen/%s/%s",
|
|
||||||
room.Protocol, e.RoomId, created_ev_id)
|
|
||||||
dbKvPutLocked(cache_key, "yes")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Room not bridged")
|
|
||||||
}
|
|
||||||
} else if e.Type == "m.room.member" {
|
|
||||||
ms, ok := e.Content["membership"].(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Invalid m.room.member event, membership is not defined: %#v", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ms == "leave" {
|
|
||||||
if pm_room := dbIsPmRoom(e.RoomId); pm_room != nil {
|
|
||||||
// If user leaves a PM room, we must drop it
|
|
||||||
dbDeletePmRoom(pm_room)
|
|
||||||
them_mx, err := dbGetMxUser(pm_room.Protocol, pm_room.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mx.RoomLeaveAs(e.RoomId, them_mx)
|
|
||||||
return nil
|
|
||||||
} else if room := dbIsPublicRoom(e.RoomId); room != nil {
|
|
||||||
// If leaving a public room, leave from server as well
|
|
||||||
acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID)
|
|
||||||
if acct != nil {
|
|
||||||
acct.Conn.Leave(room.RoomID)
|
|
||||||
acct.delAutojoin(room.RoomID)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
mx.RoomKick(e.RoomId, e.Sender, fmt.Sprintf("Not present in %s on %s, please talk with Easybridge to rejoin", room.RoomID, room.Protocol))
|
|
||||||
return fmt.Errorf("not joined %s on %s", room.RoomID, room.Protocol)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Room not bridged")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if e.Type == "m.room.topic" {
|
|
||||||
e_topic, ok := e.Content["topic"].(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("Invalid m.room.topic event, topic is not defined: %#v", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if room := dbIsPublicRoom(e.RoomId); room != nil {
|
|
||||||
acct := FindJoinedAccount(e.Sender, room.Protocol, room.RoomID)
|
|
||||||
if acct != nil {
|
|
||||||
return acct.Conn.SetRoomInfo(room.RoomID, &connector.RoomInfo{
|
|
||||||
Topic: e_topic,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("Could not find room account for %s %s %s", e.Sender, room.Protocol, room.RoomID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSystemMessage(mxid string, msg string) {
|
|
||||||
cmd := strings.Fields(msg)
|
|
||||||
if len(cmd) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cmd[0] {
|
|
||||||
case "help":
|
|
||||||
ezbrSystemSend(mxid, "Welcome to Easybridge! Here is a list of available commands:")
|
|
||||||
ezbrSystemSend(mxid, "- help: request help")
|
|
||||||
ezbrSystemSend(mxid, "- list: list accounts")
|
|
||||||
ezbrSystemSend(mxid, "- accounts: list accounts")
|
|
||||||
ezbrSystemSend(mxid, "- join <protocol or account> <room id>: join public chat room")
|
|
||||||
ezbrSystemSend(mxid, "- talk <protocol or account> <user id>: open private conversation to contact")
|
|
||||||
ezbrSystemSend(mxid, "- search <protocol or account> <name>: search for users by name")
|
|
||||||
ezbrSystemSend(mxid, "- cmd <protocol or account> <command>: send special command to account")
|
|
||||||
case "list", "account", "accounts":
|
|
||||||
one := false
|
|
||||||
if accts, ok := registeredAccounts[mxid]; ok {
|
|
||||||
for name, acct := range accts {
|
|
||||||
one = true
|
|
||||||
ezbrSystemSendf(mxid, "- %s (%s)", name, acct.Protocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !one {
|
|
||||||
ezbrSystemSendf(mxid, "No account currently configured")
|
|
||||||
}
|
|
||||||
case "join":
|
|
||||||
if len(cmd) != 3 {
|
|
||||||
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <room id>", cmd[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account := findAccount(mxid, cmd[1])
|
|
||||||
if account != nil {
|
|
||||||
err := account.Conn.Join(connector.RoomID(cmd[2]))
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(mxid, "%s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
|
|
||||||
}
|
|
||||||
case "query", "talk":
|
|
||||||
if len(cmd) != 3 {
|
|
||||||
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <user id>", cmd[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account := findAccount(mxid, cmd[1])
|
|
||||||
if account != nil {
|
|
||||||
quser := connector.UserID(cmd[2])
|
|
||||||
err := account.Conn.Invite(quser, connector.RoomID(""))
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(mxid, "%s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
quser_mxid, err := dbGetMxUser(account.Protocol, quser)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(mxid, "%s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = dbGetMxPmRoom(account.Protocol, quser, quser_mxid, mxid, account.AccountName)
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(mxid, "%s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
|
|
||||||
}
|
|
||||||
case "search":
|
|
||||||
if len(cmd) < 3 {
|
|
||||||
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <name>", cmd[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account := findAccount(mxid, cmd[1])
|
|
||||||
if account != nil {
|
|
||||||
rep, err := account.Conn.SearchForUsers(strings.Join(cmd[2:], " "))
|
|
||||||
if err != nil {
|
|
||||||
ezbrSystemSendf(mxid, "Search error: %s", err)
|
|
||||||
} else {
|
|
||||||
ezbrSystemSendf(mxid, "%d users found", len(rep))
|
|
||||||
for _, user := range rep {
|
|
||||||
ezbrSystemSendf(mxid, "- %s (%s)", user.DisplayName, user.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
|
|
||||||
}
|
|
||||||
case "cmd":
|
|
||||||
if len(cmd) < 3 {
|
|
||||||
ezbrSystemSendf(mxid, "Usage: %s <protocol or account> <name>", cmd[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account := findAccount(mxid, cmd[1])
|
|
||||||
if account != nil {
|
|
||||||
account.Conn.UserCommand(strings.Join(cmd[2:], " "))
|
|
||||||
} else {
|
|
||||||
ezbrSystemSendf(mxid, "No account with name or using protocol %s", cmd[1])
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
ezbrSystemSend(mxid, "Unrecognized command. Type `help` if you need some help!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findAccount(mxid string, q string) *Account {
|
|
||||||
if accts, ok := registeredAccounts[mxid]; ok {
|
|
||||||
for name, acct := range accts {
|
|
||||||
if strings.EqualFold(name, q) || strings.EqualFold(acct.Protocol, q) {
|
|
||||||
return acct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
7
static/css/bootstrap.min.css
vendored
7
static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -1,77 +0,0 @@
|
||||||
{{define "title"}}Account configuration |{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<div class="d-flex">
|
|
||||||
<h4>Configure account</h4>
|
|
||||||
<a class="ml-auto btn btn-info" href="/">Go back</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .ErrorMessage}}
|
|
||||||
<div class="alert alert-danger mt-4">An error occurred.
|
|
||||||
<div style="font-size: 0.8em">{{ .ErrorMessage }}</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<form method="POST" class="mt-4">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Account name:</label>
|
|
||||||
<input type="text" {{if .NameEditable}}{{else}}disabled="disabled"{{end}} id="name" name="name" class="form-control" value="{{ .Name }}" />
|
|
||||||
{{if .InvalidName}}
|
|
||||||
<div class="alert alert-warning">Invalid name (must not be empty)</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Protocol:</label>
|
|
||||||
<input type="text" disabled="disabled" class="form-control" value="{{ .Protocol }}" />
|
|
||||||
</div>
|
|
||||||
{{$config := .Config}}
|
|
||||||
{{$errors := .Errors}}
|
|
||||||
{{range $i, $schema := .Schema}}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="{{$schema.Name}}">{{$schema.Description}}:</label>
|
|
||||||
{{if $schema.FixedValue}}
|
|
||||||
<input type="text"
|
|
||||||
disabled="disabled"
|
|
||||||
class="form-control"
|
|
||||||
name="{{$schema.Name}}"
|
|
||||||
id="{{$schema.Name}}"
|
|
||||||
value="{{index $config $schema.Name}}" />
|
|
||||||
{{else if $schema.IsBoolean}}
|
|
||||||
{{$value := index $config $schema.Name}}
|
|
||||||
<label for="{{$schema.Name}}-true">
|
|
||||||
<input type="radio" name="{{$schema.Name}}" id="{{$schema.Name}}-true" value="true" {{if eq $value "true"}}checked="checked"{{end}} />
|
|
||||||
Yes
|
|
||||||
</label>
|
|
||||||
<label for="{{$schema.Name}}-false">
|
|
||||||
<input type="radio" name="{{$schema.Name}}" id="{{$schema.Name}}-false" value="false" {{if eq $value "false"}}checked="checked"{{end}} />
|
|
||||||
No
|
|
||||||
</label>
|
|
||||||
{{else if $schema.IsPassword}}
|
|
||||||
<input type="password"
|
|
||||||
class="form-control"
|
|
||||||
name="{{$schema.Name}}"
|
|
||||||
id="{{$schema.Name}}"
|
|
||||||
placeholder="(not modified if left empty)" />
|
|
||||||
{{else if $schema.IsNumeric}}
|
|
||||||
<input type="number"
|
|
||||||
class="form-control"
|
|
||||||
name="{{$schema.Name}}"
|
|
||||||
id="{{$schema.Name}}"
|
|
||||||
value="{{index $config $schema.Name}}" />
|
|
||||||
{{else}}
|
|
||||||
<input type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="{{$schema.Name}}"
|
|
||||||
id="{{$schema.Name}}"
|
|
||||||
value="{{index $config $schema.Name}}" />
|
|
||||||
{{end}}
|
|
||||||
{{$error := index $errors $schema.Name}}
|
|
||||||
{{if $error}}
|
|
||||||
<div class="alert alert-warning mt-2">{{$error}}</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<button type="submit" class="btn btn-primary">Save configuration</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{end}}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{{define "title"}}Delete account |{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
|
|
||||||
<h4>Really delete account {{.}}?</h4>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<input type="submit" class="btn btn-danger" name="delete" value="Yes" />
|
|
||||||
<a href="/" class="btn btn-secondary ml-4" href="/">No, go back</a>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{end}}
|
|
|
@ -1,42 +0,0 @@
|
||||||
{{define "title"}}{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Logged in as <strong>{{ .Login.MxId }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex">
|
|
||||||
<a class="ml-auto btn btn-sm btn-dark" href="/logout">Log out</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ if .Accounts }}
|
|
||||||
<table class="table mt-4">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Account name</th>
|
|
||||||
<th>Protocol</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{range $i, $acc := .Accounts}}
|
|
||||||
<tr>
|
|
||||||
<td>{{ $acc.AccountName }}</td>
|
|
||||||
<td>{{ $acc.Protocol }}</td>
|
|
||||||
<td>
|
|
||||||
<a class="btn btn-sm btn-primary" href="/edit/{{ $acc.AccountName }}">Modify</a>
|
|
||||||
<a class="btn btn-sm btn-danger ml-4" href="/delete/{{ $acc.AccountName }}">Delete</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<h5 class="mt-4">Add account</h5>
|
|
||||||
|
|
||||||
<a class="btn btn-sm btn-dark mr-4" href="/add/IRC">IRC</a>
|
|
||||||
<a class="btn btn-sm btn-warning mr-4" href="/add/XMPP">XMPP</a>
|
|
||||||
<a class="btn btn-sm btn-info mr-4" href="/add/Mattermost">Mattermost</a>
|
|
||||||
<a class="btn btn-sm btn-primary mr-4" href="/add/Messenger">Facebook Messenger</a>
|
|
||||||
|
|
||||||
{{end}}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
|
||||||
|
|
||||||
<title>{{template "title"}} Easybridge</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Easybridge manager</h1>
|
|
||||||
<hr />
|
|
||||||
{{template "body" .}}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,30 +0,0 @@
|
||||||
{{define "title"}}{{end}}
|
|
||||||
|
|
||||||
{{define "body"}}
|
|
||||||
<h4>Log in</h4>
|
|
||||||
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Log in using your Matrix credentials on {{ .MatrixDomain }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
{{if .WrongPass}}
|
|
||||||
<div class="alert alert-danger">Wrong password.</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .ErrorMessage}}
|
|
||||||
<div class="alert alert-danger">Unable to log in.
|
|
||||||
<div style="font-size: 0.8em">{{ .ErrorMessage }}</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="username">Username:</label>
|
|
||||||
<input type="text" name="username" id="username" class="form-control" value="{{ .Username }}" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Password:</label>
|
|
||||||
<input type="password" name="password" id="password" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Log in</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{end}}
|
|
147
test/main.go
Normal file
147
test/main.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc"
|
||||||
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TmpHandler struct{
|
||||||
|
exit chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TmpHandler) Joined(roomId connector.RoomID) {
|
||||||
|
fmt.Printf("C Joined: %s\n", roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TmpHandler) Left(roomId connector.RoomID) {
|
||||||
|
fmt.Printf("C Joined: %s\n", roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TmpHandler) UserInfoUpdated(u connector.UserID, i *connector.UserInfo) {
|
||||||
|
fmt.Printf("C User info: %s => %#v\n", u, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TmpHandler) RoomInfoUpdated(r connector.RoomID, i *connector.RoomInfo) {
|
||||||
|
fmt.Printf("C Room info: %s => %#v\n", r, i)
|
||||||
|
}
|
||||||
|
func (h *TmpHandler) Event(e *connector.Event) {
|
||||||
|
if e.Type == connector.EVENT_JOIN {
|
||||||
|
fmt.Printf("C E Join %s %s\n", e.Author, e.Room)
|
||||||
|
} else if e.Type == connector.EVENT_LEAVE {
|
||||||
|
fmt.Printf("C E Leave %s %s\n", e.Author, e.Room)
|
||||||
|
} else if e.Type == connector.EVENT_MESSAGE {
|
||||||
|
fmt.Printf("C E Message %s %s %s\n", e.Author, e.Room, e.Text)
|
||||||
|
if strings.Contains(e.Text, "ezbrexit") {
|
||||||
|
fmt.Printf("we have to exit\n")
|
||||||
|
h.exit <- true
|
||||||
|
}
|
||||||
|
} else if e.Type == connector.EVENT_ACTION {
|
||||||
|
fmt.Printf("C E Action %s %s %s\n", e.Author, e.Room, e.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIrc() {
|
||||||
|
irc := &irc.IRC{}
|
||||||
|
h := TmpHandler{
|
||||||
|
exit: make(chan bool),
|
||||||
|
}
|
||||||
|
irc.SetHandler(&h)
|
||||||
|
|
||||||
|
err := irc.Configure(connector.Configuration{
|
||||||
|
"server": "irc.ulminfo.fr",
|
||||||
|
"port": "6666",
|
||||||
|
"ssl": "true",
|
||||||
|
"nick": "ezbr",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Connect: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = irc.Join(connector.RoomID("#ezbrtest@irc.ulminfo.fr"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Join: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(1)*time.Second)
|
||||||
|
err = irc.Send(&connector.Event{
|
||||||
|
Room: connector.RoomID("#ezbrtest@irc.ulminfo.fr"),
|
||||||
|
Type: connector.EVENT_MESSAGE,
|
||||||
|
Text: "EZBR TEST",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Send: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(1)*time.Second)
|
||||||
|
err = irc.Send(&connector.Event{
|
||||||
|
Recipient: connector.UserID("lx@irc.ulminfo.fr"),
|
||||||
|
Type: connector.EVENT_MESSAGE,
|
||||||
|
Text: "EZBR TEST direct message lol",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Send: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("waiting exit signal\n")
|
||||||
|
<-h.exit
|
||||||
|
fmt.Printf("got exit signal\n")
|
||||||
|
irc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testXmpp() {
|
||||||
|
xmpp := &xmpp.XMPP{}
|
||||||
|
h := TmpHandler{
|
||||||
|
exit: make(chan bool),
|
||||||
|
}
|
||||||
|
xmpp.SetHandler(&h)
|
||||||
|
|
||||||
|
err := xmpp.Configure(connector.Configuration{
|
||||||
|
"server": "jabber.fr",
|
||||||
|
"jid": "ezbr@jabber.fr",
|
||||||
|
"password": "azerty1234",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Connect: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = xmpp.Join(connector.RoomID("ezbrtest@muc.linkmauve.fr"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Join: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(1)*time.Second)
|
||||||
|
err = xmpp.Send(&connector.Event{
|
||||||
|
Room: connector.RoomID("ezbrtest@muc.linkmauve.fr"),
|
||||||
|
Type: connector.EVENT_MESSAGE,
|
||||||
|
Text: "EZBR TEST",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Send: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(1)*time.Second)
|
||||||
|
err = xmpp.Send(&connector.Event{
|
||||||
|
Recipient: connector.UserID("alexis211@jabber.fr"),
|
||||||
|
Type: connector.EVENT_MESSAGE,
|
||||||
|
Text: "EZBR TEST direct message lol",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Send: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("waiting exit signal\n")
|
||||||
|
<-h.exit
|
||||||
|
fmt.Printf("got exit signal\n")
|
||||||
|
xmpp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
testXmpp()
|
||||||
|
}
|
143
util.go
143
util.go
|
@ -1,143 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/crypto/nacl/secretbox"
|
|
||||||
|
|
||||||
. "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const EASYBRIDGE_SYSTEM_PROTOCOL string = "✯◡✯"
|
|
||||||
|
|
||||||
func ezbrMxId() string {
|
|
||||||
return fmt.Sprintf("@%s:%s", registration.SenderLocalpart, config.MatrixDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ezbrSystemRoom(user_mx_id string) (string, error) {
|
|
||||||
mx_room_id, err := dbGetMxPmRoom(EASYBRIDGE_SYSTEM_PROTOCOL, UserID("Easybridge"), ezbrMxId(), user_mx_id, "easybridge")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
widget_kv_key := "ezbr_widget_on:" + mx_room_id
|
|
||||||
if config.WebURL != "" && dbKvGet(widget_kv_key) != "yes" {
|
|
||||||
widget := map[string]interface{}{
|
|
||||||
"type": "m.easybridge",
|
|
||||||
"url": config.WebURL,
|
|
||||||
"name": "Easybridge account configuration dashboard",
|
|
||||||
}
|
|
||||||
err = mx.PutStateAs(mx_room_id, "im.vector.modular.widgets", "ezbr_widget", widget, ezbrMxId())
|
|
||||||
if err == nil {
|
|
||||||
dbKvPut(widget_kv_key, "yes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mx_room_id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ezbrSystemSend(user_mx_id string, msg string) {
|
|
||||||
mx_room_id, err := ezbrSystemRoom(user_mx_id)
|
|
||||||
if err == nil {
|
|
||||||
err = mx.SendMessageAs(mx_room_id, "m.text", msg, ezbrMxId())
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("(%s) %s", user_mx_id, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ezbrSystemSendf(user_mx_id string, format string, args ...interface{}) {
|
|
||||||
ezbrSystemSend(user_mx_id, fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func roomAlias(protocol string, id RoomID) string {
|
|
||||||
what := fmt.Sprintf("%s_%s", safeStringForId(string(id)), protocol)
|
|
||||||
return strings.Replace(config.NameFormat, "{}", what, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func userMxId(protocol string, id UserID) string {
|
|
||||||
what := fmt.Sprintf("%s_%s", safeStringForId(string(id)), protocol)
|
|
||||||
return strings.Replace(config.NameFormat, "{}", what, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func safeStringForId(in string) string {
|
|
||||||
id2 := ""
|
|
||||||
for _, c := range in {
|
|
||||||
if c == '@' {
|
|
||||||
id2 += "__"
|
|
||||||
} else if c == ':' {
|
|
||||||
id2 += "_"
|
|
||||||
} else if unicode.IsDigit(c) || unicode.IsLetter(c) || c == '.' || c == '-' || c == '_' {
|
|
||||||
id2 += string(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return id2
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBridgedIdentifier(mxid string) bool {
|
|
||||||
if mxid[0] == '@' || mxid[0] == '#' {
|
|
||||||
return isBridgedIdentifier(mxid[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(mxid, ":") {
|
|
||||||
sp := strings.Split(mxid, ":")
|
|
||||||
return (sp[1] == config.MatrixDomain) && isBridgedIdentifier(sp[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
nameformat_fixed_part := strings.Replace(config.NameFormat, "{}", "", 1)
|
|
||||||
if strings.HasPrefix(config.NameFormat, "{}") {
|
|
||||||
return strings.HasSuffix(mxid, nameformat_fixed_part)
|
|
||||||
} else if strings.HasSuffix(config.NameFormat, "{}") {
|
|
||||||
return strings.HasPrefix(mxid, nameformat_fixed_part)
|
|
||||||
} else {
|
|
||||||
// This is not supported
|
|
||||||
log.Fatalf("Invalid name format %s, please put {} at the beginning or at the end", config.NameFormat)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Encoding and encryption of account config
|
|
||||||
|
|
||||||
func encryptAccountConfig(config map[string]string, key *[32]byte) string {
|
|
||||||
bytes, err := json.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var nonce [24]byte
|
|
||||||
_, err = rand.Read(nonce[:])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
crypto := secretbox.Seal([]byte{}, bytes, &nonce, key)
|
|
||||||
all := append(nonce[:], crypto...)
|
|
||||||
return base64.StdEncoding.EncodeToString(all)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptAccountConfig(data string, key *[32]byte) (map[string]string, error) {
|
|
||||||
bytes, err := base64.StdEncoding.DecodeString(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var nonce [24]byte
|
|
||||||
copy(nonce[:], bytes[:24])
|
|
||||||
|
|
||||||
decoded, ok := secretbox.Open([]byte{}, bytes[24:], &nonce, key)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("Invalid key")
|
|
||||||
}
|
|
||||||
|
|
||||||
var config map[string]string
|
|
||||||
err = json.Unmarshal(decoded, &config)
|
|
||||||
return config, err
|
|
||||||
}
|
|
357
web.go
357
web.go
|
@ -1,357 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"html/template"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/crypto/argon2"
|
|
||||||
"golang.org/x/crypto/blake2b"
|
|
||||||
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
|
||||||
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
const SESSION_NAME = "easybridge_session"
|
|
||||||
|
|
||||||
var sessionsStore sessions.Store = nil
|
|
||||||
var userKeys = map[string]*[32]byte{}
|
|
||||||
|
|
||||||
func StartWeb(errch chan error, ctx context.Context) *http.Server {
|
|
||||||
session_key := blake2b.Sum256([]byte(config.SessionKey))
|
|
||||||
sessionsStore = sessions.NewCookieStore(session_key[:])
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/", handleHome)
|
|
||||||
r.HandleFunc("/logout", handleLogout)
|
|
||||||
r.HandleFunc("/add/{protocol}", handleAdd)
|
|
||||||
r.HandleFunc("/edit/{account}", handleEdit)
|
|
||||||
r.HandleFunc("/delete/{account}", handleDelete)
|
|
||||||
|
|
||||||
staticfiles := http.FileServer(http.Dir("static"))
|
|
||||||
r.Handle("/static/{file:.*}", http.StripPrefix("/static/", staticfiles))
|
|
||||||
|
|
||||||
log.Printf("Starting web UI HTTP server on %s", config.WebBindAddr)
|
|
||||||
web_server := &http.Server{
|
|
||||||
Addr: config.WebBindAddr,
|
|
||||||
Handler: logRequest(r),
|
|
||||||
BaseContext: func(net.Listener) context.Context {
|
|
||||||
return ctx
|
|
||||||
},
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
err := web_server.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
errch <- err
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return web_server
|
|
||||||
}
|
|
||||||
|
|
||||||
func logRequest(handler http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL)
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type LoginInfo struct {
|
|
||||||
MxId string
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
|
|
||||||
var login_info *LoginInfo
|
|
||||||
|
|
||||||
session, err := sessionsStore.Get(r, SESSION_NAME)
|
|
||||||
if err == nil {
|
|
||||||
mxid, ok := session.Values["login_mxid"].(string)
|
|
||||||
user_key, ok2 := session.Values["login_user_key"].([]byte)
|
|
||||||
if ok && ok2 {
|
|
||||||
if _, had_key := userKeys[mxid]; !had_key && len(user_key) == 32 {
|
|
||||||
key := new([32]byte)
|
|
||||||
copy(key[:], user_key)
|
|
||||||
userKeys[mxid] = key
|
|
||||||
LoadDbAccounts(mxid, key)
|
|
||||||
}
|
|
||||||
login_info = &LoginInfo{
|
|
||||||
MxId: mxid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if login_info == nil {
|
|
||||||
login_info = handleLogin(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return login_info
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
type HomeData struct {
|
|
||||||
Login *LoginInfo
|
|
||||||
Accounts []*Account
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleHome(w http.ResponseWriter, r *http.Request) {
|
|
||||||
templateHome := template.Must(template.ParseFiles("templates/layout.html", "templates/home.html"))
|
|
||||||
|
|
||||||
login := checkLogin(w, r)
|
|
||||||
if login == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
templateHome.Execute(w, &HomeData{
|
|
||||||
Login: login,
|
|
||||||
Accounts: ListAccounts(login.MxId),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session, err := sessionsStore.Get(r, SESSION_NAME)
|
|
||||||
if err != nil {
|
|
||||||
session, _ = sessionsStore.New(r, SESSION_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(session.Values, "login_mxid")
|
|
||||||
|
|
||||||
err = session.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginFormData struct {
|
|
||||||
Username string
|
|
||||||
WrongPass bool
|
|
||||||
ErrorMessage string
|
|
||||||
MatrixDomain string
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo {
|
|
||||||
templateLogin := template.Must(template.ParseFiles("templates/layout.html", "templates/login.html"))
|
|
||||||
|
|
||||||
data := &LoginFormData{
|
|
||||||
MatrixDomain: config.MatrixDomain,
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
templateLogin.Execute(w, data)
|
|
||||||
return nil
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
r.ParseForm()
|
|
||||||
|
|
||||||
username := strings.Join(r.Form["username"], "")
|
|
||||||
password := strings.Join(r.Form["password"], "")
|
|
||||||
|
|
||||||
cli := mxlib.NewClient(config.Server, "")
|
|
||||||
mxid, err := cli.PasswordLogin(username, password, "EZBRIDGE", "Easybridge")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
data.Username = username
|
|
||||||
data.ErrorMessage = err.Error()
|
|
||||||
templateLogin.Execute(w, data)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
key := new([32]byte)
|
|
||||||
key_slice := argon2.IDKey([]byte(password), []byte("EZBRIDGE account store"), 3, 64*1024, 4, 32)
|
|
||||||
copy(key[:], key_slice)
|
|
||||||
userKeys[mxid] = key
|
|
||||||
|
|
||||||
SaveDbAccounts(mxid, key)
|
|
||||||
LoadDbAccounts(mxid, key)
|
|
||||||
|
|
||||||
// Successfully logged in, save it to session
|
|
||||||
session, err := sessionsStore.Get(r, SESSION_NAME)
|
|
||||||
if err != nil {
|
|
||||||
session, _ = sessionsStore.New(r, SESSION_NAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Values["login_mxid"] = mxid
|
|
||||||
session.Values["login_user_key"] = key_slice
|
|
||||||
|
|
||||||
err = session.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &LoginInfo{
|
|
||||||
MxId: mxid,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, "Unsupported method", http.StatusBadRequest)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----
|
|
||||||
|
|
||||||
func handleAdd(w http.ResponseWriter, r *http.Request) {
|
|
||||||
login := checkLogin(w, r)
|
|
||||||
if login == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol := mux.Vars(r)["protocol"]
|
|
||||||
|
|
||||||
configForm(w, r, login, "", protocol, map[string]string{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleEdit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
login := checkLogin(w, r)
|
|
||||||
if login == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account := mux.Vars(r)["account"]
|
|
||||||
acct := FindAccount(login.MxId, account)
|
|
||||||
if acct == nil {
|
|
||||||
http.Error(w, "No such account", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configForm(w, r, login, account, acct.Protocol, acct.Config)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigFormData struct {
|
|
||||||
ErrorMessage string
|
|
||||||
|
|
||||||
Name string
|
|
||||||
NameEditable bool
|
|
||||||
InvalidName bool
|
|
||||||
|
|
||||||
Protocol string
|
|
||||||
|
|
||||||
Config map[string]string
|
|
||||||
Errors map[string]string
|
|
||||||
Schema connector.ConfigSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
func configForm(w http.ResponseWriter, r *http.Request,
|
|
||||||
login *LoginInfo, name string, protocol string,
|
|
||||||
prevConfig map[string]string) {
|
|
||||||
templateConfig := template.Must(template.ParseFiles("templates/layout.html", "templates/config.html"))
|
|
||||||
|
|
||||||
data := &ConfigFormData{
|
|
||||||
Name: name,
|
|
||||||
NameEditable: (name == ""),
|
|
||||||
Protocol: protocol,
|
|
||||||
Config: map[string]string{},
|
|
||||||
Errors: map[string]string{},
|
|
||||||
Schema: connector.Protocols[protocol].Schema,
|
|
||||||
}
|
|
||||||
for k, v := range prevConfig {
|
|
||||||
data.Config[k] = v
|
|
||||||
}
|
|
||||||
for _, sch := range data.Schema {
|
|
||||||
if _, ok := data.Config[sch.Name]; !ok && sch.Default != "" {
|
|
||||||
data.Config[sch.Name] = sch.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == "POST" {
|
|
||||||
ok := true
|
|
||||||
r.ParseForm()
|
|
||||||
|
|
||||||
if data.NameEditable {
|
|
||||||
data.Name = strings.Join(r.Form["name"], "")
|
|
||||||
if data.Name == "" {
|
|
||||||
ok = false
|
|
||||||
data.InvalidName = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, schema := range data.Schema {
|
|
||||||
field := schema.Name
|
|
||||||
old_value := data.Config[field]
|
|
||||||
data.Config[field] = strings.Join(r.Form[field], "")
|
|
||||||
if schema.IsPassword {
|
|
||||||
if data.Config[field] == "" {
|
|
||||||
data.Config[field] = old_value
|
|
||||||
}
|
|
||||||
} else if data.Config[field] == "" {
|
|
||||||
if schema.Required {
|
|
||||||
ok = false
|
|
||||||
data.Errors[field] = "This field is required"
|
|
||||||
}
|
|
||||||
} else if schema.FixedValue != "" {
|
|
||||||
if data.Config[field] != schema.FixedValue {
|
|
||||||
ok = false
|
|
||||||
data.Errors[field] = "This field must be equal to " + schema.FixedValue
|
|
||||||
}
|
|
||||||
} else if schema.IsBoolean {
|
|
||||||
if data.Config[field] != "false" && data.Config[field] != "true" {
|
|
||||||
ok = false
|
|
||||||
data.Errors[field] = "This field must be 'true' or 'false'"
|
|
||||||
}
|
|
||||||
} else if schema.IsNumeric {
|
|
||||||
_, err := strconv.Atoi(data.Config[field])
|
|
||||||
if err != nil {
|
|
||||||
ok = false
|
|
||||||
data.Errors[field] = "This field must be a valid number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
var entry DbAccountConfig
|
|
||||||
db.Where(&DbAccountConfig{
|
|
||||||
MxUserID: login.MxId,
|
|
||||||
Name: data.Name,
|
|
||||||
}).Assign(&DbAccountConfig{
|
|
||||||
Protocol: protocol,
|
|
||||||
Config: encryptAccountConfig(data.Config, userKeys[login.MxId]),
|
|
||||||
}).FirstOrCreate(&entry)
|
|
||||||
|
|
||||||
err := SetAccount(login.MxId, data.Name, protocol, data.Config)
|
|
||||||
if err == nil {
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data.ErrorMessage = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templateConfig.Execute(w, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDelete(w http.ResponseWriter, r *http.Request) {
|
|
||||||
templateDelete := template.Must(template.ParseFiles("templates/layout.html", "templates/delete.html"))
|
|
||||||
|
|
||||||
login := checkLogin(w, r)
|
|
||||||
if login == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
account := mux.Vars(r)["account"]
|
|
||||||
|
|
||||||
if r.Method == "POST" {
|
|
||||||
r.ParseForm()
|
|
||||||
del := strings.Join(r.Form["delete"], "")
|
|
||||||
if del == "Yes" {
|
|
||||||
RemoveAccount(login.MxId, account)
|
|
||||||
db.Where(&DbAccountConfig{
|
|
||||||
MxUserID: login.MxId,
|
|
||||||
Name: account,
|
|
||||||
}).Delete(&DbAccountConfig{})
|
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templateDelete.Execute(w, account)
|
|
||||||
}
|
|
Loading…
Reference in a new issue