From 22b090698e5320c20dea91ecdbaf50a43ecbab4d Mon Sep 17 00:00:00 2001 From: kelaresg <66099035+kelaresg@users.noreply.github.com> Date: Wed, 2 Sep 2020 11:52:34 +0800 Subject: [PATCH 001/109] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..60b684e --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# go-skype-bridge +This is a Matrix bridge for Skype using Go-lang. From de791615be88e8e92c598f5cf85692e2a6f1e613 Mon Sep 17 00:00:00 2001 From: kelaresg <66099035+kelaresg@users.noreply.github.com> Date: Wed, 2 Sep 2020 11:54:06 +0800 Subject: [PATCH 002/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60b684e..217919e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # go-skype-bridge -This is a Matrix bridge for Skype using Go-lang. +A Matrix-Skype puppeting bridge From 8bd0dab9d13a31b068623fa36786385efe94df3b Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 8 Sep 2020 12:02:17 +0800 Subject: [PATCH 003/109] first commit --- Dockerfile | 23 + Dockerfile.ci | 15 + LICENSE | 661 +++++ README.md | 3 +- build.sh | 2 + commands.go | 1211 +++++++++ community.go | 116 + config/bridge.go | 312 +++ config/config.go | 104 + config/registration.go | 67 + crypto.go | 226 ++ custompuppet.go | 257 ++ database/cryptostore.go | 438 +++ database/database.go | 81 + database/message.go | 141 + database/migrate.go | 152 ++ database/portal.go | 175 ++ database/puppet.go | 113 + database/statestore.go | 286 ++ .../upgrades/2018-09-01-initial-schema.go | 67 + .../2019-05-16-message-delete-cascade.go | 31 + .../2019-05-21-message-timestamp-column.go | 15 + .../2019-05-22-user-last-connection-column.go | 15 + database/upgrades/2019-05-23-protoupgrade.go | 61 + .../2019-05-23-puppet-custom-mxid-columns.go | 23 + .../upgrades/2019-05-28-user-portal-table.go | 19 + .../upgrades/2019-06-01-avatar-url-fields.go | 19 + .../2019-08-10-portal-in-community-field.go | 12 + .../2019-08-25-move-state-store-to-db.go | 141 + .../2019-11-10-full-member-state-store.go | 16 + .../2019-11-12-fix-room-topic-length.go | 16 + .../2020-05-09-add-portal-encrypted-field.go | 12 + database/upgrades/2020-05-09-crypto-store.go | 73 + ...2020-05-12-outbound-group-session-store.go | 25 + .../upgrades/2020-08-25-message-id-column.go | 15 + database/upgrades/upgrades.go | 101 + database/user.go | 230 ++ docker-run.sh | 31 + example-config.yaml | 217 ++ formatting.go | 170 ++ go.mod | 25 + go.sum | 375 +++ main.go | 400 +++ matrix.go | 331 +++ no-cgo.go | 22 + portal.go | 2411 +++++++++++++++++ provisioning.go | 345 +++ puppet.go | 323 +++ skype-ext/chat.go | 181 ++ skype-ext/cmd.go | 69 + skype-ext/jsonmessage.go | 104 + skype-ext/presence.go | 9 + skype-ext/skype.go | 304 +++ skype-ext/userid.go | 154 ++ test/main.go | 139 + types/types.go | 7 + user.go | 1158 ++++++++ webp.go | 14 + whatsapp-ext/call.go | 72 + whatsapp-ext/chat.go | 174 ++ whatsapp-ext/cmd.go | 69 + whatsapp-ext/conn.go | 65 + whatsapp-ext/jsonmessage.go | 105 + whatsapp-ext/msginfo.go | 95 + whatsapp-ext/presence.go | 67 + whatsapp-ext/props.go | 73 + whatsapp-ext/protomessage.go | 59 + whatsapp-ext/stream.go | 68 + whatsapp-ext/whatsapp.go | 206 ++ 69 files changed, 13114 insertions(+), 2 deletions(-) create mode 100644 Dockerfile create mode 100644 Dockerfile.ci create mode 100644 LICENSE create mode 100755 build.sh create mode 100644 commands.go create mode 100644 community.go create mode 100644 config/bridge.go create mode 100644 config/config.go create mode 100644 config/registration.go create mode 100644 crypto.go create mode 100644 custompuppet.go create mode 100644 database/cryptostore.go create mode 100644 database/database.go create mode 100644 database/message.go create mode 100644 database/migrate.go create mode 100644 database/portal.go create mode 100644 database/puppet.go create mode 100644 database/statestore.go create mode 100644 database/upgrades/2018-09-01-initial-schema.go create mode 100644 database/upgrades/2019-05-16-message-delete-cascade.go create mode 100644 database/upgrades/2019-05-21-message-timestamp-column.go create mode 100644 database/upgrades/2019-05-22-user-last-connection-column.go create mode 100644 database/upgrades/2019-05-23-protoupgrade.go create mode 100644 database/upgrades/2019-05-23-puppet-custom-mxid-columns.go create mode 100644 database/upgrades/2019-05-28-user-portal-table.go create mode 100644 database/upgrades/2019-06-01-avatar-url-fields.go create mode 100644 database/upgrades/2019-08-10-portal-in-community-field.go create mode 100644 database/upgrades/2019-08-25-move-state-store-to-db.go create mode 100644 database/upgrades/2019-11-10-full-member-state-store.go create mode 100644 database/upgrades/2019-11-12-fix-room-topic-length.go create mode 100644 database/upgrades/2020-05-09-add-portal-encrypted-field.go create mode 100644 database/upgrades/2020-05-09-crypto-store.go create mode 100644 database/upgrades/2020-05-12-outbound-group-session-store.go create mode 100644 database/upgrades/2020-08-25-message-id-column.go create mode 100644 database/upgrades/upgrades.go create mode 100644 database/user.go create mode 100755 docker-run.sh create mode 100644 example-config.yaml create mode 100644 formatting.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 matrix.go create mode 100644 no-cgo.go create mode 100644 portal.go create mode 100644 provisioning.go create mode 100644 puppet.go create mode 100644 skype-ext/chat.go create mode 100644 skype-ext/cmd.go create mode 100644 skype-ext/jsonmessage.go create mode 100644 skype-ext/presence.go create mode 100644 skype-ext/skype.go create mode 100644 skype-ext/userid.go create mode 100644 test/main.go create mode 100644 types/types.go create mode 100644 user.go create mode 100644 webp.go create mode 100644 whatsapp-ext/call.go create mode 100644 whatsapp-ext/chat.go create mode 100644 whatsapp-ext/cmd.go create mode 100644 whatsapp-ext/conn.go create mode 100644 whatsapp-ext/jsonmessage.go create mode 100644 whatsapp-ext/msginfo.go create mode 100644 whatsapp-ext/presence.go create mode 100644 whatsapp-ext/props.go create mode 100644 whatsapp-ext/protomessage.go create mode 100644 whatsapp-ext/stream.go create mode 100644 whatsapp-ext/whatsapp.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..19a1a06 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1-alpine AS builder + +RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories +RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev@edge_community + +COPY . /build +WORKDIR /build +RUN go build -o /usr/bin/matrix-skype + +FROM alpine:latest + +ENV UID=1337 \ + GID=1337 + +RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories +RUN apk add --no-cache su-exec ca-certificates olm@edge_community + +COPY --from=builder /usr/bin/matrix-skype /usr/bin/matrix-skype +COPY --from=builder /build/example-config.yaml /opt/matrix-skype/example-config.yaml +COPY --from=builder /build/docker-run.sh /docker-run.sh +VOLUME /data + +CMD ["/docker-run.sh"] diff --git a/Dockerfile.ci b/Dockerfile.ci new file mode 100644 index 0000000..8445754 --- /dev/null +++ b/Dockerfile.ci @@ -0,0 +1,15 @@ +FROM alpine:latest + +ENV UID=1337 \ + GID=1337 + +RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories +RUN apk add --no-cache su-exec ca-certificates olm@edge_community + +ARG EXECUTABLE=./matrix-skype +COPY $EXECUTABLE /usr/bin/matrix-skype +COPY ./example-config.yaml /opt/matrix-skype/example-config.yaml +COPY ./docker-run.sh /docker-run.sh +VOLUME /data + +CMD ["/docker-run.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/README.md b/README.md index 217919e..7708e80 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# go-skype-bridge -A Matrix-Skype puppeting bridge +# matrix-skype \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..2409c5b --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +go build -ldflags "-X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date '+%b %_d %Y, %H:%M:%S'`'" "$@" diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..92fcc0a --- /dev/null +++ b/commands.go @@ -0,0 +1,1211 @@ +package main + +import ( + "fmt" + skype "github.com/kelaresg/go-skypeapi" + "github.com/kelaresg/matrix-skype/database" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "math" + "time" + + //"math" + "sort" + "strconv" + "strings" + //"time" + + "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/whatsapp-ext" +) + +type CommandHandler struct { + bridge *Bridge + log maulogger.Logger +} + +// NewCommandHandler creates a CommandHandler +func NewCommandHandler(bridge *Bridge) *CommandHandler { + return &CommandHandler{ + bridge: bridge, + log: bridge.Log.Sub("Command handler"), + } +} + +// CommandEvent stores all data which might be used to handle commands +type CommandEvent struct { + Bot *appservice.IntentAPI + Bridge *Bridge + Handler *CommandHandler + RoomID id.RoomID + User *User + Command string + Args []string +} + +// Reply sends a reply to command as notice +func (ce *CommandEvent) Reply(msg string, args ...interface{}) { + content := format.RenderMarkdown(fmt.Sprintf(msg, args...), true, false) + content.MsgType = event.MsgNotice + room := ce.User.ManagementRoom + if len(room) == 0 { + room = ce.RoomID + } + _, err := ce.Bot.SendMessageEvent(room, event.EventMessage, content) + if err != nil { + ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err) + } +} + +// Handle handles messages to the bridge +func (handler *CommandHandler) Handle(roomID id.RoomID, user *User, message string) { + args := strings.Fields(message) + ce := &CommandEvent{ + Bot: handler.bridge.Bot, + Bridge: handler.bridge, + Handler: handler, + RoomID: roomID, + User: user, + Command: strings.ToLower(args[0]), + Args: args[1:], + } + handler.log.Debugfln("%s sent '%s' in %s", user.MXID, message, roomID) + if roomID == handler.bridge.Config.Bridge.Relaybot.ManagementRoom { + handler.CommandRelaybot(ce) + } else { + handler.CommandMux(ce) + } +} + +func (handler *CommandHandler) CommandMux(ce *CommandEvent) { + switch ce.Command { + case "relaybot": + handler.CommandRelaybot(ce) + case "login": + handler.CommandLogin(ce) + //case "logout-matrix": + // handler.CommandLogoutMatrix(ce) + case "help": + handler.CommandHelp(ce) + //case "version": + // handler.CommandVersion(ce) + //case "reconnect": + // handler.CommandReconnect(ce) + //case "disconnect": + // handler.CommandDisconnect(ce) + //case "ping": + // handler.CommandPing(ce) + //case "delete-connection": + // handler.CommandDeleteConnection(ce) + //case "delete-session": + // handler.CommandDeleteSession(ce) + //case "delete-portal": + // handler.CommandDeletePortal(ce) + //case "delete-all-portals": + // handler.CommandDeleteAllPortals(ce) + //case "dev-test": + // handler.CommandDevTest(ce) + //case "set-pl": + // handler.CommandSetPowerLevel(ce) + //case "logout": + // handler.CommandLogout(ce) + case "login-matrix", "sync", "list", "open", "pm", "invite", "kick", "leave", "join", "create", "share": + if !ce.User.HasSession() { + ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") + return + } + + switch ce.Command { + //case "login-matrix": + // handler.CommandLoginMatrix(ce) + case "sync": + handler.CommandSync(ce) + case "list": + handler.CommandList(ce) + case "open": + handler.CommandOpen(ce) + case "pm": + handler.CommandPM(ce) + case "invite": + handler.CommandInvite(ce) + case "kick": + handler.CommandKick(ce) + case "leave": + handler.CommandLeave(ce) + case "join": + handler.CommandJoin(ce) + case "share": + handler.CommandShare(ce) + case "create": + handler.CommandCreate(ce) + } + default: + ce.Reply("Unknown Command") + } +} + +func (handler *CommandHandler) CommandRelaybot(ce *CommandEvent) { + if handler.bridge.Relaybot == nil { + ce.Reply("The relaybot is disabled") + } else if !ce.User.Admin { + ce.Reply("Only admins can manage the relaybot") + } else { + if ce.Command == "relaybot" { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `relaybot `") + return + } + ce.Command = strings.ToLower(ce.Args[0]) + ce.Args = ce.Args[1:] + } + ce.User = handler.bridge.Relaybot + handler.CommandMux(ce) + } +} + +func (handler *CommandHandler) CommandDevTest(_ *CommandEvent) { + +} + +const cmdVersionHelp = `version - View the bridge version` + +func (handler *CommandHandler) CommandVersion(ce *CommandEvent) { + version := fmt.Sprintf("v%s.unknown", Version) + if Tag == Version { + version = fmt.Sprintf("[v%s](%s/releases/v%s) (%s)", Version, URL, Tag, BuildTime) + } else if len(Commit) > 8 { + version = fmt.Sprintf("v%s.[%s](%s/commit/%s) (%s)", Version, Commit[:8], URL, Commit, BuildTime) + } + ce.Reply(fmt.Sprintf("[%s](%s) %s", Name, URL, version)) +} + +const cmdSetPowerLevelHelp = `set-pl [user ID] - Change the power level in a portal room. Only for bridge admins.` + +func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) { + portal := ce.Bridge.GetPortalByMXID(ce.RoomID) + if portal == nil { + ce.Reply("Not a portal room") + return + } + var level int + var userID id.UserID + var err error + if len(ce.Args) == 1 { + level, err = strconv.Atoi(ce.Args[0]) + if err != nil { + ce.Reply("Invalid power level \"%s\"", ce.Args[0]) + return + } + userID = ce.User.MXID + } else if len(ce.Args) == 2 { + userID = id.UserID(ce.Args[0]) + _, _, err := userID.Parse() + if err != nil { + ce.Reply("Invalid user ID \"%s\"", ce.Args[0]) + return + } + level, err = strconv.Atoi(ce.Args[1]) + if err != nil { + ce.Reply("Invalid power level \"%s\"", ce.Args[1]) + return + } + } else { + ce.Reply("**Usage:** `set-pl [user] `") + return + } + intent := portal.MainIntent() + _, err = intent.SetPowerLevel(ce.RoomID, userID, level) + if err != nil { + ce.Reply("Failed to set power levels: %v", err) + } +} + +const cmdLoginHelp = `login - login <_username_> <_password_>` + +// CommandLogin handles login command +func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `login username password`") + return + } + + if !ce.User.Connect(true) { + ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.") + return + } + ce.User.Login(ce, ce.Args[0], ce.Args[1]) + syncAll(ce.User, true) +} + +const cmdLogoutHelp = `logout - Logout from WhatsApp` + +// CommandLogout handles !logout command +//func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { +// if ce.User.Session == nil { +// ce.Reply("You're not logged in.") +// return +// } else if !ce.User.IsConnected() { +// ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information.") +// return +// } +// puppet := handler.bridge.GetPuppetByJID(ce.User.JID) +// if puppet.CustomMXID != "" { +// err := puppet.SwitchCustomMXID("", "") +// if err != nil { +// ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err) +// } +// } +// err := ce.User.Conn.Logout() +// if err != nil { +// ce.User.log.Warnln("Error while logging out:", err) +// ce.Reply("Unknown error while logging out: %v", err) +// return +// } +// _, err = ce.User.Conn.Disconnect() +// if err != nil { +// ce.User.log.Warnln("Error while disconnecting after logout:", err) +// } +// ce.User.Conn.RemoveHandlers() +// ce.User.Conn = nil +// ce.User.removeFromJIDMap() +// // TODO this causes a foreign key violation, which should be fixed +// //ce.User.JID = "" +// ce.User.SetSession(nil) +// ce.Reply("Logged out successfully.") +//} + +const cmdDeleteSessionHelp = `delete-session - Delete session information and disconnect from WhatsApp without sending a logout request` + +//func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) { +// if ce.User.Session == nil && ce.User.Conn == nil { +// ce.Reply("Nothing to purge: no session information stored and no active connection.") +// return +// } +// ce.User.SetSession(nil) +// if ce.User.Conn != nil { +// _, _ = ce.User.Conn.Disconnect() +// ce.User.Conn.RemoveHandlers() +// ce.User.Conn = nil +// } +// ce.Reply("Session information purged") +//} + +const cmdReconnectHelp = `reconnect - Reconnect to WhatsApp` + +//func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { +// if ce.User.Conn == nil { +// if ce.User.Session == nil { +// ce.Reply("No existing connection and no session. Did you mean `login`?") +// } else { +// ce.Reply("No existing connection, creating one...") +// ce.User.Connect(false) +// } +// return +// } +// +// wasConnected := true +// sess, err := ce.User.Conn.Disconnect() +// if err == whatsapp.ErrNotConnected { +// wasConnected = false +// } else if err != nil { +// ce.User.log.Warnln("Error while disconnecting:", err) +// } else if len(sess.Wid) > 0 { +// ce.User.SetSession(&sess) +// } +// +// err = ce.User.Conn.Restore() +// if err == whatsapp.ErrInvalidSession { +// if ce.User.Session != nil { +// ce.User.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...") +// var sess whatsapp.Session +// sess, err = ce.User.Conn.RestoreWithSession(*ce.User.Session) +// if err == nil { +// ce.User.SetSession(&sess) +// } +// } else { +// ce.Reply("You are not logged in.") +// return +// } +// } else if err == whatsapp.ErrLoginInProgress { +// ce.Reply("A login or reconnection is already in progress.") +// return +// } else if err == whatsapp.ErrAlreadyLoggedIn { +// ce.Reply("You were already connected.") +// return +// } +// if err != nil { +// ce.User.log.Warnln("Error while reconnecting:", err) +// if err.Error() == "restore session connection timed out" { +// ce.Reply("Reconnection timed out. Is WhatsApp on your phone reachable?") +// } else { +// ce.Reply("Unknown error while reconnecting: %v", err) +// } +// ce.User.log.Debugln("Disconnecting due to failed session restore in reconnect command...") +// sess, err := ce.User.Conn.Disconnect() +// if err != nil { +// ce.User.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err) +// } else if len(sess.Wid) > 0 { +// ce.User.SetSession(&sess) +// } +// return +// } +// ce.User.ConnectionErrors = 0 +// +// var msg string +// if wasConnected { +// msg = "Reconnected successfully." +// } else { +// msg = "Connected successfully." +// } +// ce.Reply(msg) +// ce.User.PostLogin() +//} + +const cmdDeleteConnectionHelp = `delete-connection - Disconnect ignoring errors and delete internal connection state.` + +//func (handler *CommandHandler) CommandDeleteConnection(ce *CommandEvent) { +// if ce.User.Conn == nil { +// ce.Reply("You don't have a WhatsApp connection.") +// return +// } +// sess, err := ce.User.Conn.Disconnect() +// if err == nil && len(sess.Wid) > 0 { +// ce.User.SetSession(&sess) +// } +// ce.User.Conn.RemoveHandlers() +// ce.User.Conn = nil +// ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.") +//} + +const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without logging out)` + +//func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) { +// if ce.User.Conn == nil { +// ce.Reply("You don't have a WhatsApp connection.") +// return +// } +// sess, err := ce.User.Conn.Disconnect() +// if err == whatsapp.ErrNotConnected { +// ce.Reply("You were not connected.") +// return +// } else if err != nil { +// ce.User.log.Warnln("Error while disconnecting:", err) +// ce.Reply("Unknown error while disconnecting: %v", err) +// return +// } else if len(sess.Wid) > 0 { +// ce.User.SetSession(&sess) +// } +// ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.") +//} + +const cmdPingHelp = `ping - Check your connection to WhatsApp.` + +//func (handler *CommandHandler) CommandPing(ce *CommandEvent) { +// if ce.User.Session == nil { +// if ce.User.IsLoginInProgress() { +// ce.Reply("You're not logged into WhatsApp, but there's a login in progress.") +// } else { +// ce.Reply("You're not logged into WhatsApp.") +// } +// } else if ce.User.Conn == nil { +// ce.Reply("You don't have a WhatsApp connection.") +// } else if ok, err := ce.User.Conn.AdminTest(); err != nil { +// if ce.User.IsLoginInProgress() { +// ce.Reply("Connection not OK: %v, but login in progress", err) +// } else { +// ce.Reply("Connection not OK: %v", err) +// } +// } else if !ok { +// if ce.User.IsLoginInProgress() { +// ce.Reply("Connection not OK, but no error received and login in progress") +// } else { +// ce.Reply("Connection not OK, but no error received") +// } +// } else { +// ce.Reply("Connection to WhatsApp OK") +// } +//} + +const cmdHelpHelp = `help - Prints this help` + +// CommandHelp handles help command +func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { + cmdPrefix := "" + if ce.User.ManagementRoom != ce.RoomID || ce.User.IsRelaybot { + cmdPrefix = handler.bridge.Config.Bridge.CommandPrefix + " " + } + + ce.Reply("* " + strings.Join([]string{ + cmdPrefix + cmdHelpHelp, + cmdPrefix + cmdLoginHelp, + //cmdPrefix + cmdLogoutHelp, + //cmdPrefix + cmdDeleteSessionHelp, + //cmdPrefix + cmdReconnectHelp, + //cmdPrefix + cmdDisconnectHelp, + //cmdPrefix + cmdDeleteConnectionHelp, + //cmdPrefix + cmdPingHelp, + //cmdPrefix + cmdLoginMatrixHelp, + //cmdPrefix + cmdLogoutMatrixHelp, + cmdPrefix + cmdSyncHelp, + cmdPrefix + cmdListHelp, + cmdPrefix + cmdOpenHelp, + cmdPrefix + cmdPMHelp, + //cmdPrefix + cmdSetPowerLevelHelp, + //cmdPrefix + cmdDeletePortalHelp, + //cmdPrefix + cmdDeleteAllPortalsHelp, + cmdPrefix + cmdCreateHelp, + cmdPrefix + cmdInviteHelp, + cmdPrefix + cmdKickHelp, + cmdPrefix + cmdLeaveHelp, + cmdPrefix + cmdJoinHelp, + cmdPrefix + cmdShareHelp, + + }, "\n* ")) +} + +const cmdSyncHelp = `sync - Synchronize contacts and optionally create portals for group chats.` + +func (handler *CommandHandler) CommandSync(ce *CommandEvent) { + user := ce.User + create := len(ce.Args) > 0 && ce.Args[0] == "--create-all" + ce.Reply("Updating contact and chat list...") + handler.log.Debugln("Importing contacts of", user.MXID) + + ce.Reply("Syncing contacts...") + err := user.Conn.Conn.ContactList(ce.User.Conn.UserProfile.Username) + if err != nil { + user.log.Errorln("Error get contacts:", err) + ce.Reply("Failed to contacts chat list (see logs for details)") + } + ce.Reply("Syncing conversations...") + err = ce.User.Conn.GetConversations("") + if err != nil { + user.log.Errorln("Error get conversations:", err) + ce.Reply("Failed to conversations list (see logs for details)") + } + handler.log.Debugln("Importing chats of", user.MXID) + syncAll(user, create) + + ce.Reply("Sync complete.") +} + +func syncAll(user *User, create bool) { + //ce.Reply("Syncing contacts...") + user.syncPuppets(nil) + //ce.Reply("Syncing chats...") + user.syncPortals(nil, create) + //sync information from non-contacts in the conversation, + syncNonContactInfo(user) +} + +func syncNonContactInfo(user *User) { + nonContacts := map[string]skype.Contact{} + for personId, contact := range user.Conn.Store.Contacts { + if contact.PersonId == "" { + nonContacts[personId] = contact + } + } + user.syncPuppets(nonContacts) +} + +const cmdDeletePortalHelp = `delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins.` + +func (handler *CommandHandler) CommandDeletePortal(ce *CommandEvent) { + portal := ce.Bridge.GetPortalByMXID(ce.RoomID) + if portal == nil { + ce.Reply("You must be in a portal room to use that command") + return + } + + if !ce.User.Admin { + users := portal.GetUserIDs() + if len(users) > 1 || (len(users) == 1 && users[0] != ce.User.MXID) { + ce.Reply("Only bridge admins can delete portals with other Matrix users") + return + } + } + + portal.log.Infoln(ce.User.MXID, "requested deletion of portal.") + portal.Delete() + portal.Cleanup(false) +} + +const cmdDeleteAllPortalsHelp = `delete-all-portals - Delete all your portals that aren't used by any other user.'` + +func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) { + portals := ce.User.GetPortals() + portalsToDelete := make([]*Portal, 0, len(portals)) + for _, portal := range portals { + users := portal.GetUserIDs() + if len(users) == 1 && users[0] == ce.User.MXID { + portalsToDelete = append(portalsToDelete, portal) + } + } + leave := func(portal *Portal) { + if len(portal.MXID) > 0 { + _, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ + Reason: "Deleting portal", + UserID: ce.User.MXID, + }) + } + } + customPuppet := handler.bridge.GetPuppetByCustomMXID(ce.User.MXID) + if customPuppet != nil && customPuppet.CustomIntent() != nil { + intent := customPuppet.CustomIntent() + leave = func(portal *Portal) { + if len(portal.MXID) > 0 { + _, _ = intent.LeaveRoom(portal.MXID) + _, _ = intent.ForgetRoom(portal.MXID) + } + } + } + ce.Reply("Found %d portals with no other users, deleting...", len(portalsToDelete)) + for _, portal := range portalsToDelete { + portal.Delete() + leave(portal) + } + ce.Reply("Finished deleting portal info. Now cleaning up rooms in background. " + + "You may already continue using the bridge. Use `sync` to recreate portals.") + + go func() { + for _, portal := range portalsToDelete { + portal.Cleanup(false) + } + ce.Reply("Finished background cleanup of deleted portal rooms.") + }() +} + +const cmdListHelp = `list <_contacts|groups_> [page] [items per page] - Get a list of all contacts and groups.` + +func formatLists(isContacts bool, contacts map[string]skype.Contact, conversations map[string]skype.Conversation) (result []string) { + if isContacts { + for _, contact := range contacts { + result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.DisplayName, contact.DisplayNameSource, strings.Replace(contact.PersonId, skypeExt.NewUserSuffix, "", 1))) + } + sort.Sort(sort.StringSlice(result)) + } else { + for _, conversation := range conversations { + if strings.HasPrefix(fmt.Sprint(conversation.Id), "19:") { + result = append(result, fmt.Sprintf("* %s - `%s`", conversation.ThreadProperties.Topic, conversation.Id)) + } + } + sort.Sort(sort.StringSlice(result)) + } + return +} + +func (handler *CommandHandler) CommandList(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `list [page] [items per page]`") + return + } + mode := strings.ToLower(ce.Args[0]) + if mode[0] != 'g' && mode[0] != 'c' { + ce.Reply("**Usage:** `list [page] [items per page]`") + return + } + + var err error + page := 1 + max := 100 + + isContact := mode[0] == 'c' + typeName := "Groups" + if isContact { + typeName = "Contacts" + err = ce.User.Conn.ContactList(ce.User.Conn.UserProfile.Username) + if err != nil { + ce.Reply("Get Contacts error") + return + } + } else { + err = ce.User.Conn.GetConversations("") + if err != nil { + ce.Reply("Get conversations error") + return + } + } + + result := formatLists(isContact, ce.User.Conn.Store.Contacts, ce.User.Conn.Store.Chats) + if len(result) == 0 { + ce.Reply("No %s found", strings.ToLower(typeName)) + return + } + pages := int(math.Ceil(float64(len(result)) / float64(max))) + if (page-1)*max >= len(result) { + if pages == 1 { + ce.Reply("There is only 1 page of %s", strings.ToLower(typeName)) + } else { + ce.Reply("There are only %d pages of %s", pages, strings.ToLower(typeName)) + } + return + } + lastIndex := page * max + if lastIndex > len(result) { + lastIndex = len(result) + } + result = result[(page-1)*max : lastIndex] + ce.Reply("### %s (page %d of %d)\n\n%s", typeName, page, pages, strings.Join(result, "\n")) +} + +const cmdOpenHelp = `open <_group ID_> - Open a group chat portal.` + +func (handler *CommandHandler) CommandOpen(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `open `") + return + } + + user := ce.User + jid := ce.Args[0] + + if strings.HasSuffix(jid, skypeExt.NewUserSuffix) { + ce.Reply("That looks like a user ID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsappExt.NewUserSuffix)]) + return + } + ce.User.Conn.GetConversations("") + fmt.Println("user.Conn.Store.Chats: ", user.Conn.Store.Chats) + chat, ok := user.Conn.Store.Chats[jid] + if !ok { + ce.Reply("Group ID not found in contacts. Try syncing contacts with `sync` first.") + return + } + handler.log.Debugln("Importing", jid, "for", user) + portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid)) + fmt.Println("CommandOpen portal.MXID", portal.MXID) + if len(portal.MXID) > 0 { + portal.SyncSkype(user, chat) + ce.Reply("Portal room synced.") + } else { + portal.SyncSkype(user, chat) + ce.Reply("Portal room created.") + } + _, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID}) + + //resp, err := ce.User.Conn.GetConsumptionHorizons(jid) + //if err != nil { + // return + //} + //// var existIds []string + //for _, con := range resp.ConsumptionHorizons { + // // existIds = append(existIds, strings.Replace(userId, skypeExt.NewUserSuffix, "", 1)) + // has := false + // uId := "" + // for userId, _ := range ce.User.Conn.Store.Contacts { + // if con.Id == strings.Replace(userId, skypeExt.NewUserSuffix, "", 1) { + // has = true + // } + // uId = con.Id + // } + // if user.JID == con.Id + skypeExt.NewUserSuffix { + // continue + // } + // if !has && uId != "" { + // fmt.Println(fmt.Sprintf("https://avatar.skype.com/v1/avatars/%s/public?returnDefaultImage=false", uId)) + // fmt.Println() + // newId := strings.Replace(con.Id, "8:", "", 1) + // avatar := &skypeExt.ProfilePicInfo{ + // URL: fmt.Sprintf("https://avatar.skype.com/v1/avatars/%s/public?returnDefaultImage=false", newId), + // Tag: fmt.Sprintf("https://avatar.skype.com/v1/avatars/%s/public?returnDefaultImage=false", newId), + // Status: 0, + // } + // puppet := user.bridge.GetPuppetByJID(con.Id + skypeExt.NewUserSuffix) + // if puppet.Avatar != avatar.URL { + // puppet.UpdateAvatar(nil, avatar) + // _, err = ce.User.Conn.NameSearch(newId) + // if err != nil { + // ce.Reply("Failed to synchronize non-contact %s info", newId) + // } + // } + // } + //} + syncNonContactInfo(ce.User) +} + +//func (handler *CommandHandler) CommandOpen(ce *CommandEvent) { +// if len(ce.Args) == 0 { +// ce.Reply("**Usage:** `open `") +// return +// } +// +// user := ce.User +// jid := ce.Args[0] +// +// if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) { +// ce.Reply("That looks like a user JID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsappExt.NewUserSuffix)]) +// return +// } +// +// contact, ok := user.Conn.Store.Contacts[jid] +// if !ok { +// ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.") +// return +// } +// handler.log.Debugln("Importing", jid, "for", user) +// portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid)) +// if len(portal.MXID) > 0 { +// portal.Sync(user, contact) +// ce.Reply("Portal room synced.") +// } else { +// portal.Sync(user, contact) +// ce.Reply("Portal room created.") +// } +// _, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID}) +//} + +const cmdPMHelp = `pm <_user ID_> - Open a private chat with the given user id.` + +func (handler *CommandHandler) CommandPM(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `pm `") + return + } + jid := ce.Args[0] + skypeExt.NewUserSuffix + + handler.log.Debugln("Importing", jid, "for", ce.User) + + contact, ok := ce.User.Conn.Store.Contacts[jid] + if !ok { + //if !force { + ce.Reply("User id not found in contacts. Try syncing contacts with `sync` first. ") + return + //} + //contact = skype.Contact{PersonId: jid} + } + + puppet := ce.User.bridge.GetPuppetByJID(contact.PersonId) + puppet.Sync(ce.User, contact) + portal := ce.User.bridge.GetPortalByJID(database.NewPortalKey(ce.Args[0], ce.User.JID)) + fmt.Println("CommandPM user.JID", ce.User.JID) + if len(portal.MXID) > 0 { + _, err := portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: ce.User.MXID}) + if err != nil { + fmt.Println(err) + } else { + ce.Reply("Existing portal room found, invited you to it.") + } + return + } + err := portal.CreateMatrixRoom(ce.User) + if err != nil { + ce.Reply("Failed to create portal room: %v", err) + return + } + ce.Reply("Created portal room and invited you to it.") +} + +//func (handler *CommandHandler) CommandPM(ce *CommandEvent) { +// if len(ce.Args) == 0 { +// ce.Reply("**Usage:** `pm [--force] `") +// return +// } +// +// force := ce.Args[0] == "--force" +// if force { +// ce.Args = ce.Args[1:] +// } +// +// user := ce.User +// +// number := strings.Join(ce.Args, "") +// if number[0] == '+' { +// number = number[1:] +// } +// for _, char := range number { +// if char < '0' || char > '9' { +// ce.Reply("Invalid phone number.") +// return +// } +// } +// jid := number + whatsappExt.NewUserSuffix +// +// handler.log.Debugln("Importing", jid, "for", user) +// +// contact, ok := user.Conn.Store.Contacts[jid] +// if !ok { +// if !force { +// ce.Reply("Phone number not found in contacts. Try syncing contacts with `sync` first. " + +// "To create a portal anyway, use `pm --force `.") +// return +// } +// contact = whatsapp.Contact{Jid: jid} +// } +// puppet := user.bridge.GetPuppetByJID(contact.Jid) +// puppet.Sync(user, contact) +// portal := user.bridge.GetPortalByJID(database.NewPortalKey(contact.Jid, user.JID)) +// if len(portal.MXID) > 0 { +// _, err := portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: user.MXID}) +// if err != nil { +// fmt.Println(err) +// } else { +// ce.Reply("Existing portal room found, invited you to it.") +// } +// return +// } +// err := portal.CreateMatrixRoom(user) +// if err != nil { +// ce.Reply("Failed to create portal room: %v", err) +// return +// } +// ce.Reply("Created portal room and invited you to it.") +//} + +const cmdLoginMatrixHelp = `login-matrix <_access token_> - Replace your WhatsApp account's Matrix puppet with your real Matrix account.'` + +func (handler *CommandHandler) CommandLoginMatrix(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `login-matrix `") + return + } + puppet := handler.bridge.GetPuppetByJID(ce.User.JID) + err := puppet.SwitchCustomMXID(ce.Args[0], ce.User.MXID) + if err != nil { + ce.Reply("Failed to switch puppet: %v", err) + return + } + ce.Reply("Successfully switched puppet") +} + +const cmdLogoutMatrixHelp = `logout-matrix - Switch your WhatsApp account's Matrix puppet back to the default one.` + +func (handler *CommandHandler) CommandLogoutMatrix(ce *CommandEvent) { + puppet := handler.bridge.GetPuppetByJID(ce.User.JID) + if len(puppet.CustomMXID) == 0 { + ce.Reply("You had not changed your WhatsApp account's Matrix puppet.") + return + } + err := puppet.SwitchCustomMXID("", "") + if err != nil { + ce.Reply("Failed to remove custom puppet: %v", err) + return + } + ce.Reply("Successfully removed custom puppet") +} + +const cmdInviteHelp = `invite <_group ID_> <_contact id_>,... - Invite members to a group.` + +func (handler *CommandHandler) CommandInvite(ce *CommandEvent) { + if len(ce.Args) < 2 { + ce.Reply("**Usage:** `invite ,...`") + return + } + user := ce.User + conversationId := ce.Args[0] + + userNumbers := strings.Split(ce.Args[1], ",") + + if strings.HasSuffix(conversationId, whatsappExt.NewUserSuffix) { + ce.Reply("**Usage:** `invite ,...`") + return + } + + for i, number := range userNumbers { + // + number = strings.Replace(number, "8:", "", 1) + userNumbers[i] = number // + whatsappExt.NewUserSuffix + } + fmt.Println("sign in invite function") + fmt.Printf("%+v \n", user) + fmt.Printf("%+v \n", userNumbers) + fmt.Printf("%+v \n", conversationId) + //jidStr := strings.Split(jid, "@s.skype.net") + //fmt.Println(jidStr) + //jid = jidStr[0] + //contact, ok := user.Conn.Store.Contacts[jid] + group, ok := user.Conn.Store.Chats[conversationId] + fmt.Println("group first : ", group) + fmt.Println("user.Conn.Store.Chats", user.Conn.Store.Contacts) + if !ok { + //user.Conn + err := ce.User.Conn.GetConversations("") + //time.Sleep(5 * time.Second) + if err != nil { + fmt.Println(err) + ce.Reply("get conversations failed. Try syncing contacts with `sync` first.") + } else { + group, ok = user.Conn.Store.Chats[conversationId] + if !ok { + ce.Reply("Group JID not found in chats. Try syncing groups with `sync` first.") + return + } + } + } + fmt.Println("group", group) + fmt.Println("GetConversations", user.Conn.Store.Contacts) + handler.log.Debugln("GetConversations", conversationId, "for", user) + + //portal := user.bridge.GetPortalByJID(database.GroupPortalKey(conversationId)) + //fmt.Printf("portal %+v : ", portal) + //if len(portal.MXID) > 0 { + // //portaFl.Sync(user, contact) + // ce.Reply("Portal room synced.") + //} else { + // //portal.Sync(user, contact) + // //ce.Reply("Portal room created.") + //} + // + handler.log.Debugln("Inviting", userNumbers, "to", conversationId) + err := user.Conn.HandleGroupInvite(conversationId, userNumbers) + if err != nil { + ce.Reply("Please confirm that you have permission to invite members.") + } else { + ce.Reply("Group invitation sent.\nIf the member fails to join the group, please check your permissions or command parameters") + } + //time.Sleep(time.Duration(3)*time.Second) + //ce.Reply("Syncing room puppet...") + //chatMap := make(map[string]whatsapp.Chat) + //for _, chat := range user.Conn.Store.Chats { + // if chat.Jid == jid { + // chatMap[chat.Jid]= chat + // } + //} + //user.syncPortals(chatMap, false) + //ce.Reply("Syncing room puppet completed") +} + +const cmdKickHelp = `kick <_group ID_> <_contact Id>,... - Remove members from the group.` + +func (handler *CommandHandler) CommandKick(ce *CommandEvent) { + if len(ce.Args) < 2 { + ce.Reply("**Usage:** `kick ,... reason`") + return + } + + user := ce.User + converationId := ce.Args[0] + userNumbers := strings.Split(ce.Args[1], ",") + //reason := "omitempty" + //if len(ce.Args) > 2 { + // reason = ce.Args[0] + //} + + if strings.HasSuffix(converationId, whatsappExt.NewUserSuffix) { + ce.Reply("**Usage:** `kick ,... reason`") + return + } + + //fmt.Println("user:", user) + //fmt.Println("chats", user.Conn.Store.Chats) + _, ok := user.Conn.Store.Chats[converationId] + //fmt.Println("查找用户组是否存在:") + //fmt.Println(group) + if !ok { + ce.Reply("Group ID not found in contacts. Try syncing contacts with `sync` first.") + return + } + handler.log.Debugln("Importing", converationId, "for", user) + portal := user.bridge.GetPortalByJID(database.GroupPortalKey(converationId)) + + for i, number := range userNumbers { + userNumbers[i] = number // + whatsappExt.NewUserSuffix + member := portal.bridge.GetPuppetByJID(number + whatsappExt.NewUserSuffix) + + if member == nil { + portal.log.Errorln("%s is not a puppet", number) + return + } + } + + handler.log.Debugln("Kicking", userNumbers, "to", converationId) + err := user.Conn.HandleGroupKick(converationId, userNumbers) + if err != nil { + handler.log.Errorln("Kicking err", err) + ce.Reply("Please confirm that you have permission to kick members.") + } else { + ce.Reply("Remove operation completed.\nIf the member has not been removed, please check your permissions or command parameters") + } +} + +const cmdLeaveHelp = `leave <_group ID_> - Leave a group.` + +func (handler *CommandHandler) CommandLeave(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `leave `") + return + } + + user := ce.User + groupId := ce.Args[0] + + if strings.HasSuffix(groupId, whatsappExt.NewUserSuffix) { + ce.Reply("**Usage:** `leave `") + return + } + // + handler.log.Debugln("Importing", groupId, "for", user) + portal := user.bridge.GetPortalByJID(database.GroupPortalKey(groupId)) + + if len(portal.MXID) > 0 { + cli := handler.bridge.Bot + fmt.Println("cli appurl:", cli.Prefix, cli.AppServiceUserID, cli.HomeserverURL.String(), ) + //res, errLeave := cli.LeaveRoom(portal.MXID) + //cli.AppServiceUserID = ce.User.MXID + u := cli.BuildURL("rooms", portal.MXID, "leave") + fmt.Println(u) + resp := mautrix.RespLeaveRoom{} + res , err := cli.MakeRequest("POST", u, struct{}{}, &resp) + fmt.Println("leave res : ", res) + fmt.Println("leave res err: ", err) + //if errLeave != nil { + // portal.log.Errorln("Error leaving matrix room:", errLeave) + //} + } + err := user.Conn.HandleGroupLeave(groupId) + if err != nil { + fmt.Println(err) + ce.Reply("Leave operation failed.") + return + } + ce.Reply("Leave operation completed and successful.") + +} +const cmdShareHelp = `share <_group ID_> - Generate a link to join the group.` + +func (handler *CommandHandler) CommandShare(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `share `") + return + } + user := ce.User + converationId := ce.Args[0] + fmt.Println("share converationId : ", converationId) + //check the group is exists + _, ok := user.Conn.Store.Chats[converationId] + if !ok { + ce.Reply("Group ID not found in groups. Try syncing groups with `sync` first.") + return + } + //set enabled + enstr := map[string]string{ + "joiningenabled":"true", + } + _, err := user.Conn.SetConversationThreads(converationId, enstr) + if err != nil { + ce.Reply("Set ConversationThreads failed.") + return + } + //create share link + err, link := user.Conn.HandleGroupShare(converationId) + if err != nil { + ce.Reply("Generate the share link failed.") + } + ce.Reply("The link : " + link) + +} + +const cmdJoinHelp = `join <_invitation link_> - Join the group via the invitation link.` + +func (handler *CommandHandler) CommandJoin(ce *CommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `join `") + return + } + + user := ce.User + joinurl := ce.Args[0] + + err, _ := user.Conn.HandleGroupJoin(joinurl) + + if err != nil { + ce.Reply("Join group completed and failed.") + } + ce.Reply("Join group completed and successful.") + //contact, ok := user.Conn.Store.Contacts[jid] + //if !ok { + // ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.") + // return + //} + //handler.log.Debugln("Importing", jid, "for", user) + //portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid)) + //if len(portal.MXID) > 0 { + // portal.Sync(user, contact) + // ce.Reply("Portal room synced.") + //} else { + // portal.Sync(user, contact) + // ce.Reply("Portal room created.") + //} +} + +// +//func (handler *CommandHandler) CommandJoin(ce *CommandEvent) { +// if len(ce.Args) == 0 { +// ce.Reply("**Usage:** `join `") +// return +// } +// +// user := ce.User +// params := strings.Split(ce.Args[0], "com/") +// +// jid, err := user.Conn.HandleGroupJoin(params[len(params)-1]) +// if err == nil { +// ce.Reply("Join operation completed.") +// } +// +// contact, ok := user.Conn.Store.Contacts[jid] +// if !ok { +// ce.Reply("Group JID not found in contacts. Try syncing contacts with `sync` first.") +// return +// } +// handler.log.Debugln("Importing", jid, "for", user) +// portal := user.bridge.GetPortalByJID(database.GroupPortalKey(jid)) +// if len(portal.MXID) > 0 { +// portal.Sync(user, contact) +// ce.Reply("Portal room synced.") +// } else { +// portal.Sync(user, contact) +// ce.Reply("Portal room created.") +// } +//} + +const cmdCreateHelp = `create <_topic_> <_member user id_>,... - Create a group.` + +func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { + if len(ce.Args) < 2 { + ce.Reply("**Usage:** `create ,...`") + return + } + + user := ce.User + topic := ce.Args[0] + members := skype.Members{} + + // The user who created the group must be in the members and have "Admin" rights + userId := ce.User.Conn.UserProfile.Username + member2 := skype.Member{ + Id: "8:" + userId, + Role: "Admin", + } + + members.Members = append(members.Members, member2) + members.Properties = skype.Properties{ + HistoryDisclosed: "true", + Topic: topic, + } + + handler.log.Debugln("Create Group", topic, "with", members) + err := user.Conn.HandleGroupCreate(members) + + inputArr := strings.Split(ce.Args[1], ",") + inputArr = inputArr[1:] + members = skype.Members{} + for _, memberId := range inputArr { + members.Members = append(members.Members, skype.Member{ + Id: "8:" + memberId, + Role: "Admin", + }) + } + err = user.Conn.AddMember(members, "") + if err != nil { + ce.Reply("Please confirm that parameters is correct.") + } else { + ce.Reply("Syncing group list...") + time.Sleep(time.Duration(3) * time.Second) + ce.Reply("Syncing group list completed") + } +} diff --git a/community.go b/community.go new file mode 100644 index 0000000..849fbca --- /dev/null +++ b/community.go @@ -0,0 +1,116 @@ +package main + +import ( + "fmt" + "net/http" + + "maunium.net/go/mautrix" +) + +func (user *User) inviteToCommunity() { + url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", user.MXID) + reqBody := map[string]interface{}{} + _, err := user.bridge.Bot.MakeRequest(http.MethodPut, url, &reqBody, nil) + if err != nil { + user.log.Warnfln("Failed to invite user to personal filtering community %s: %v", user.CommunityID, err) + } +} + +func (user *User) updateCommunityProfile() { + url := user.bridge.Bot.BuildURL("groups", user.CommunityID, "profile") + profileReq := struct { + Name string `json:"name"` + AvatarURL string `json:"avatar_url"` + ShortDescription string `json:"short_description"` + }{"WhatsApp", user.bridge.Config.AppService.Bot.Avatar, "Your WhatsApp bridged chats"} + _, err := user.bridge.Bot.MakeRequest(http.MethodPost, url, &profileReq, nil) + if err != nil { + user.log.Warnfln("Failed to update metadata of %s: %v", user.CommunityID, err) + } +} + +func (user *User) createCommunity() { + if user.IsRelaybot || !user.bridge.Config.Bridge.EnableCommunities() { + return + } + + localpart, server, _ := user.MXID.Parse() + community := user.bridge.Config.Bridge.FormatCommunity(localpart, server) + user.log.Debugln("Creating personal filtering community", community) + bot := user.bridge.Bot + req := struct { + Localpart string `json:"localpart"` + }{community} + resp := struct { + GroupID string `json:"group_id"` + }{} + _, err := bot.MakeRequest(http.MethodPost, bot.BuildURL("create_group"), &req, &resp) + if err != nil { + if httpErr, ok := err.(mautrix.HTTPError); ok { + if httpErr.RespError.Err != "Group already exists" { + user.log.Warnln("Server responded with error creating personal filtering community:", err) + return + } else { + user.log.Debugln("Personal filtering community", resp.GroupID, "already existed") + user.CommunityID = fmt.Sprintf("+%s:%s", req.Localpart, user.bridge.Config.Homeserver.Domain) + } + } else { + user.log.Warnln("Unknown error creating personal filtering community:", err) + return + } + } else { + user.log.Infoln("Created personal filtering community %s", resp.GroupID) + user.CommunityID = resp.GroupID + user.inviteToCommunity() + user.updateCommunityProfile() + } +} + +func (user *User) addPuppetToCommunity(puppet *Puppet) bool { + if user.IsRelaybot || len(user.CommunityID) == 0 { + return false + } + bot := user.bridge.Bot + url := bot.BuildURL("groups", user.CommunityID, "admin", "users", "invite", puppet.MXID) + blankReqBody := map[string]interface{}{} + _, err := bot.MakeRequest(http.MethodPut, url, &blankReqBody, nil) + if err != nil { + user.log.Warnfln("Failed to invite %s to %s: %v", puppet.MXID, user.CommunityID, err) + return false + } + reqBody := map[string]map[string]string{ + "m.visibility": { + "type": "private", + }, + } + url = bot.BuildURLWithQuery(mautrix.URLPath{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{ + "user_id": puppet.MXID.String(), + }) + _, err = bot.MakeRequest(http.MethodPut, url, &reqBody, nil) + if err != nil { + user.log.Warnfln("Failed to join %s as %s: %v", user.CommunityID, puppet.MXID, err) + return false + } + user.log.Debugln("Added", puppet.MXID, "to", user.CommunityID) + return true +} + +func (user *User) addPortalToCommunity(portal *Portal) bool { + if user.IsRelaybot || len(user.CommunityID) == 0 || len(portal.MXID) == 0 { + return false + } + bot := user.bridge.Bot + url := bot.BuildURL("groups", user.CommunityID, "admin", "rooms", portal.MXID) + reqBody := map[string]map[string]string{ + "m.visibility": { + "type": "private", + }, + } + _, err := bot.MakeRequest(http.MethodPut, url, &reqBody, nil) + if err != nil { + user.log.Warnfln("Failed to add %s to %s: %v", portal.MXID, user.CommunityID, err) + return false + } + user.log.Debugln("Added", portal.MXID, "to", user.CommunityID) + return true +} diff --git a/config/bridge.go b/config/bridge.go new file mode 100644 index 0000000..b93eb50 --- /dev/null +++ b/config/bridge.go @@ -0,0 +1,312 @@ +package config + +import ( + "bytes" + skype "github.com/kelaresg/go-skypeapi" + "strconv" + "strings" + "text/template" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/types" +) + +type BridgeConfig struct { + UsernameTemplate string `yaml:"username_template"` + DisplaynameTemplate string `yaml:"displayname_template"` + CommunityTemplate string `yaml:"community_template"` + + ConnectionTimeout int `yaml:"connection_timeout"` + FetchMessageOnTimeout bool `yaml:"fetch_message_on_timeout"` + DeliveryReceipts bool `yaml:"delivery_receipts"` + LoginQRRegenCount int `yaml:"login_qr_regen_count"` + MaxConnectionAttempts int `yaml:"max_connection_attempts"` + ConnectionRetryDelay int `yaml:"connection_retry_delay"` + ReportConnectionRetry bool `yaml:"report_connection_retry"` + ChatListWait int `yaml:"chat_list_wait"` + PortalSyncWait int `yaml:"portal_sync_wait"` + + CallNotices struct { + Start bool `yaml:"start"` + End bool `yaml:"end"` + } `yaml:"call_notices"` + + InitialChatSync int `yaml:"initial_chat_sync_count"` + InitialHistoryFill int `yaml:"initial_history_fill_count"` + HistoryDisableNotifs bool `yaml:"initial_history_disable_notifications"` + RecoverChatSync int `yaml:"recovery_chat_sync_count"` + RecoverHistory bool `yaml:"recovery_history_backfill"` + SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"` + + SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` + LoginSharedSecret string `yaml:"login_shared_secret"` + + InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` + PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` + + WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"` + + AllowUserInvite bool `yaml:"allow_user_invite"` + + CommandPrefix string `yaml:"command_prefix"` + + Encryption struct { + Allow bool `yaml:"allow"` + Default bool `yaml:"default"` + } `yaml:"encryption"` + + Permissions PermissionConfig `yaml:"permissions"` + + Relaybot RelaybotConfig `yaml:"relaybot"` + + usernameTemplate *template.Template `yaml:"-"` + displaynameTemplate *template.Template `yaml:"-"` + communityTemplate *template.Template `yaml:"-"` +} + +func (bc *BridgeConfig) setDefaults() { + bc.ConnectionTimeout = 20 + bc.FetchMessageOnTimeout = false + bc.DeliveryReceipts = false + bc.LoginQRRegenCount = 2 + bc.MaxConnectionAttempts = 3 + bc.ConnectionRetryDelay = -1 + bc.ReportConnectionRetry = true + bc.ChatListWait = 30 + bc.PortalSyncWait = 600 + + bc.CallNotices.Start = true + bc.CallNotices.End = true + + bc.InitialChatSync = 10 + bc.InitialHistoryFill = 20 + bc.RecoverChatSync = -1 + bc.RecoverHistory = true + bc.SyncChatMaxAge = 259200 + + bc.SyncWithCustomPuppets = true + bc.LoginSharedSecret = "" + + bc.InviteOwnPuppetForBackfilling = true + bc.PrivateChatPortalMeta = false +} + +type umBridgeConfig BridgeConfig + +func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + err := unmarshal((*umBridgeConfig)(bc)) + if err != nil { + return err + } + + bc.usernameTemplate, err = template.New("username").Parse(bc.UsernameTemplate) + if err != nil { + return err + } + + bc.displaynameTemplate, err = template.New("displayname").Parse(bc.DisplaynameTemplate) + if err != nil { + return err + } + + if len(bc.CommunityTemplate) > 0 { + bc.communityTemplate, err = template.New("community").Parse(bc.CommunityTemplate) + if err != nil { + return err + } + } + + return nil +} + +type UsernameTemplateArgs struct { + UserID id.UserID +} + +func (bc BridgeConfig) FormatDisplayname(contact skype.Contact) (string, int8) { + var buf bytes.Buffer + if index := strings.IndexRune(contact.PersonId, '@'); index > 0 { + contact.PersonId = "+" + contact.PersonId[:index] + } + bc.displaynameTemplate.Execute(&buf, contact) + var quality int8 + switch { + //case len(contact.Notify) > 0: + // quality = 3 + case len(contact.DisplayName) > 0: + quality = 3 + case len(contact.PersonId) > 0: + quality = 1 + default: + quality = 0 + } + return buf.String(), quality +} + +func (bc BridgeConfig) FormatUsername(userID types.SkypeID) string { + var buf bytes.Buffer + bc.usernameTemplate.Execute(&buf, userID) + return buf.String() +} + +type CommunityTemplateArgs struct { + Localpart string + Server string +} + +func (bc BridgeConfig) EnableCommunities() bool { + return bc.communityTemplate != nil +} + +func (bc BridgeConfig) FormatCommunity(localpart, server string) string { + var buf bytes.Buffer + bc.communityTemplate.Execute(&buf, CommunityTemplateArgs{localpart, server}) + return buf.String() +} + +type PermissionConfig map[string]PermissionLevel + +type PermissionLevel int + +const ( + PermissionLevelDefault PermissionLevel = 0 + PermissionLevelRelaybot PermissionLevel = 5 + PermissionLevelUser PermissionLevel = 10 + PermissionLevelAdmin PermissionLevel = 100 +) + +func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + rawPC := make(map[string]string) + err := unmarshal(&rawPC) + if err != nil { + return err + } + + if *pc == nil { + *pc = make(map[string]PermissionLevel) + } + for key, value := range rawPC { + switch strings.ToLower(value) { + case "relaybot": + (*pc)[key] = PermissionLevelRelaybot + case "user": + (*pc)[key] = PermissionLevelUser + case "admin": + (*pc)[key] = PermissionLevelAdmin + default: + val, err := strconv.Atoi(value) + if err != nil { + (*pc)[key] = PermissionLevelDefault + } else { + (*pc)[key] = PermissionLevel(val) + } + } + } + return nil +} + +func (pc *PermissionConfig) MarshalYAML() (interface{}, error) { + if *pc == nil { + return nil, nil + } + rawPC := make(map[string]string) + for key, value := range *pc { + switch value { + case PermissionLevelRelaybot: + rawPC[key] = "relaybot" + case PermissionLevelUser: + rawPC[key] = "user" + case PermissionLevelAdmin: + rawPC[key] = "admin" + default: + rawPC[key] = strconv.Itoa(int(value)) + } + } + return rawPC, nil +} + +func (pc PermissionConfig) IsRelaybotWhitelisted(userID id.UserID) bool { + return pc.GetPermissionLevel(userID) >= PermissionLevelRelaybot +} + +func (pc PermissionConfig) IsWhitelisted(userID id.UserID) bool { + return pc.GetPermissionLevel(userID) >= PermissionLevelUser +} + +func (pc PermissionConfig) IsAdmin(userID id.UserID) bool { + return pc.GetPermissionLevel(userID) >= PermissionLevelAdmin +} + +func (pc PermissionConfig) GetPermissionLevel(userID id.UserID) PermissionLevel { + permissions, ok := pc[string(userID)] + if ok { + return permissions + } + + _, homeserver, _ := userID.Parse() + permissions, ok = pc[homeserver] + if len(homeserver) > 0 && ok { + return permissions + } + + permissions, ok = pc["*"] + if ok { + return permissions + } + + return PermissionLevelDefault +} + +type RelaybotConfig struct { + Enabled bool `yaml:"enabled"` + ManagementRoom id.RoomID `yaml:"management"` + InviteUsers []id.UserID `yaml:"invites"` + + MessageFormats map[event.MessageType]string `yaml:"message_formats"` + messageTemplates *template.Template `yaml:"-"` +} + +type umRelaybotConfig RelaybotConfig + +func (rc *RelaybotConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + err := unmarshal((*umRelaybotConfig)(rc)) + if err != nil { + return err + } + + rc.messageTemplates = template.New("messageTemplates") + for key, format := range rc.MessageFormats { + _, err := rc.messageTemplates.New(string(key)).Parse(format) + if err != nil { + return err + } + } + + return nil +} + +type Sender struct { + UserID id.UserID + *event.MemberEventContent +} + +type formatData struct { + Sender Sender + Message string + Content *event.MessageEventContent +} + +func (rc *RelaybotConfig) FormatMessage(content *event.MessageEventContent, sender id.UserID, member *event.MemberEventContent) (string, error) { + var output strings.Builder + err := rc.messageTemplates.ExecuteTemplate(&output, string(content.MsgType), formatData{ + Sender: Sender{ + UserID: sender, + MemberEventContent: member, + }, + Content: content, + Message: content.FormattedBody, + }) + return output.String(), err +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..78af0fd --- /dev/null +++ b/config/config.go @@ -0,0 +1,104 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" + + "maunium.net/go/mautrix/appservice" +) + +type Config struct { + Homeserver struct { + Address string `yaml:"address"` + Domain string `yaml:"domain"` + } `yaml:"homeserver"` + + AppService struct { + Address string `yaml:"address"` + Hostname string `yaml:"hostname"` + Port uint16 `yaml:"port"` + + Database struct { + Type string `yaml:"type"` + URI string `yaml:"uri"` + + MaxOpenConns int `yaml:"max_open_conns"` + MaxIdleConns int `yaml:"max_idle_conns"` + } `yaml:"database"` + + StateStore string `yaml:"state_store_path,omitempty"` + + Provisioning struct { + Prefix string `yaml:"prefix"` + SharedSecret string `yaml:"shared_secret"` + } `yaml:"provisioning"` + + ID string `yaml:"id"` + Bot struct { + Username string `yaml:"username"` + Displayname string `yaml:"displayname"` + Avatar string `yaml:"avatar"` + } `yaml:"bot"` + + ASToken string `yaml:"as_token"` + HSToken string `yaml:"hs_token"` + } `yaml:"appservice"` + + Bridge BridgeConfig `yaml:"bridge"` + + Logging appservice.LogConfig `yaml:"logging"` +} + +func (config *Config) setDefaults() { + config.AppService.Database.MaxOpenConns = 20 + config.AppService.Database.MaxIdleConns = 2 + config.Bridge.setDefaults() +} + +func Load(path string) (*Config, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + var config = &Config{} + config.setDefaults() + err = yaml.Unmarshal(data, config) + return config, err +} + +func (config *Config) Save(path string) error { + data, err := yaml.Marshal(config) + if err != nil { + return err + } + return ioutil.WriteFile(path, data, 0600) +} + +func (config *Config) MakeAppService() (*appservice.AppService, error) { + as := appservice.Create() + as.HomeserverDomain = config.Homeserver.Domain + as.HomeserverURL = config.Homeserver.Address + as.Host.Hostname = config.AppService.Hostname + as.Host.Port = config.AppService.Port + var err error + as.Registration, err = config.GetRegistration() + return as, err +} diff --git a/config/registration.go b/config/registration.go new file mode 100644 index 0000000..aa3b054 --- /dev/null +++ b/config/registration.go @@ -0,0 +1,67 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package config + +import ( + "fmt" + "regexp" + + "maunium.net/go/mautrix/appservice" +) + +func (config *Config) NewRegistration() (*appservice.Registration, error) { + registration := appservice.CreateRegistration() + + err := config.copyToRegistration(registration) + if err != nil { + return nil, err + } + + config.AppService.ASToken = registration.AppToken + config.AppService.HSToken = registration.ServerToken + return registration, nil +} + +func (config *Config) GetRegistration() (*appservice.Registration, error) { + registration := appservice.CreateRegistration() + + err := config.copyToRegistration(registration) + if err != nil { + return nil, err + } + + registration.AppToken = config.AppService.ASToken + registration.ServerToken = config.AppService.HSToken + return registration, nil +} + +func (config *Config) copyToRegistration(registration *appservice.Registration) error { + registration.ID = config.AppService.ID + registration.URL = config.AppService.Address + registration.RateLimited = false + registration.SenderLocalpart = config.AppService.Bot.Username + + userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", + //config.Bridge.FormatUsername("[0-9]+"), + config.Bridge.FormatUsername("(.*)"), + config.Homeserver.Domain)) + if err != nil { + return err + } + registration.Namespaces.RegisterUserIDs(userIDRegex, true) + return nil +} diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..65f728b --- /dev/null +++ b/crypto.go @@ -0,0 +1,226 @@ +// +build cgo + +package main + +import ( + "crypto/hmac" + "crypto/sha512" + "encoding/hex" + "fmt" + "time" + + "github.com/pkg/errors" + "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix" + "github.com/kelaresg/matrix-skype/database" + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +var levelTrace = maulogger.Level{ + Name: "Trace", + Severity: -10, + Color: -1, +} + +type CryptoHelper struct { + bridge *Bridge + client *mautrix.Client + mach *crypto.OlmMachine + store *database.SQLCryptoStore + log maulogger.Logger + baseLog maulogger.Logger +} + +func NewCryptoHelper(bridge *Bridge) Crypto { + if !bridge.Config.Bridge.Encryption.Allow { + bridge.Log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config") + return nil + } else if bridge.Config.Bridge.LoginSharedSecret == "" { + bridge.Log.Warnln("End-to-bridge encryption enabled, but login_shared_secret not set") + return nil + } + baseLog := bridge.Log.Sub("Crypto") + return &CryptoHelper{ + bridge: bridge, + log: baseLog.Sub("Helper"), + baseLog: baseLog, + } +} + +func (helper *CryptoHelper) Init() error { + helper.log.Debugln("Initializing end-to-bridge encryption...") + var err error + helper.client, err = helper.loginBot() + if err != nil { + return err + } + + helper.log.Debugln("Logged in as bridge bot with device ID", helper.client.DeviceID) + logger := &cryptoLogger{helper.baseLog} + stateStore := &cryptoStateStore{helper.bridge} + helper.store = database.NewSQLCryptoStore(helper.bridge.DB, helper.client.DeviceID) + helper.store.UserID = helper.client.UserID + helper.store.GhostIDFormat = fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.AS.HomeserverDomain) + helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore) + + helper.client.Logger = logger.int.Sub("Bot") + helper.client.Syncer = &cryptoSyncer{helper.mach} + helper.client.Store = &cryptoClientStore{helper.store} + + return helper.mach.Load() +} + +func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { + deviceID := helper.bridge.DB.FindDeviceID() + if len(deviceID) > 0 { + helper.log.Debugln("Found existing device ID for bot in database:", deviceID) + } + mac := hmac.New(sha512.New, []byte(helper.bridge.Config.Bridge.LoginSharedSecret)) + mac.Write([]byte(helper.bridge.AS.BotMXID())) + resp, err := helper.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ + Type: "m.login.password", + Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(helper.bridge.AS.BotMXID())}, + Password: hex.EncodeToString(mac.Sum(nil)), + DeviceID: deviceID, + InitialDeviceDisplayName: "Skype Bridge", + }) + if err != nil { + return nil, err + } + client, err := mautrix.NewClient(helper.bridge.AS.HomeserverURL, helper.bridge.AS.BotMXID(), resp.AccessToken) + if err != nil { + return nil, err + } + client.DeviceID = resp.DeviceID + return client, nil +} + +func (helper *CryptoHelper) Start() { + helper.log.Debugln("Starting syncer for receiving to-device messages") + err := helper.client.Sync() + if err != nil { + helper.log.Errorln("Fatal error syncing:", err) + } +} + +func (helper *CryptoHelper) Stop() { + helper.client.StopSync() +} + +func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) { + return helper.mach.DecryptMegolmEvent(evt) +} + +func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) { + encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, content) + if err != nil { + if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession { + return nil, err + } + helper.log.Debugfln("Got %v while encrypting event for %s, sharing group session and trying again...", err, roomID) + users, err := helper.store.GetRoomMembers(roomID) + if err != nil { + return nil, errors.Wrap(err, "failed to get room member list") + } + err = helper.mach.ShareGroupSession(roomID, users) + if err != nil { + return nil, errors.Wrap(err, "failed to share group session") + } + encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, content) + if err != nil { + return nil, errors.Wrap(err, "failed to encrypt event after re-sharing group session") + } + } + return encrypted, nil +} + +func (helper *CryptoHelper) HandleMemberEvent(evt *event.Event) { + helper.mach.HandleMemberEvent(evt) +} + +type cryptoSyncer struct { + *crypto.OlmMachine +} + +func (syncer *cryptoSyncer) ProcessResponse(resp *mautrix.RespSync, since string) error { + syncer.ProcessSyncResponse(resp, since) + return nil +} + +func (syncer *cryptoSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { + syncer.Log.Error("Error /syncing, waiting 10 seconds: %v", err) + return 10 * time.Second, nil +} + +func (syncer *cryptoSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter { + everything := []event.Type{{Type: "*"}} + return &mautrix.Filter{ + Presence: mautrix.FilterPart{NotTypes: everything}, + AccountData: mautrix.FilterPart{NotTypes: everything}, + Room: mautrix.RoomFilter{ + IncludeLeave: false, + Ephemeral: mautrix.FilterPart{NotTypes: everything}, + AccountData: mautrix.FilterPart{NotTypes: everything}, + State: mautrix.FilterPart{NotTypes: everything}, + Timeline: mautrix.FilterPart{NotTypes: everything}, + }, + } +} + +type cryptoLogger struct { + int maulogger.Logger +} + +func (c *cryptoLogger) Error(message string, args ...interface{}) { + c.int.Errorfln(message, args...) +} + +func (c *cryptoLogger) Warn(message string, args ...interface{}) { + c.int.Warnfln(message, args...) +} + +func (c *cryptoLogger) Debug(message string, args ...interface{}) { + c.int.Debugfln(message, args...) +} + +func (c *cryptoLogger) Trace(message string, args ...interface{}) { + c.int.Logfln(levelTrace, message, args...) +} + +type cryptoClientStore struct { + int *database.SQLCryptoStore +} + +func (c cryptoClientStore) SaveFilterID(_ id.UserID, _ string) {} +func (c cryptoClientStore) LoadFilterID(_ id.UserID) string { return "" } +func (c cryptoClientStore) SaveRoom(_ *mautrix.Room) {} +func (c cryptoClientStore) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } + +func (c cryptoClientStore) SaveNextBatch(_ id.UserID, nextBatchToken string) { + c.int.PutNextBatch(nextBatchToken) +} + +func (c cryptoClientStore) LoadNextBatch(_ id.UserID) string { + return c.int.GetNextBatch() +} + +var _ mautrix.Storer = (*cryptoClientStore)(nil) + +type cryptoStateStore struct { + bridge *Bridge +} + +func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool { + portal := c.bridge.GetPortalByMXID(id) + if portal != nil { + return portal.Encrypted + } + return false +} + +func (c *cryptoStateStore) FindSharedRooms(id id.UserID) []id.RoomID { + return c.bridge.StateStore.FindSharedRooms(id) +} diff --git a/custompuppet.go b/custompuppet.go new file mode 100644 index 0000000..91ea32c --- /dev/null +++ b/custompuppet.go @@ -0,0 +1,257 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha512" + "encoding/hex" + "time" + + "github.com/pkg/errors" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +var ( + ErrNoCustomMXID = errors.New("no custom mxid set") + ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") +) + +func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error { + prevCustomMXID := puppet.CustomMXID + if puppet.customIntent != nil { + puppet.stopSyncing() + } + puppet.CustomMXID = mxid + puppet.AccessToken = accessToken + + err := puppet.StartCustomMXID() + if err != nil { + return err + } + + if len(prevCustomMXID) > 0 { + delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID) + } + if len(puppet.CustomMXID) > 0 { + puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } + puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID) + puppet.Update() + // TODO leave rooms with default puppet + return nil +} + +func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { + mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret)) + mac.Write([]byte(mxid)) + resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ + Type: "m.login.password", + Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(mxid)}, + Password: hex.EncodeToString(mac.Sum(nil)), + DeviceID: "WhatsApp Bridge", + InitialDeviceDisplayName: "WhatsApp Bridge", + }) + if err != nil { + return "", err + } + return resp.AccessToken, nil +} + +func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { + if len(puppet.CustomMXID) == 0 { + return nil, ErrNoCustomMXID + } + client, err := mautrix.NewClient(puppet.bridge.AS.HomeserverURL, puppet.CustomMXID, puppet.AccessToken) + if err != nil { + return nil, err + } + client.Logger = puppet.bridge.AS.Log.Sub(string(puppet.CustomMXID)) + client.Syncer = puppet + client.Store = puppet + + ia := puppet.bridge.AS.NewIntentAPI("custom") + ia.Client = client + ia.Localpart, _, _ = puppet.CustomMXID.Parse() + ia.UserID = puppet.CustomMXID + ia.IsCustomPuppet = true + return ia, nil +} + +func (puppet *Puppet) clearCustomMXID() { + puppet.CustomMXID = "" + puppet.AccessToken = "" + puppet.customIntent = nil + puppet.customTypingIn = nil + puppet.customUser = nil +} + +func (puppet *Puppet) StartCustomMXID() error { + if len(puppet.CustomMXID) == 0 { + puppet.clearCustomMXID() + return nil + } + intent, err := puppet.newCustomIntent() + if err != nil { + puppet.clearCustomMXID() + return err + } + resp, err := intent.Whoami() + if err != nil { + puppet.clearCustomMXID() + return err + } + if resp.UserID != puppet.CustomMXID { + puppet.clearCustomMXID() + return ErrMismatchingMXID + } + puppet.customIntent = intent + puppet.customTypingIn = make(map[id.RoomID]bool) + puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) + puppet.startSyncing() + return nil +} + +func (puppet *Puppet) startSyncing() { + if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + return + } + go func() { + puppet.log.Debugln("Starting syncing...") + puppet.customIntent.SyncPresence = "offline" + err := puppet.customIntent.Sync() + if err != nil { + puppet.log.Errorln("Fatal error syncing:", err) + } + }() +} + +func (puppet *Puppet) stopSyncing() { + if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { + return + } + puppet.customIntent.StopSync() +} + +func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) error { + if !puppet.customUser.IsConnected() { + puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp") + return nil + } + for roomID, events := range resp.Rooms.Join { + portal := puppet.bridge.GetPortalByMXID(roomID) + if portal == nil { + continue + } + for _, evt := range events.Ephemeral.Events { + err := evt.Content.ParseRaw(evt.Type) + if err != nil { + continue + } + switch evt.Type { + case event.EphemeralEventReceipt: + go puppet.handleReceiptEvent(portal, evt) + case event.EphemeralEventTyping: + go puppet.handleTypingEvent(portal, evt) + } + } + } + for _, evt := range resp.Presence.Events { + if evt.Sender != puppet.CustomMXID { + continue + } + err := evt.Content.ParseRaw(evt.Type) + if err != nil { + continue + } + go puppet.handlePresenceEvent(evt) + } + return nil +} + +func (puppet *Puppet) handlePresenceEvent(event *event.Event) { + //presence := whatsapp.PresenceAvailable + //if event.Content.Raw["presence"].(string) != "online" { + // presence = whatsapp.PresenceUnavailable + // puppet.customUser.log.Debugln("Marking offline") + //} else { + // puppet.customUser.log.Debugln("Marking online") + //} + //_, err := puppet.customUser.Conn.Presence("", presence) + //if err != nil { + // puppet.customUser.log.Warnln("Failed to set presence:", err) + //} +} + +func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) { + //for eventID, receipts := range *event.Content.AsReceipt() { + // if _, ok := receipts.Read[puppet.CustomMXID]; !ok { + // continue + // } + // message := puppet.bridge.DB.Message.GetByMXID(eventID) + // if message == nil { + // continue + // } + // puppet.customUser.log.Infofln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID) + // _, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID) + // if err != nil { + // puppet.customUser.log.Warnln("Error marking read:", err) + // } + //} +} + +func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) { + //isTyping := false + //for _, userID := range evt.Content.AsTyping().UserIDs { + // if userID == puppet.CustomMXID { + // isTyping = true + // break + // } + //} + //if puppet.customTypingIn[evt.RoomID] != isTyping { + // puppet.customTypingIn[evt.RoomID] = isTyping + // presence := whatsapp.PresenceComposing + // if !isTyping { + // puppet.customUser.log.Infofln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID) + // presence = whatsapp.PresencePaused + // } else { + // puppet.customUser.log.Infofln("Marking typing in %s/%s", portal.Key.JID, portal.MXID) + // } + // _, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence) + // if err != nil { + // puppet.customUser.log.Warnln("Error setting typing:", err) + // } + //} +} + +func (puppet *Puppet) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) { + puppet.log.Warnln("Sync error:", err) + return 10 * time.Second, nil +} + +func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter { + everything := []event.Type{{Type: "*"}} + return &mautrix.Filter{ + Presence: mautrix.FilterPart{ + Senders: []id.UserID{puppet.CustomMXID}, + Types: []event.Type{event.EphemeralEventPresence}, + }, + AccountData: mautrix.FilterPart{NotTypes: everything}, + Room: mautrix.RoomFilter{ + Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}}, + IncludeLeave: false, + AccountData: mautrix.FilterPart{NotTypes: everything}, + State: mautrix.FilterPart{NotTypes: everything}, + Timeline: mautrix.FilterPart{NotTypes: everything}, + }, + } +} + +func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {} +func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() } +func (puppet *Puppet) SaveRoom(room *mautrix.Room) {} +func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" } +func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch } +func (puppet *Puppet) LoadRoom(roomID id.RoomID) *mautrix.Room { return nil } diff --git a/database/cryptostore.go b/database/cryptostore.go new file mode 100644 index 0000000..e0fe724 --- /dev/null +++ b/database/cryptostore.go @@ -0,0 +1,438 @@ +// +build cgo + +package database + +import ( + "database/sql" + "fmt" + "strings" + + "github.com/lib/pq" + "github.com/pkg/errors" + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/crypto/olm" + "maunium.net/go/mautrix/id" +) + +type SQLCryptoStore struct { + db *Database + log log.Logger + + UserID id.UserID + DeviceID id.DeviceID + SyncToken string + PickleKey []byte + Account *crypto.OlmAccount + + GhostIDFormat string +} + +var _ crypto.Store = (*SQLCryptoStore)(nil) + +func NewSQLCryptoStore(db *Database, deviceID id.DeviceID) *SQLCryptoStore { + return &SQLCryptoStore{ + db: db, + log: db.log.Sub("CryptoStore"), + PickleKey: []byte("github.com/kelaresg/matrix-skype"), + DeviceID: deviceID, + } +} + +func (db *Database) FindDeviceID() (deviceID id.DeviceID) { + err := db.QueryRow("SELECT device_id FROM crypto_account LIMIT 1").Scan(&deviceID) + if err != nil && err != sql.ErrNoRows { + db.log.Warnln("Failed to scan device ID:", err) + } + return +} + +func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) (members []id.UserID, err error) { + var rows *sql.Rows + rows, err = store.db.Query(` + SELECT user_id FROM mx_user_profile + WHERE room_id=$1 + AND (membership='join' OR membership='invite') + AND user_id<>$2 + AND user_id NOT LIKE $3 + `, roomID, store.UserID, store.GhostIDFormat) + if err != nil { + return + } + for rows.Next() { + var userID id.UserID + err := rows.Scan(&userID) + if err != nil { + store.log.Warnfln("Failed to scan member in %s: %v", roomID, err) + } else { + members = append(members, userID) + } + } + return +} + +func (store *SQLCryptoStore) Flush() error { + return nil +} + +func (store *SQLCryptoStore) PutNextBatch(nextBatch string) { + store.SyncToken = nextBatch + _, err := store.db.Exec(`UPDATE crypto_account SET sync_token=$1 WHERE device_id=$2`, store.SyncToken, store.DeviceID) + if err != nil { + store.log.Warnln("Failed to store sync token:", err) + } +} + +func (store *SQLCryptoStore) GetNextBatch() string { + if store.SyncToken == "" { + err := store.db. + QueryRow("SELECT sync_token FROM crypto_account WHERE device_id=$1", store.DeviceID). + Scan(&store.SyncToken) + if err != nil && err != sql.ErrNoRows { + store.log.Warnln("Failed to scan sync token:", err) + } + } + return store.SyncToken +} + +func (store *SQLCryptoStore) PutAccount(account *crypto.OlmAccount) error { + store.Account = account + bytes := account.Internal.Pickle(store.PickleKey) + var err error + if store.db.dialect == "postgres" { + _, err = store.db.Exec(` + INSERT INTO crypto_account (device_id, shared, sync_token, account) VALUES ($1, $2, $3, $4) + ON CONFLICT (device_id) DO UPDATE SET shared=$2, sync_token=$3, account=$4`, + store.DeviceID, account.Shared, store.SyncToken, bytes) + } else if store.db.dialect == "sqlite3" { + _, err = store.db.Exec("INSERT OR REPLACE INTO crypto_account (device_id, shared, sync_token, account) VALUES ($1, $2, $3, $4)", + store.DeviceID, account.Shared, store.SyncToken, bytes) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + if err != nil { + store.log.Warnln("Failed to store account:", err) + } + return nil +} + +func (store *SQLCryptoStore) GetAccount() (*crypto.OlmAccount, error) { + if store.Account == nil { + row := store.db.QueryRow("SELECT shared, sync_token, account FROM crypto_account WHERE device_id=$1", store.DeviceID) + acc := &crypto.OlmAccount{Internal: *olm.NewBlankAccount()} + var accountBytes []byte + err := row.Scan(&acc.Shared, &store.SyncToken, &accountBytes) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + err = acc.Internal.Unpickle(accountBytes, store.PickleKey) + if err != nil { + return nil, err + } + store.Account = acc + } + return store.Account, nil +} + +func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool { + // TODO this may need to be changed if olm sessions start expiring + var sessionID id.SessionID + err := store.db.QueryRow("SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 LIMIT 1", key).Scan(&sessionID) + if err == sql.ErrNoRows { + return false + } + return len(sessionID) > 0 +} + +func (store *SQLCryptoStore) GetSessions(key id.SenderKey) (crypto.OlmSessionList, error) { + rows, err := store.db.Query("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 ORDER BY session_id", key) + if err != nil { + return nil, err + } + list := crypto.OlmSessionList{} + for rows.Next() { + sess := crypto.OlmSession{Internal: *olm.NewBlankSession()} + var sessionBytes []byte + err := rows.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime) + if err != nil { + return nil, err + } + err = sess.Internal.Unpickle(sessionBytes, store.PickleKey) + if err != nil { + return nil, err + } + list = append(list, &sess) + } + return list, nil +} + +func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*crypto.OlmSession, error) { + row := store.db.QueryRow("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 ORDER BY session_id DESC LIMIT 1", key) + sess := crypto.OlmSession{Internal: *olm.NewBlankSession()} + var sessionBytes []byte + err := row.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + return &sess, sess.Internal.Unpickle(sessionBytes, store.PickleKey) +} + +func (store *SQLCryptoStore) AddSession(key id.SenderKey, session *crypto.OlmSession) error { + sessionBytes := session.Internal.Pickle(store.PickleKey) + _, err := store.db.Exec("INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_used) VALUES ($1, $2, $3, $4, $5)", + session.ID(), key, sessionBytes, session.CreationTime, session.UseTime) + return err +} + +func (store *SQLCryptoStore) UpdateSession(key id.SenderKey, session *crypto.OlmSession) error { + sessionBytes := session.Internal.Pickle(store.PickleKey) + _, err := store.db.Exec("UPDATE crypto_olm_session SET session=$1, last_used=$2 WHERE session_id=$3", + sessionBytes, session.UseTime, session.ID()) + return err +} + +func (store *SQLCryptoStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, session *crypto.InboundGroupSession) error { + sessionBytes := session.Internal.Pickle(store.PickleKey) + forwardingChains := strings.Join(session.ForwardingChains, ",") + _, err := store.db.Exec("INSERT INTO crypto_megolm_inbound_session (session_id, sender_key, signing_key, room_id, session, forwarding_chains) VALUES ($1, $2, $3, $4, $5, $6)", + sessionID, senderKey, session.SigningKey, roomID, sessionBytes, forwardingChains) + return err +} + +func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*crypto.InboundGroupSession, error) { + var signingKey id.Ed25519 + var sessionBytes []byte + var forwardingChains string + err := store.db.QueryRow(` + SELECT signing_key, session, forwarding_chains + FROM crypto_megolm_inbound_session + WHERE room_id=$1 AND sender_key=$2 AND session_id=$3`, + roomID, senderKey, sessionID, + ).Scan(&signingKey, &sessionBytes, &forwardingChains) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + igs := olm.NewBlankInboundGroupSession() + err = igs.Unpickle(sessionBytes, store.PickleKey) + if err != nil { + return nil, err + } + return &crypto.InboundGroupSession{ + Internal: *igs, + SigningKey: signingKey, + SenderKey: senderKey, + RoomID: roomID, + ForwardingChains: strings.Split(forwardingChains, ","), + }, nil +} + +func (store *SQLCryptoStore) AddOutboundGroupSession(session *crypto.OutboundGroupSession) (err error) { + sessionBytes := session.Internal.Pickle(store.PickleKey) + if store.db.dialect == "postgres" { + _, err = store.db.Exec(` + INSERT INTO crypto_megolm_outbound_session ( + room_id, session_id, session, shared, max_messages, message_count, max_age, created_at, last_used + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (room_id) DO UPDATE SET session_id=$2, session=$3, shared=$4, max_messages=$5, message_count=$6, max_age=$7, created_at=$8, last_used=$9`, + session.RoomID, session.ID(), sessionBytes, session.Shared, session.MaxMessages, session.MessageCount, session.MaxAge, session.CreationTime, session.UseTime) + } else if store.db.dialect == "sqlite3" { + _, err = store.db.Exec(` + INSERT OR REPLACE INTO crypto_megolm_outbound_session ( + room_id, session_id, session, shared, max_messages, message_count, max_age, created_at, last_used + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + session.RoomID, session.ID(), sessionBytes, session.Shared, session.MaxMessages, session.MessageCount, session.MaxAge, session.CreationTime, session.UseTime) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + return +} + +func (store *SQLCryptoStore) UpdateOutboundGroupSession(session *crypto.OutboundGroupSession) error { + sessionBytes := session.Internal.Pickle(store.PickleKey) + _, err := store.db.Exec("UPDATE crypto_megolm_outbound_session SET session=$1, message_count=$2, last_used=$3 WHERE room_id=$4 AND session_id=$5", + sessionBytes, session.MessageCount, session.UseTime, session.RoomID, session.ID()) + return err +} + +func (store *SQLCryptoStore) GetOutboundGroupSession(roomID id.RoomID) (*crypto.OutboundGroupSession, error) { + var ogs crypto.OutboundGroupSession + var sessionBytes []byte + err := store.db.QueryRow(` + SELECT session, shared, max_messages, message_count, max_age, created_at, last_used + FROM crypto_megolm_outbound_session WHERE room_id=$1`, + roomID, + ).Scan(&sessionBytes, &ogs.Shared, &ogs.MaxMessages, &ogs.MessageCount, &ogs.MaxAge, &ogs.CreationTime, &ogs.UseTime) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + intOGS := olm.NewBlankOutboundGroupSession() + err = intOGS.Unpickle(sessionBytes, store.PickleKey) + if err != nil { + return nil, err + } + ogs.Internal = *intOGS + ogs.RoomID = roomID + return &ogs, nil +} + +func (store *SQLCryptoStore) RemoveOutboundGroupSession(roomID id.RoomID) error { + _, err := store.db.Exec("DELETE FROM crypto_megolm_outbound_session WHERE room_id=$1", roomID) + return err +} + +func (store *SQLCryptoStore) ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) bool { + var resultEventID id.EventID + var resultTimestamp int64 + err := store.db.QueryRow( + `SELECT event_id, timestamp FROM crypto_message_index WHERE sender_key=$1 AND session_id=$2 AND "index"=$3`, + senderKey, sessionID, index, + ).Scan(&resultEventID, &resultTimestamp) + if err == sql.ErrNoRows { + _, err := store.db.Exec(`INSERT INTO crypto_message_index (sender_key, session_id, "index", event_id, timestamp) VALUES ($1, $2, $3, $4, $5)`, + senderKey, sessionID, index, eventID, timestamp) + if err != nil { + store.log.Warnln("Failed to store message index:", err) + } + return true + } else if err != nil { + store.log.Warnln("Failed to scan message index:", err) + return true + } + if resultEventID != eventID || resultTimestamp != timestamp { + return false + } + return true +} + +func (store *SQLCryptoStore) GetDevices(userID id.UserID) (map[id.DeviceID]*crypto.DeviceIdentity, error) { + var ignore id.UserID + err := store.db.QueryRow("SELECT user_id FROM crypto_tracked_user WHERE user_id=$1", userID).Scan(&ignore) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + + rows, err := store.db.Query("SELECT device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1", userID) + if err != nil { + return nil, err + } + data := make(map[id.DeviceID]*crypto.DeviceIdentity) + for rows.Next() { + var identity crypto.DeviceIdentity + err := rows.Scan(&identity.DeviceID, &identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) + if err != nil { + return nil, err + } + identity.UserID = userID + data[identity.DeviceID] = &identity + } + return data, nil +} + +func (store *SQLCryptoStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*crypto.DeviceIdentity, error) { + var identity crypto.DeviceIdentity + err := store.db.QueryRow(` + SELECT identity_key, signing_key, trust, deleted, name + FROM crypto_device WHERE user_id=$1 AND device_id=$2`, + userID, deviceID, + ).Scan(&identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &identity, nil +} + +func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*crypto.DeviceIdentity) error { + tx, err := store.db.Begin() + if err != nil { + return err + } + + if store.db.dialect == "postgres" { + _, err = tx.Exec("INSERT INTO crypto_tracked_user (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) + } else if store.db.dialect == "sqlite3" { + _, err = tx.Exec("INSERT OR IGNORE INTO crypto_tracked_user (user_id) VALUES ($1)", userID) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + if err != nil { + return errors.Wrap(err, "failed to add user to tracked users list") + } + + _, err = tx.Exec("DELETE FROM crypto_device WHERE user_id=$1", userID) + if err != nil { + _ = tx.Rollback() + return errors.Wrap(err, "failed to delete old devices") + } + if len(devices) == 0 { + err = tx.Commit() + if err != nil { + return errors.Wrap(err, "failed to commit changes (no devices added)") + } + return nil + } + // TODO do this in batches to avoid too large db queries + values := make([]interface{}, 1, len(devices)*6+1) + values[0] = userID + valueStrings := make([]string, 0, len(devices)) + i := 2 + for deviceID, identity := range devices { + values = append(values, deviceID, identity.IdentityKey, identity.SigningKey, identity.Trust, identity.Deleted, identity.Name) + valueStrings = append(valueStrings, fmt.Sprintf("($1, $%d, $%d, $%d, $%d, $%d, $%d)", i, i+1, i+2, i+3, i+4, i+5)) + i += 6 + } + valueString := strings.Join(valueStrings, ",") + _, err = tx.Exec("INSERT INTO crypto_device (user_id, device_id, identity_key, signing_key, trust, deleted, name) VALUES "+valueString, values...) + if err != nil { + _ = tx.Rollback() + return errors.Wrap(err, "failed to insert new devices") + } + err = tx.Commit() + if err != nil { + return errors.Wrap(err, "failed to commit changes") + } + return nil +} + +func (store *SQLCryptoStore) FilterTrackedUsers(users []id.UserID) []id.UserID { + var rows *sql.Rows + var err error + if store.db.dialect == "postgres" { + rows, err = store.db.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id = ANY($1)", pq.Array(users)) + } else { + queryString := make([]string, len(users)) + params := make([]interface{}, len(users)) + for i, user := range users { + queryString[i] = fmt.Sprintf("$%d", i+1) + params[i] = user + } + rows, err = store.db.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id IN ("+strings.Join(queryString, ",")+")", params...) + } + if err != nil { + store.log.Warnln("Failed to filter tracked users:", err) + return users + } + var ptr int + for rows.Next() { + err = rows.Scan(&users[ptr]) + if err != nil { + store.log.Warnln("Failed to tracked user ID:", err) + } else { + ptr++ + } + } + return users[:ptr] +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..b217ae4 --- /dev/null +++ b/database/database.go @@ -0,0 +1,81 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package database + +import ( + "database/sql" + + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" + + log "maunium.net/go/maulogger/v2" + + "github.com/kelaresg/matrix-skype/database/upgrades" +) + +type Database struct { + *sql.DB + log log.Logger + dialect string + + User *UserQuery + Portal *PortalQuery + Puppet *PuppetQuery + Message *MessageQuery +} + +func New(dbType string, uri string) (*Database, error) { + conn, err := sql.Open(dbType, uri) + if err != nil { + return nil, err + } + + if dbType == "sqlite3" { + _, _ = conn.Exec("PRAGMA foreign_keys = ON") + } + + db := &Database{ + DB: conn, + log: log.Sub("Database"), + dialect: dbType, + } + db.User = &UserQuery{ + db: db, + log: db.log.Sub("User"), + } + db.Portal = &PortalQuery{ + db: db, + log: db.log.Sub("Portal"), + } + db.Puppet = &PuppetQuery{ + db: db, + log: db.log.Sub("Puppet"), + } + db.Message = &MessageQuery{ + db: db, + log: db.log.Sub("Message"), + } + return db, nil +} + +func (db *Database) Init() error { + return upgrades.Run(db.log.Sub("Upgrade"), db.dialect, db.DB) +} + +type Scannable interface { + Scan(...interface{}) error +} diff --git a/database/message.go b/database/message.go new file mode 100644 index 0000000..fff6f86 --- /dev/null +++ b/database/message.go @@ -0,0 +1,141 @@ +package database + +import ( + "bytes" + "database/sql" + "encoding/json" + skype "github.com/kelaresg/go-skypeapi" + + log "maunium.net/go/maulogger/v2" + + "github.com/kelaresg/matrix-skype/types" + "maunium.net/go/mautrix/id" +) + +type MessageQuery struct { + db *Database + log log.Logger +} + +func (mq *MessageQuery) New() *Message { + return &Message{ + db: mq.db, + log: mq.log, + } +} + +func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) { + rows, err := mq.db.Query("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content FROM message WHERE chat_jid=$1 AND chat_receiver=$2", chat.JID, chat.Receiver) + if err != nil || rows == nil { + return nil + } + defer rows.Close() + for rows.Next() { + messages = append(messages, mq.New().Scan(rows)) + } + return +} + +func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.SkypeMessageID) *Message { + return mq.get("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + + "FROM message WHERE chat_jid=$1 AND jid=$2", chat.JID, jid) +} + +func (mq *MessageQuery) oldGetByJID(chat PortalKey, jid types.SkypeMessageID) *Message { + return mq.get("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + + "FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid) +} + +func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message { + return mq.get("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + + "FROM message WHERE mxid=$1", mxid) +} + +func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message { + msg := mq.get("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + + "FROM message WHERE chat_jid=$1 AND chat_receiver=$2 ORDER BY timestamp DESC LIMIT 1", chat.JID, chat.Receiver) + if msg == nil || msg.Timestamp == 0 { + // Old db, we don't know what the last message is. + return nil + } + return msg +} + +func (mq *MessageQuery) get(query string, args ...interface{}) *Message { + row := mq.db.QueryRow(query, args...) + if row == nil { + return nil + } + return mq.New().Scan(row) +} + +type Message struct { + db *Database + log log.Logger + + ID types.SkypeMessageID + Chat PortalKey + JID types.SkypeMessageID + MXID id.EventID + Sender types.SkypeID + Timestamp uint64 + Content *skype.Resource +} + +func (msg *Message) Scan(row Scannable) *Message { + var content []byte + err := row.Scan(&msg.ID, &msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.Timestamp, &content) + if err != nil { + if err != sql.ErrNoRows { + msg.log.Errorln("Database scan failed:", err) + } + return nil + } + + msg.decodeBinaryContent(content) + + return msg +} + +func (msg *Message) decodeBinaryContent(content []byte) { + msg.Content = &skype.Resource{} + reader := bytes.NewReader(content) + dec := json.NewDecoder(reader) + err := dec.Decode(&msg.Content) + if err != nil { + msg.log.Warnln("Failed to decode message content:", err) + } +} + +func (msg *Message) encodeBinaryContent() []byte { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + err := enc.Encode(msg.Content) + if err != nil { + msg.log.Warnln("Failed to encode message content:", err) + } + return buf.Bytes() +} + +func (msg *Message) Insert() { + _, err := msg.db.Exec("INSERT INTO message (id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content) " + + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + msg.ID, msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.Timestamp, msg.encodeBinaryContent()) + if err != nil { + msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err) + } +} + +func (msg *Message) Delete() { + _, err := msg.db.Exec("DELETE FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", msg.Chat.JID, msg.Chat.Receiver, msg.JID) + if err != nil { + msg.log.Warnfln("Failed to delete %s@%s: %v", msg.Chat, msg.JID, err) + } +} + +func (msg *Message) UpdateIDByJID(id string) { + _, err := msg.db.Exec("UPDATE message SET id=$1 WHERE chat_jid=$2 AND jid=$3", id, msg.Chat.JID, msg.JID) + if err != nil { + msg.log.Warnfln("Failed to UpdateIDByJID %s@%s: %v", msg.Chat.JID, msg.JID, err) + } +} diff --git a/database/migrate.go b/database/migrate.go new file mode 100644 index 0000000..de59a3e --- /dev/null +++ b/database/migrate.go @@ -0,0 +1,152 @@ +package database + +import ( + "fmt" + "math" + "strings" +) + +func countRows(db *Database, table string) (int, error) { + countRow := db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)) + var count int + err := countRow.Scan(&count) + return count, err +} + +const VariableCountLimit = 512 + +func migrateTable(old *Database, new *Database, table string, columns ...string) error { + columnNames := strings.Join(columns, ",") + fmt.Printf("Migrating %s: ", table) + rowCount, err := countRows(old, table) + if err != nil { + return err + } + fmt.Print("found ", rowCount, " rows of data, ") + rows, err := old.Query(fmt.Sprintf("SELECT %s FROM \"%s\"", columnNames, table)) + if err != nil { + return err + } + serverColNames, err := rows.Columns() + if err != nil { + return err + } + colCount := len(serverColNames) + valueStringFormat := strings.Repeat("$%d, ", colCount) + valueStringFormat = fmt.Sprintf("(%s)", valueStringFormat[:len(valueStringFormat)-2]) + cols := make([]interface{}, colCount) + colPtrs := make([]interface{}, colCount) + for i := 0; i < colCount; i++ { + colPtrs[i] = &cols[i] + } + batchSize := VariableCountLimit / colCount + values := make([]interface{}, batchSize*colCount) + valueStrings := make([]string, batchSize) + var inserted int64 + batchCount := int(math.Ceil(float64(rowCount) / float64(batchSize))) + tx, err := new.Begin() + if err != nil { + return err + } + fmt.Printf("migrating in %d batches: ", batchCount) + for rowCount > 0 { + var i int + for ; rows.Next() && i < batchSize; i++ { + colPtrs := make([]interface{}, colCount) + valueStringArgs := make([]interface{}, colCount) + for j := 0; j < colCount; j++ { + pos := i*colCount + j + colPtrs[j] = &values[pos] + valueStringArgs[j] = pos + 1 + } + valueStrings[i] = fmt.Sprintf(valueStringFormat, valueStringArgs...) + err = rows.Scan(colPtrs...) + if err != nil { + panic(err) + } + } + slicedValues := values + slicedValueStrings := valueStrings + if i < len(valueStrings) { + slicedValueStrings = slicedValueStrings[:i] + slicedValues = slicedValues[:i*colCount] + } + res, err := tx.Exec(fmt.Sprintf("INSERT INTO \"%s\" (%s) VALUES %s", table, columnNames, strings.Join(slicedValueStrings, ",")), slicedValues...) + if err != nil { + panic(err) + } + count, _ := res.RowsAffected() + inserted += count + rowCount -= batchSize + fmt.Print("#") + } + err = tx.Commit() + if err != nil { + return err + } + fmt.Println(" -- done with", inserted, "rows inserted") + return nil +} + +func Migrate(old *Database, new *Database) { + err := migrateTable(old, new, "portal", "jid", "receiver", "mxid", "name", "topic", "avatar", "avatar_url", "encrypted") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "user", "mxid", "jid", "management_room", "endpoint_id", "skype_token", "registration_token", "registration_token_str", "location_host", "last_connection") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "puppet", "jid", "avatar", "displayname", "name_quality", "custom_mxid", "access_token", "next_batch", "avatar_url") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "user_portal", "user_jid", "portal_jid", "portal_receiver", "in_community") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "message", "chat_jid", "chat_receiver", "jid", "mxid", "sender", "content", "timestamp") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "mx_registrations", "user_id") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "mx_user_profile", "room_id", "user_id", "membership") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "mx_room_state", "room_id", "power_levels") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_account", "device_id", "shared", "sync_token", "account") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_message_index", "sender_key", "session_id", `"index"`, "event_id", "timestamp") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_tracked_user", "user_id") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_device", "user_id", "device_id", "identity_key", "signing_key", "trust", "deleted", "name") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_olm_session", "session_id", "sender_key", "session", "created_at", "last_used") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_megolm_inbound_session", "session_id", "sender_key", "signing_key", "room_id", "session", "forwarding_chains") + if err != nil { + panic(err) + } + err = migrateTable(old, new, "crypto_megolm_outbound_session", "room_id", "session_id", "session", "shared", "max_messages", "message_count", "max_age", "created_at", "last_used") + if err != nil { + panic(err) + } +} diff --git a/database/portal.go b/database/portal.go new file mode 100644 index 0000000..756d197 --- /dev/null +++ b/database/portal.go @@ -0,0 +1,175 @@ +package database + +import ( + "database/sql" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "strings" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/types" +) + +type PortalKey struct { + JID types.SkypeID + Receiver types.SkypeID +} + +func GroupPortalKey(jid types.SkypeID) PortalKey { + return PortalKey{ + JID: jid, + Receiver: jid, + } +} + +func NewPortalKey(jid, receiver types.SkypeID) PortalKey { + if strings.HasSuffix(jid, skypeExt.GroupSuffix) { + receiver = jid + } + return PortalKey{ + JID: jid, + Receiver: receiver, + } +} + +func (key PortalKey) String() string { + if key.Receiver == key.JID { + return key.JID + } + return key.JID + "-" + key.Receiver +} + +type PortalQuery struct { + db *Database + log log.Logger +} + +func (pq *PortalQuery) New() *Portal { + return &Portal{ + db: pq.db, + log: pq.log, + } +} + +func (pq *PortalQuery) GetAll() []*Portal { + return pq.getAll("SELECT * FROM portal") +} + +func (pq *PortalQuery) GetByJID(key PortalKey) *Portal { + return pq.get("SELECT * FROM portal WHERE jid=$1 AND receiver=$2", key.JID, key.Receiver) +} + +func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal { + return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid) +} + +func (pq *PortalQuery) GetAllByJID(jid types.SkypeID) []*Portal { + return pq.getAll("SELECT * FROM portal WHERE jid=$1", jid) +} + +func (pq *PortalQuery) getAll(query string, args ...interface{}) (portals []*Portal) { + rows, err := pq.db.Query(query, args...) + if err != nil || rows == nil { + return nil + } + defer rows.Close() + for rows.Next() { + portals = append(portals, pq.New().Scan(rows)) + } + return +} + +func (pq *PortalQuery) get(query string, args ...interface{}) *Portal { + row := pq.db.QueryRow(query, args...) + if row == nil { + return nil + } + return pq.New().Scan(row) +} + +type Portal struct { + db *Database + log log.Logger + + Key PortalKey + MXID id.RoomID + + Name string + Topic string + Avatar string + AvatarURL id.ContentURI + Encrypted bool +} + +func (portal *Portal) Scan(row Scannable) *Portal { + var mxid, avatarURL sql.NullString + err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted) + if err != nil { + if err != sql.ErrNoRows { + portal.log.Errorln("Database scan failed:", err) + } + return nil + } + portal.MXID = id.RoomID(mxid.String) + portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String) + return portal +} + +func (portal *Portal) mxidPtr() *id.RoomID { + if len(portal.MXID) > 0 { + return &portal.MXID + } + return nil +} + +func (portal *Portal) Insert() { + _, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted) + if err != nil { + portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) + } +} + +func (portal *Portal) Update() { + var mxid *id.RoomID + if len(portal.MXID) > 0 { + mxid = &portal.MXID + } + _, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6 WHERE jid=$7 AND receiver=$8", + mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.Key.JID, portal.Key.Receiver) + if err != nil { + portal.log.Warnfln("Failed to update %s: %v", portal.Key, err) + } +} + +func (portal *Portal) Delete() { + _, err := portal.db.Exec("DELETE FROM portal WHERE jid=$1 AND receiver=$2", portal.Key.JID, portal.Key.Receiver) + if err != nil { + portal.log.Warnfln("Failed to delete %s: %v", portal.Key, err) + } +} + +func (portal *Portal) GetUserIDs() []id.UserID { + rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal + WHERE "user".jid=user_portal.user_jid + AND user_portal.portal_jid=$1 + AND user_portal.portal_receiver=$2`, + portal.Key.JID, portal.Key.Receiver) + if err != nil { + portal.log.Debugln("Failed to get portal user ids:", err) + return nil + } + var userIDs []id.UserID + for rows.Next() { + var userID id.UserID + err = rows.Scan(&userID) + if err != nil { + portal.log.Warnln("Failed to scan row:", err) + continue + } + userIDs = append(userIDs, userID) + } + return userIDs +} diff --git a/database/puppet.go b/database/puppet.go new file mode 100644 index 0000000..b21db69 --- /dev/null +++ b/database/puppet.go @@ -0,0 +1,113 @@ +package database + +import ( + "database/sql" + + log "maunium.net/go/maulogger/v2" + + "github.com/kelaresg/matrix-skype/types" + "maunium.net/go/mautrix/id" +) + +type PuppetQuery struct { + db *Database + log log.Logger +} + +func (pq *PuppetQuery) New() *Puppet { + return &Puppet{ + db: pq.db, + log: pq.log, + } +} + +func (pq *PuppetQuery) GetAll() (puppets []*Puppet) { + rows, err := pq.db.Query("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch FROM puppet") + if err != nil || rows == nil { + return nil + } + defer rows.Close() + for rows.Next() { + puppets = append(puppets, pq.New().Scan(rows)) + } + return +} + +func (pq *PuppetQuery) Get(jid types.SkypeID) *Puppet { + row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch FROM puppet WHERE jid=$1", jid) + if row == nil { + return nil + } + return pq.New().Scan(row) +} + +func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet { + row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch FROM puppet WHERE custom_mxid=$1", mxid) + if row == nil { + return nil + } + return pq.New().Scan(row) +} + +func (pq *PuppetQuery) GetAllWithCustomMXID() (puppets []*Puppet) { + rows, err := pq.db.Query("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch FROM puppet WHERE custom_mxid<>''") + if err != nil || rows == nil { + return nil + } + defer rows.Close() + for rows.Next() { + puppets = append(puppets, pq.New().Scan(rows)) + } + return +} + +type Puppet struct { + db *Database + log log.Logger + + JID types.SkypeID + Avatar string + AvatarURL id.ContentURI + Displayname string + NameQuality int8 + + CustomMXID id.UserID + AccessToken string + NextBatch string +} + +func (puppet *Puppet) Scan(row Scannable) *Puppet { + var displayname, avatar, avatarURL, customMXID, accessToken, nextBatch sql.NullString + var quality sql.NullInt64 + err := row.Scan(&puppet.JID, &avatar, &avatarURL, &displayname, &quality, &customMXID, &accessToken, &nextBatch) + if err != nil { + if err != sql.ErrNoRows { + puppet.log.Errorln("Database scan failed:", err) + } + return nil + } + puppet.Displayname = displayname.String + puppet.Avatar = avatar.String + puppet.AvatarURL, _ = id.ParseContentURI(avatarURL.String) + puppet.NameQuality = int8(quality.Int64) + puppet.CustomMXID = id.UserID(customMXID.String) + puppet.AccessToken = accessToken.String + puppet.NextBatch = nextBatch.String + return puppet +} + +func (puppet *Puppet) Insert() { + _, err := puppet.db.Exec("INSERT INTO puppet (jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + puppet.JID, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch) + if err != nil { + puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err) + } +} + +func (puppet *Puppet) Update() { + _, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7 WHERE jid=$8", + puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.JID) + if err != nil { + puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err) + } +} diff --git a/database/statestore.go b/database/statestore.go new file mode 100644 index 0000000..cded90b --- /dev/null +++ b/database/statestore.go @@ -0,0 +1,286 @@ +package database + +import ( + "database/sql" + "encoding/json" + "fmt" + "sync" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +type SQLStateStore struct { + *appservice.TypingStateStore + + db *Database + log log.Logger + + Typing map[id.RoomID]map[id.UserID]int64 + typingLock sync.RWMutex +} + +var _ appservice.StateStore = (*SQLStateStore)(nil) + +func NewSQLStateStore(db *Database) *SQLStateStore { + return &SQLStateStore{ + TypingStateStore: appservice.NewTypingStateStore(), + db: db, + log: db.log.Sub("StateStore"), + } +} + +func (store *SQLStateStore) IsRegistered(userID id.UserID) bool { + row := store.db.QueryRow("SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)", userID) + var isRegistered bool + err := row.Scan(&isRegistered) + if err != nil { + store.log.Warnfln("Failed to scan registration existence for %s: %v", userID, err) + } + return isRegistered +} + +func (store *SQLStateStore) MarkRegistered(userID id.UserID) { + var err error + if store.db.dialect == "postgres" { + _, err = store.db.Exec("INSERT INTO mx_registrations (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) + } else if store.db.dialect == "sqlite3" { + _, err = store.db.Exec("INSERT OR REPLACE INTO mx_registrations (user_id) VALUES ($1)", userID) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + if err != nil { + store.log.Warnfln("Failed to mark %s as registered: %v", userID, err) + } +} + +func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent { + members := make(map[id.UserID]*event.MemberEventContent) + rows, err := store.db.Query("SELECT user_id, membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1", roomID) + if err != nil { + return members + } + var userID id.UserID + var member event.MemberEventContent + for rows.Next() { + err := rows.Scan(&userID, &member.Membership, &member.Displayname, &member.AvatarURL) + if err != nil { + store.log.Warnfln("Failed to scan member in %s: %v", roomID, err) + } else { + members[userID] = &member + } + } + return members +} + +func (store *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership { + row := store.db.QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID) + membership := event.MembershipLeave + err := row.Scan(&membership) + if err != nil && err != sql.ErrNoRows { + store.log.Warnfln("Failed to scan membership of %s in %s: %v", userID, roomID, err) + } + return membership +} + +func (store *SQLStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent { + member, ok := store.TryGetMember(roomID, userID) + if !ok { + member.Membership = event.MembershipLeave + } + return member +} + +func (store *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) { + row := store.db.QueryRow("SELECT membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID) + var member event.MemberEventContent + err := row.Scan(&member.Membership, &member.Displayname, &member.AvatarURL) + if err != nil && err != sql.ErrNoRows { + store.log.Warnfln("Failed to scan member info of %s in %s: %v", userID, roomID, err) + } + return &member, err == nil +} + +func (store *SQLStateStore) FindSharedRooms(userID id.UserID) (rooms []id.RoomID) { + rows, err := store.db.Query(` + SELECT room_id FROM mx_user_profile + LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id + WHERE user_id=$1 AND portal.encrypted=true + `, userID) + if err != nil { + store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err) + return + } + for rows.Next() { + var roomID id.RoomID + err := rows.Scan(&roomID) + if err != nil { + store.log.Warnfln("Failed to scan room ID: %v", err) + } else { + rooms = append(rooms, roomID) + } + } + return +} + +func (store *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(roomID, userID, "join") +} + +func (store *SQLStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool { + return store.IsMembership(roomID, userID, "join", "invite") +} + +func (store *SQLStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool { + membership := store.GetMembership(roomID, userID) + for _, allowedMembership := range allowedMemberships { + if allowedMembership == membership { + return true + } + } + return false +} + +func (store *SQLStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) { + var err error + if store.db.dialect == "postgres" { + _, err = store.db.Exec(`INSERT INTO mx_user_profile (room_id, user_id, membership) VALUES ($1, $2, $3) + ON CONFLICT (room_id, user_id) DO UPDATE SET membership=$3`, roomID, userID, membership) + } else if store.db.dialect == "sqlite3" { + _, err = store.db.Exec("INSERT OR REPLACE INTO mx_user_profile (room_id, user_id, membership) VALUES ($1, $2, $3)", roomID, userID, membership) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + if err != nil { + store.log.Warnfln("Failed to set membership of %s in %s to %s: %v", userID, roomID, membership, err) + } +} + +func (store *SQLStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) { + var err error + if store.db.dialect == "postgres" { + _, err = store.db.Exec(`INSERT INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (room_id, user_id) DO UPDATE SET membership=$3`, roomID, userID, member.Membership, member.Displayname, member.AvatarURL) + } else if store.db.dialect == "sqlite3" { + _, err = store.db.Exec("INSERT OR REPLACE INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, $4, $5)", + roomID, userID, member.Membership, member.Displayname, member.AvatarURL) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + if err != nil { + store.log.Warnfln("Failed to set membership of %s in %s to %s: %v", userID, roomID, member, err) + } +} + +func (store *SQLStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) { + levelsBytes, err := json.Marshal(levels) + if err != nil { + store.log.Errorfln("Failed to marshal power levels of %s: %v", roomID, err) + return + } + if store.db.dialect == "postgres" { + _, err = store.db.Exec(`INSERT INTO mx_room_state (room_id, power_levels) VALUES ($1, $2) + ON CONFLICT (room_id) DO UPDATE SET power_levels=$2`, roomID, levelsBytes) + } else if store.db.dialect == "sqlite3" { + _, err = store.db.Exec("INSERT OR REPLACE INTO mx_room_state (room_id, power_levels) VALUES ($1, $2)", roomID, levelsBytes) + } else { + err = fmt.Errorf("unsupported dialect %s", store.db.dialect) + } + if err != nil { + store.log.Warnfln("Failed to store power levels of %s: %v", roomID, err) + } +} + +func (store *SQLStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent) { + row := store.db.QueryRow("SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID) + if row == nil { + return + } + var data []byte + err := row.Scan(&data) + if err != nil { + store.log.Errorln("Failed to scan power levels of %s: %v", roomID, err) + return + } + levels = &event.PowerLevelsEventContent{} + err = json.Unmarshal(data, levels) + if err != nil { + store.log.Errorln("Failed to parse power levels of %s: %v", roomID, err) + return nil + } + return +} + +func (store *SQLStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int { + if store.db.dialect == "postgres" { + row := store.db.QueryRow(`SELECT + COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) + FROM mx_room_state WHERE room_id=$1`, roomID, userID) + if row == nil { + // Power levels not in db + return 0 + } + var powerLevel int + err := row.Scan(&powerLevel) + if err != nil { + store.log.Errorln("Failed to scan power level of %s in %s: %v", userID, roomID, err) + } + return powerLevel + } + return store.GetPowerLevels(roomID).GetUserLevel(userID) +} + +func (store *SQLStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int { + if store.db.dialect == "postgres" { + defaultType := "events_default" + defaultValue := 0 + if eventType.IsState() { + defaultType = "state_default" + defaultValue = 50 + } + row := store.db.QueryRow(`SELECT + COALESCE((power_levels->'events'->$2)::int, (power_levels->'$3')::int, $4) + FROM mx_room_state WHERE room_id=$1`, roomID, eventType.Type, defaultType, defaultValue) + if row == nil { + // Power levels not in db + return defaultValue + } + var powerLevel int + err := row.Scan(&powerLevel) + if err != nil { + store.log.Errorln("Failed to scan power level for %s in %s: %v", eventType, roomID, err) + } + return powerLevel + } + return store.GetPowerLevels(roomID).GetEventLevel(eventType) +} + +func (store *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool { + if store.db.dialect == "postgres" { + defaultType := "events_default" + defaultValue := 0 + if eventType.IsState() { + defaultType = "state_default" + defaultValue = 50 + } + row := store.db.QueryRow(`SELECT + COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) + >= + COALESCE((power_levels->'events'->$3)::int, (power_levels->'$4')::int, $5) + FROM mx_room_state WHERE room_id=$1`, roomID, userID, eventType.Type, defaultType, defaultValue) + if row == nil { + // Power levels not in db + return defaultValue == 0 + } + var hasPower bool + err := row.Scan(&hasPower) + if err != nil { + store.log.Errorln("Failed to scan power level for %s in %s: %v", eventType, roomID, err) + } + return hasPower + } + return store.GetPowerLevel(roomID, userID) >= store.GetPowerLevelRequirement(roomID, eventType) +} diff --git a/database/upgrades/2018-09-01-initial-schema.go b/database/upgrades/2018-09-01-initial-schema.go new file mode 100644 index 0000000..6e097cc --- /dev/null +++ b/database/upgrades/2018-09-01-initial-schema.go @@ -0,0 +1,67 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[0] = upgrade{"Initial schema", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS portal ( + jid VARCHAR(255), + receiver VARCHAR(255), + mxid VARCHAR(255) UNIQUE, + + name VARCHAR(255) NOT NULL, + topic VARCHAR(255) NOT NULL, + avatar VARCHAR(255) NOT NULL, + + PRIMARY KEY (jid, receiver) + )`) + if err != nil { + return err + } + + _, err = tx.Exec(`CREATE TABLE IF NOT EXISTS puppet ( + jid VARCHAR(255) PRIMARY KEY, + avatar VARCHAR(255), + displayname VARCHAR(255), + name_quality SMALLINT + )`) + if err != nil { + return err + } + + _, err = tx.Exec(`CREATE TABLE IF NOT EXISTS "user" ( + mxid VARCHAR(255) PRIMARY KEY, + jid VARCHAR(255) UNIQUE, + + management_room VARCHAR(255), + + endpoint_id VARCHAR(255), + skype_token VARCHAR(255), + registration_token VARCHAR(255), + registration_token_str VARCHAR(255), + location_host VARCHAR(255) + )`) + if err != nil { + return err + } + + _, err = tx.Exec(`CREATE TABLE IF NOT EXISTS message ( + chat_jid VARCHAR(255), + chat_receiver VARCHAR(255), + jid VARCHAR(255), + mxid VARCHAR(255) NOT NULL UNIQUE, + sender VARCHAR(255) NOT NULL, + content bytea NOT NULL, + + PRIMARY KEY (chat_jid, chat_receiver, jid), + FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) ON DELETE CASCADE + )`) + if err != nil { + return err + } + + return nil + }} +} diff --git a/database/upgrades/2019-05-16-message-delete-cascade.go b/database/upgrades/2019-05-16-message-delete-cascade.go new file mode 100644 index 0000000..97ee5c9 --- /dev/null +++ b/database/upgrades/2019-05-16-message-delete-cascade.go @@ -0,0 +1,31 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[1] = upgrade{"Add ON DELETE CASCADE to message table", func(tx *sql.Tx, ctx context) error { + if ctx.dialect == SQLite { + // SQLite doesn't support constraint updates, but it isn't that careful with constraints anyway. + return nil + } + res, _ := ctx.db.Query(`SELECT EXISTS(SELECT constraint_name FROM information_schema.table_constraints + WHERE table_name='message' AND constraint_name='message_chat_jid_fkey')`) + var exists bool + _ = res.Scan(&exists) + if exists { + _, err := tx.Exec("ALTER TABLE message DROP CONSTRAINT IF EXISTS message_chat_jid_fkey") + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE message ADD CONSTRAINT message_chat_jid_fkey + FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) + ON DELETE CASCADE`) + if err != nil { + return err + } + } + return nil + }} +} diff --git a/database/upgrades/2019-05-21-message-timestamp-column.go b/database/upgrades/2019-05-21-message-timestamp-column.go new file mode 100644 index 0000000..cb93614 --- /dev/null +++ b/database/upgrades/2019-05-21-message-timestamp-column.go @@ -0,0 +1,15 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[2] = upgrade{"Add timestamp column to messages", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec("ALTER TABLE message ADD COLUMN timestamp BIGINT NOT NULL DEFAULT 0") + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2019-05-22-user-last-connection-column.go b/database/upgrades/2019-05-22-user-last-connection-column.go new file mode 100644 index 0000000..3e1a236 --- /dev/null +++ b/database/upgrades/2019-05-22-user-last-connection-column.go @@ -0,0 +1,15 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[3] = upgrade{"Add last_connection column to users", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE "user" ADD COLUMN last_connection BIGINT NOT NULL DEFAULT 0`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2019-05-23-protoupgrade.go b/database/upgrades/2019-05-23-protoupgrade.go new file mode 100644 index 0000000..ce250dd --- /dev/null +++ b/database/upgrades/2019-05-23-protoupgrade.go @@ -0,0 +1,61 @@ +package upgrades + +import ( + "database/sql" + "encoding/json" + "fmt" +) + +func init() { + var keys = []string{"imageMessage", "contactMessage", "locationMessage", "extendedTextMessage", "documentMessage", "audioMessage", "videoMessage"} + upgrades[4] = upgrade{"Update message content to new protocol version. This may take a while.", func(tx *sql.Tx, ctx context) error { + rows, err := ctx.db.Query("SELECT mxid, content FROM message") + if err != nil { + return err + } + for rows.Next() { + var mxid string + var rawContent []byte + err = rows.Scan(&mxid, &rawContent) + if err != nil { + fmt.Println("Error scanning:", err) + continue + } + var content map[string]interface{} + err = json.Unmarshal(rawContent, &content) + if err != nil { + fmt.Printf("Error unmarshaling content of %s: %v\n", mxid, err) + continue + } + + for _, key := range keys { + val, ok := content[key].(map[string]interface{}) + if !ok { + continue + } + ci, ok := val["contextInfo"].(map[string]interface{}) + if !ok { + continue + } + qm, ok := ci["quotedMessage"].([]interface{}) + if !ok { + continue + } + ci["quotedMessage"] = qm[0] + goto save + } + continue + + save: + rawContent, err = json.Marshal(&content) + if err != nil { + fmt.Printf("Error marshaling updated content of %s: %v\n", mxid, err) + } + _, err = tx.Exec("UPDATE message SET content=$1 WHERE mxid=$2", rawContent, mxid) + if err != nil { + fmt.Printf("Error updating row of %s: %v\n", mxid, err) + } + } + return nil + }} +} diff --git a/database/upgrades/2019-05-23-puppet-custom-mxid-columns.go b/database/upgrades/2019-05-23-puppet-custom-mxid-columns.go new file mode 100644 index 0000000..2f17154 --- /dev/null +++ b/database/upgrades/2019-05-23-puppet-custom-mxid-columns.go @@ -0,0 +1,23 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[5] = upgrade{"Add columns to store custom puppet info", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE puppet ADD COLUMN custom_mxid VARCHAR(255)`) + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE puppet ADD COLUMN access_token VARCHAR(1023)`) + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE puppet ADD COLUMN next_batch VARCHAR(255)`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2019-05-28-user-portal-table.go b/database/upgrades/2019-05-28-user-portal-table.go new file mode 100644 index 0000000..18d8550 --- /dev/null +++ b/database/upgrades/2019-05-28-user-portal-table.go @@ -0,0 +1,19 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[6] = upgrade{"Add user-portal mapping table", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`CREATE TABLE user_portal ( + user_jid VARCHAR(255), + portal_jid VARCHAR(255), + portal_receiver VARCHAR(255), + PRIMARY KEY (user_jid, portal_jid, portal_receiver), + FOREIGN KEY (user_jid) REFERENCES "user"(jid) ON DELETE CASCADE, + FOREIGN KEY (portal_jid, portal_receiver) REFERENCES portal(jid, receiver) ON DELETE CASCADE + )`) + return err + }} +} diff --git a/database/upgrades/2019-06-01-avatar-url-fields.go b/database/upgrades/2019-06-01-avatar-url-fields.go new file mode 100644 index 0000000..938b291 --- /dev/null +++ b/database/upgrades/2019-06-01-avatar-url-fields.go @@ -0,0 +1,19 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[7] = upgrade{"Add columns to store avatar MXC URIs", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE puppet ADD COLUMN avatar_url VARCHAR(255)`) + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE portal ADD COLUMN avatar_url VARCHAR(255)`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2019-08-10-portal-in-community-field.go b/database/upgrades/2019-08-10-portal-in-community-field.go new file mode 100644 index 0000000..44893fd --- /dev/null +++ b/database/upgrades/2019-08-10-portal-in-community-field.go @@ -0,0 +1,12 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[8] = upgrade{"Add columns to store portal in filtering community meta", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE user_portal ADD COLUMN in_community BOOLEAN NOT NULL DEFAULT FALSE`) + return err + }} +} diff --git a/database/upgrades/2019-08-25-move-state-store-to-db.go b/database/upgrades/2019-08-25-move-state-store-to-db.go new file mode 100644 index 0000000..cbb6001 --- /dev/null +++ b/database/upgrades/2019-08-25-move-state-store-to-db.go @@ -0,0 +1,141 @@ +package upgrades + +import ( + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "strings" + + "maunium.net/go/mautrix/event" +) + +func init() { + migrateRegistrations := func(tx *sql.Tx, registrations map[string]bool) error { + if len(registrations) == 0 { + return nil + } + + executeBatch := func(tx *sql.Tx, valueStrings []string, values ...interface{}) error { + valueString := strings.Join(valueStrings, ",") + _, err := tx.Exec("INSERT INTO mx_registrations (user_id) VALUES "+valueString, values...) + return err + } + + batchSize := 100 + values := make([]interface{}, 0, batchSize) + valueStrings := make([]string, 0, batchSize) + i := 1 + for userID, registered := range registrations { + if i == batchSize { + err := executeBatch(tx, valueStrings, values...) + if err != nil { + return err + } + i = 1 + values = make([]interface{}, 0, batchSize) + valueStrings = make([]string, 0, batchSize) + } + if registered { + values = append(values, userID) + valueStrings = append(valueStrings, fmt.Sprintf("($%d)", i)) + i++ + } + } + return executeBatch(tx, valueStrings, values...) + } + + migrateMemberships := func(tx *sql.Tx, rooms map[string]map[string]event.Membership) error { + for roomID, members := range rooms { + if len(members) == 0 { + continue + } + var values []interface{} + var valueStrings []string + i := 1 + for userID, membership := range members { + values = append(values, roomID, userID, membership) + valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d, $%d)", i, i+1, i+2)) + i += 3 + } + valueString := strings.Join(valueStrings, ",") + _, err := tx.Exec("INSERT INTO mx_user_profile (room_id, user_id, membership) VALUES "+valueString, values...) + if err != nil { + return err + } + } + return nil + } + + migratePowerLevels := func(tx *sql.Tx, rooms map[string]*event.PowerLevelsEventContent) error { + if len(rooms) == 0 { + return nil + } + var values []interface{} + var valueStrings []string + i := 1 + for roomID, powerLevels := range rooms { + powerLevelBytes, err := json.Marshal(powerLevels) + if err != nil { + return err + } + values = append(values, roomID, powerLevelBytes) + valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d)", i, i+1)) + i += 2 + } + valueString := strings.Join(valueStrings, ",") + _, err := tx.Exec("INSERT INTO mx_room_state (room_id, power_levels) VALUES "+valueString, values...) + return err + } + + userProfileTable := `CREATE TABLE mx_user_profile ( + room_id VARCHAR(255), + user_id VARCHAR(255), + membership VARCHAR(15) NOT NULL, + PRIMARY KEY (room_id, user_id) + )` + + roomStateTable := `CREATE TABLE mx_room_state ( + room_id VARCHAR(255) PRIMARY KEY, + power_levels TEXT + )` + + registrationsTable := `CREATE TABLE mx_registrations ( + user_id VARCHAR(255) PRIMARY KEY + )` + + type TempStateStore struct { + Registrations map[string]bool `json:"registrations"` + Members map[string]map[string]event.Membership `json:"memberships"` + PowerLevels map[string]*event.PowerLevelsEventContent `json:"power_levels"` + } + + upgrades[9] = upgrade{"Move state store to main DB", func(tx *sql.Tx, ctx context) error { + if ctx.dialect == Postgres { + roomStateTable = strings.Replace(roomStateTable, "TEXT", "JSONB", 1) + } + + var store TempStateStore + if _, err := tx.Exec(userProfileTable); err != nil { + return err + } else if _, err = tx.Exec(roomStateTable); err != nil { + return err + } else if _, err = tx.Exec(registrationsTable); err != nil { + return err + } else if data, err := ioutil.ReadFile("mx-state.json"); err != nil { + ctx.log.Debugln("mx-state.json not found, not migrating state store") + } else if err = json.Unmarshal(data, &store); err != nil { + return err + } else if err = migrateRegistrations(tx, store.Registrations); err != nil { + return err + } else if err = migrateMemberships(tx, store.Members); err != nil { + return err + } else if err = migratePowerLevels(tx, store.PowerLevels); err != nil { + return err + } else if err = os.Rename("mx-state.json", "mx-state.json.bak"); err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2019-11-10-full-member-state-store.go b/database/upgrades/2019-11-10-full-member-state-store.go new file mode 100644 index 0000000..4040e7f --- /dev/null +++ b/database/upgrades/2019-11-10-full-member-state-store.go @@ -0,0 +1,16 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[10] = upgrade{"Add columns to store full member info in state store", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE mx_user_profile ADD COLUMN displayname TEXT`) + if err != nil { + return err + } + _, err = tx.Exec(`ALTER TABLE mx_user_profile ADD COLUMN avatar_url VARCHAR(255)`) + return err + }} +} diff --git a/database/upgrades/2019-11-12-fix-room-topic-length.go b/database/upgrades/2019-11-12-fix-room-topic-length.go new file mode 100644 index 0000000..3532d35 --- /dev/null +++ b/database/upgrades/2019-11-12-fix-room-topic-length.go @@ -0,0 +1,16 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[11] = upgrade{"Adjust the length of column topic in portal", func(tx *sql.Tx, ctx context) error { + if ctx.dialect == SQLite { + // SQLite doesn't support constraint updates, but it isn't that careful with constraints anyway. + return nil + } + _, err := tx.Exec(`ALTER TABLE portal ALTER COLUMN topic TYPE VARCHAR(512)`) + return err + }} +} diff --git a/database/upgrades/2020-05-09-add-portal-encrypted-field.go b/database/upgrades/2020-05-09-add-portal-encrypted-field.go new file mode 100644 index 0000000..ef0f963 --- /dev/null +++ b/database/upgrades/2020-05-09-add-portal-encrypted-field.go @@ -0,0 +1,12 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[12] = upgrade{"Add encryption status to portal table", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE portal ADD COLUMN encrypted BOOLEAN NOT NULL DEFAULT false`) + return err + }} +} diff --git a/database/upgrades/2020-05-09-crypto-store.go b/database/upgrades/2020-05-09-crypto-store.go new file mode 100644 index 0000000..8be6cd8 --- /dev/null +++ b/database/upgrades/2020-05-09-crypto-store.go @@ -0,0 +1,73 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[13] = upgrade{"Add crypto store to database", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`CREATE TABLE crypto_account ( + device_id VARCHAR(255) PRIMARY KEY, + shared BOOLEAN NOT NULL, + sync_token TEXT NOT NULL, + account bytea NOT NULL + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE crypto_message_index ( + sender_key CHAR(43), + session_id CHAR(43), + "index" INTEGER, + event_id VARCHAR(255) NOT NULL, + timestamp BIGINT NOT NULL, + + PRIMARY KEY (sender_key, session_id, "index") + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE crypto_tracked_user ( + user_id VARCHAR(255) PRIMARY KEY + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE crypto_device ( + user_id VARCHAR(255), + device_id VARCHAR(255), + identity_key CHAR(43) NOT NULL, + signing_key CHAR(43) NOT NULL, + trust SMALLINT NOT NULL, + deleted BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + + PRIMARY KEY (user_id, device_id) + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE crypto_olm_session ( + session_id CHAR(43) PRIMARY KEY, + sender_key CHAR(43) NOT NULL, + session bytea NOT NULL, + created_at timestamp NOT NULL, + last_used timestamp NOT NULL + )`) + if err != nil { + return err + } + _, err = tx.Exec(`CREATE TABLE crypto_megolm_inbound_session ( + session_id CHAR(43) PRIMARY KEY, + sender_key CHAR(43) NOT NULL, + signing_key CHAR(43) NOT NULL, + room_id VARCHAR(255) NOT NULL, + session bytea NOT NULL, + forwarding_chains bytea NOT NULL + )`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2020-05-12-outbound-group-session-store.go b/database/upgrades/2020-05-12-outbound-group-session-store.go new file mode 100644 index 0000000..0f108a6 --- /dev/null +++ b/database/upgrades/2020-05-12-outbound-group-session-store.go @@ -0,0 +1,25 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[14] = upgrade{"Add outbound group sessions to database", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`CREATE TABLE crypto_megolm_outbound_session ( + room_id VARCHAR(255) PRIMARY KEY, + session_id CHAR(43) NOT NULL UNIQUE, + session bytea NOT NULL, + shared BOOLEAN NOT NULL, + max_messages INTEGER NOT NULL, + message_count INTEGER NOT NULL, + max_age BIGINT NOT NULL, + created_at timestamp NOT NULL, + last_used timestamp NOT NULL + )`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/2020-08-25-message-id-column.go b/database/upgrades/2020-08-25-message-id-column.go new file mode 100644 index 0000000..79139f2 --- /dev/null +++ b/database/upgrades/2020-08-25-message-id-column.go @@ -0,0 +1,15 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[15] = upgrade{"Add id column to messages", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE message ADD COLUMN id CHAR(13) DEFAULT ""`) + if err != nil { + return err + } + return nil + }} +} diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go new file mode 100644 index 0000000..9b1d572 --- /dev/null +++ b/database/upgrades/upgrades.go @@ -0,0 +1,101 @@ +package upgrades + +import ( + "database/sql" + "fmt" + "strings" + + log "maunium.net/go/maulogger/v2" +) + +type Dialect int + +const ( + Postgres Dialect = iota + SQLite +) + +type upgradeFunc func(*sql.Tx, context) error + +type context struct { + dialect Dialect + db *sql.DB + log log.Logger +} + +type upgrade struct { + message string + fn upgradeFunc +} + +const NumberOfUpgrades = 16 + +var upgrades [NumberOfUpgrades]upgrade + +var UnsupportedDatabaseVersion = fmt.Errorf("unsupported database version") + +func GetVersion(db *sql.DB) (int, error) { + _, err := db.Exec("CREATE TABLE IF NOT EXISTS version (version INTEGER)") + if err != nil { + return -1, err + } + + version := 0 + row := db.QueryRow("SELECT version FROM version LIMIT 1") + if row != nil { + _ = row.Scan(&version) + } + return version, nil +} + +func SetVersion(tx *sql.Tx, version int) error { + _, err := tx.Exec("DELETE FROM version") + if err != nil { + return err + } + _, err = tx.Exec("INSERT INTO version (version) VALUES ($1)", version) + return err +} + +func Run(log log.Logger, dialectName string, db *sql.DB) error { + var dialect Dialect + switch strings.ToLower(dialectName) { + case "postgres": + dialect = Postgres + case "sqlite3": + dialect = SQLite + default: + return fmt.Errorf("unknown dialect %s", dialectName) + } + + version, err := GetVersion(db) + if err != nil { + return err + } + + if version > NumberOfUpgrades { + return UnsupportedDatabaseVersion + } + + log.Infofln("Database currently on v%d, latest: v%d", version, NumberOfUpgrades) + for i, upgrade := range upgrades[version:] { + log.Infofln("Upgrading database to v%d: %s", version+i+1, upgrade.message) + tx, err := db.Begin() + if err != nil { + return err + } + err = upgrade.fn(tx, context{dialect, db, log}) + if err != nil { + return err + } + err = SetVersion(tx, version+i+1) + if err != nil { + return err + } + err = tx.Commit() + if err != nil { + return err + } + } + return nil +} diff --git a/database/user.go b/database/user.go new file mode 100644 index 0000000..14ed243 --- /dev/null +++ b/database/user.go @@ -0,0 +1,230 @@ +package database + +import ( + "database/sql" + "fmt" + skype "github.com/kelaresg/go-skypeapi" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "strings" + "time" + + log "maunium.net/go/maulogger/v2" + + "github.com/kelaresg/matrix-skype/types" + "maunium.net/go/mautrix/id" +) + +type UserQuery struct { + db *Database + log log.Logger +} + +func (uq *UserQuery) New() *User { + return &User{ + db: uq.db, + log: uq.log, + } +} + +func (uq *UserQuery) GetAll() (users []*User) { + rows, err := uq.db.Query(`SELECT mxid, jid, management_room, last_connection, endpoint_id, skype_token, registration_token, registration_token_str, location_host FROM "user"`) + if err != nil || rows == nil { + return nil + } + defer rows.Close() + for rows.Next() { + users = append(users, uq.New().Scan(rows)) + } + return +} + +func (uq *UserQuery) GetByMXID(userID id.UserID) *User { + row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, endpoint_id, skype_token, registration_token, registration_token_str, location_host FROM "user" WHERE mxid=$1`, userID) + if row == nil { + return nil + } + return uq.New().Scan(row) +} + +func (uq *UserQuery) GetByJID(userID types.SkypeID) *User { + row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, endpoint_id, skype_token, registration_token, registration_token_str, location_host FROM "user" WHERE jid=$1`, stripSuffix(userID)) + if row == nil { + return nil + } + return uq.New().Scan(row) +} + +type User struct { + db *Database + log log.Logger + + MXID id.UserID + JID types.SkypeID + ManagementRoom id.RoomID + Session *skype.Session + LastConnection uint64 +} + +func (user *User) Scan(row Scannable) *User { + var jid, endpointId, skypeToken, registrationToken, registrationTokenStr, locationHost sql.NullString + err := row.Scan(&user.MXID, &jid, &user.ManagementRoom, &user.LastConnection, &endpointId, &skypeToken, ®istrationToken, ®istrationTokenStr, &locationHost) + if err != nil { + if err != sql.ErrNoRows { + user.log.Errorln("Database scan failed:", err) + } + return nil + } + if len(jid.String) > 0 && len(endpointId.String) > 0 { + user.JID = jid.String + skypeExt.NewUserSuffix + user.Session = &skype.Session{ + EndpointId: endpointId.String, + SkypeToken: skypeToken.String, + RegistrationToken: registrationToken.String, + RegistrationTokenStr: registrationTokenStr.String, + LocationHost: locationHost.String, + } + } else { + user.Session = nil + } + return user +} + +func stripSuffix(jid types.SkypeID) string { + if len(jid) == 0 { + return jid + } + + index := strings.IndexRune(jid, '@') + if index < 0 { + return jid + } + + return jid[:index] +} + +func (user *User) jidPtr() *string { + if len(user.JID) > 0 { + str := stripSuffix(user.JID) + return &str + } + return nil +} + +func (user *User) sessionUnptr() (sess skype.Session) { + if user.Session != nil { + sess = *user.Session + } + return +} + +func (user *User) Insert() { + sess := user.sessionUnptr() + _, err := user.db.Exec(`INSERT INTO "user" (mxid, jid, management_room, last_connection, endpoint_id, skype_token, registration_token, registration_token_str, location_host) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + user.MXID, user.jidPtr(), + user.ManagementRoom, user.LastConnection, + sess.EndpointId, sess.SkypeToken, sess.RegistrationToken, sess.RegistrationTokenStr, sess.LocationHost) + if err != nil { + user.log.Warnfln("Failed to insert %s: %v", user.MXID, err) + } +} + +func (user *User) UpdateLastConnection() { + user.LastConnection = uint64(time.Now().Unix()) + _, err := user.db.Exec(`UPDATE "user" SET last_connection=$1 WHERE mxid=$2`, + user.LastConnection, user.MXID) + if err != nil { + user.log.Warnfln("Failed to update last connection ts: %v", err) + } +} + +func (user *User) Update() { + sess := user.sessionUnptr() + _, err := user.db.Exec(`UPDATE "user" SET jid=$1, management_room=$2, last_connection=$3, endpoint_id=$4, skype_token=$5, registration_token=$6, registration_token_str=$7, location_host=$8 WHERE mxid=$9`, + user.jidPtr(), user.ManagementRoom, user.LastConnection, + sess.EndpointId, sess.SkypeToken, sess.RegistrationToken, sess.RegistrationTokenStr, sess.LocationHost, + user.MXID) + if err != nil { + user.log.Warnfln("Failed to update %s: %v", user.MXID, err) + } +} + +type PortalKeyWithMeta struct { + PortalKey + InCommunity bool +} + +func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error { + tx, err := user.db.Begin() + if err != nil { + return err + } + _, err = tx.Exec("DELETE FROM user_portal WHERE user_jid=$1", user.jidPtr()) + if err != nil { + _ = tx.Rollback() + return err + } + valueStrings := make([]string, len(newKeys)) + values := make([]interface{}, len(newKeys)*4) + for i, key := range newKeys { + pos := i * 4 + valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4) + values[pos] = user.jidPtr() + values[pos+1] = key.JID + values[pos+2] = key.Receiver + values[pos+3] = key.InCommunity + } + query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s", + strings.Join(valueStrings, ", ")) + _, err = tx.Exec(query, values...) + if err != nil { + _ = tx.Rollback() + return err + } + return tx.Commit() +} + +func (user *User) IsInPortal(key PortalKey) bool { + row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver) + var exists bool + _ = row.Scan(&exists) + return exists +} + +func (user *User) GetPortalKeys() []PortalKey { + rows, err := user.db.Query(`SELECT portal_jid, portal_receiver FROM user_portal WHERE user_jid=$1`, user.jidPtr()) + if err != nil { + user.log.Warnln("Failed to get user portal keys:", err) + return nil + } + var keys []PortalKey + for rows.Next() { + var key PortalKey + err = rows.Scan(&key.JID, &key.Receiver) + if err != nil { + user.log.Warnln("Failed to scan row:", err) + continue + } + keys = append(keys, key) + } + return keys +} + +func (user *User) GetInCommunityMap() map[PortalKey]bool { + rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr()) + if err != nil { + user.log.Warnln("Failed to get user portal keys:", err) + return nil + } + keys := make(map[PortalKey]bool) + for rows.Next() { + var key PortalKey + var inCommunity bool + err = rows.Scan(&key.JID, &key.Receiver, &inCommunity) + if err != nil { + user.log.Warnln("Failed to scan row:", err) + continue + } + keys[key] = inCommunity + } + return keys +} diff --git a/docker-run.sh b/docker-run.sh new file mode 100755 index 0000000..a55cae1 --- /dev/null +++ b/docker-run.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +if [[ -z "$GID" ]]; then + GID="$UID" +fi + +# Define functions. +function fixperms { + chown -R $UID:$GID /data /opt/matrix-skype +} + +if [[ ! -f /data/config.yaml ]]; then + cp /opt/matrix-skype/example-config.yaml /data/config.yaml + echo "Didn't find a config file." + echo "Copied default config file to /data/config.yaml" + echo "Modify that config file to your liking." + echo "Start the container again after that to generate the registration file." + exit +fi + +if [[ ! -f /data/registration.yaml ]]; then + /usr/bin/matrix-skype -g -c /data/config.yaml -r /data/registration.yaml + echo "Didn't find a registration file." + echo "Generated one for you." + echo "Copy that over to synapses app service directory." + exit +fi + +cd /data +fixperms +exec su-exec $UID:$GID /usr/bin/matrix-skype diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..e1281ba --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,217 @@ +# Homeserver details. +homeserver: + # The address that this appservice can use to connect to the homeserver. + address: https://example.com + # The domain of the homeserver (for MXIDs, etc). + domain: example.com + +# Application service host/registration related details. +# Changing these values requires regeneration of the registration. +appservice: + # The address that the homeserver can use to connect to this appservice. + address: http://localhost:29319 + + # The hostname and port where this appservice should listen. + hostname: 0.0.0.0 + port: 29319 + + # Database config. + database: + # The database type. "sqlite3" and "postgres" are supported. + type: sqlite3 + # The database URI. + # SQLite: File name is enough. https://github.com/mattn/go-sqlite3#connection-string + # Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable + uri: matrix-skype.db + # Maximum number of connections. Mostly relevant for Postgres. + max_open_conns: 20 + max_idle_conns: 2 + + # Settings for provisioning API + provisioning: + # Prefix for the provisioning API paths. + prefix: /_matrix/provision/v1 + # Shared secret for authentication. If set to "disable", the provisioning API will be disabled. + shared_secret: disable + + # The unique ID of this appservice. + id: skype + # Appservice bot details. + bot: + # Username of the appservice bot. + username: skypebridgebot + # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty + # to leave display name/avatar as-is. + displayname: Skype bridge bot + avatar: https://secure.skypeassets.com/wcss/8-61-0-80/images/favicons/favicon.ico + + # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. + as_token: "This value is generated when generating the registration" + hs_token: "This value is generated when generating the registration" + +# Bridge config +bridge: + # Localpart template of MXIDs for Skype users. + # {{.}} is replaced with the phone number of the Skype user. + username_template: skype&{{.}} + # Displayname template for Skype users. + # {{.Notify}} - nickname set by the Skype user + # {{.Jid}} - phone number (international format) + # The following variables are also available, but will cause problems on multi-user instances: + # {{.Name}} - display name from contact list + # {{.Short}} - short display name from contact list + # To use multiple if's, you need to use: {{else if .Name}}, for example: + # "{{if .Notify}}{{.Notify}}{{else if .Name}}{{.Name}}{{else}}{{.Jid}}{{end}} (WA)" + displayname_template: "{{if .DisplayName}}{{.DisplayName}}{{else}}{{.PersonId}}{{end}} (Skype)" + # Localpart template for per-user room grouping community IDs. + # On startup, the bridge will try to create these communities, add all of the specific user's + # portals to the community, and invite the Matrix user to it. + # (Note that, by default, non-admins might not have your homeserver's permission to create + # communities.) + # {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user. + community_template: skype&{{.Localpart}}={{.Server}} + + # Skype connection timeout in seconds. + connection_timeout: 20 + # If Skype doesn't respond within connection_timeout, should the bridge try to fetch the message + # to see if it was actually bridged? Use this if you have problems with sends timing out but actually + # succeeding. + fetch_message_on_timeout: false + # Whether or not the bridge should send a read receipt from the bridge bot when a message has been + # sent to Skype. If fetch_message_on_timeout is enabled, a successful post-timeout fetch will + # trigger a read receipt too. + delivery_receipts: false + # Number of times to regenerate QR code when logging in. + # The regenerated QR code is sent as an edit and essentially multiplies the login timeout (20 seconds) + login_qr_regen_count: 2 + # Maximum number of times to retry connecting on connection error. + max_connection_attempts: 3 + # Number of seconds to wait between connection attempts. + # Negative numbers are exponential backoff: -connection_retry_delay + 1 + 2^attempts + connection_retry_delay: -1 + # Whether or not the bridge should send a notice to the user's management room when it retries connecting. + # If false, it will only report when it stops retrying. + report_connection_retry: true + # Maximum number of seconds to wait for chats to be sent at startup. + # If this is too low and you have lots of chats, it could cause backfilling to fail. + chat_list_wait: 30 + # Maximum number of seconds to wait to sync portals before force unlocking message processing. + # If this is too low and you have lots of chats, it could cause backfilling to fail. + portal_sync_wait: 600 + + # Whether or not to send call start/end notices to Matrix. + call_notices: + start: true + end: true + + # Number of chats to sync for new users. + initial_chat_sync_count: 10 + # Number of old messages to fill when creating new portal rooms. + initial_history_fill_count: 20 + # Whether or not notifications should be turned off while filling initial history. + # Only applicable when using double puppeting. + initial_history_disable_notifications: false + # Maximum number of chats to sync when recovering from downtime. + # Set to -1 to sync all new chats during downtime. + recovery_chat_sync_limit: -1 + # Whether or not to sync history when recovering from downtime. + recovery_history_backfill: true + # Maximum number of seconds since last message in chat to skip + # syncing the chat in any case. This setting will take priority + # over both recovery_chat_sync_limit and initial_chat_sync_count. + # Default is 3 days = 259200 seconds + sync_max_chat_age: 259200 + + # Whether or not to sync with custom puppets to receive EDUs that + # are not normally sent to appservices. + sync_with_custom_puppets: true + # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth + # + # If set, custom puppets will be enabled automatically for local users + # instead of users having to find an access token and run `login-matrix` + # manually. + login_shared_secret: null + + # Whether or not to invite own Skype user's Matrix puppet into private + # chat portals when backfilling if needed. + # This always uses the default puppet instead of custom puppets due to + # rate limits and timestamp massaging. + invite_own_puppet_for_backfilling: true + # Whether or not to explicitly set the avatar and room name for private + # chat portal rooms. This can be useful if the previous field works fine, + # but causes room avatar/name bugs. + private_chat_portal_meta: false + + # Whether or not thumbnails from Skype should be sent. + # They're disabled by default due to very low resolution. + Skype_thumbnail: false + + # Allow invite permission for user. User can invite any bots to room with Skype + # users (private chat and groups) + allow_user_invite: false + + # The prefix for commands. Only required in non-management rooms. + command_prefix: "!wa" + + # End-to-bridge encryption support options. This requires login_shared_secret to be configured + # in order to get a device for the bridge bot. + # + # Additionally, https://github.com/matrix-org/synapse/pull/5758 is required if using a normal + # application service. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + # It is recommended to also set private_chat_portal_meta to true when using this. + default: false + + # Permissions for using the bridge. + # Permitted values: + # relaybot - Talk through the relaybot (if enabled), no access otherwise + # user - Access to use the bridge to chat with a Skype account. + # admin - User level and some additional administration tools + # Permitted keys: + # * - All Matrix users + # domain - All users on that homeserver + # mxid - Specific user + permissions: + "*": relaybot + "example.com": user + "@admin:example.com": admin + + relaybot: + # Whether or not relaybot support is enabled. + enabled: false + # The management room for the bot. This is where all status notifications are posted and + # in this room, you can use `!wa ` instead of `!wa relaybot `. Omitting + # the command prefix completely like in user management rooms is not possible. + management: !foo:example.com + # List of users to invite to all created rooms that include the relaybot. + invites: [] + # The formats to use when sending messages to Skype via the relaybot. + message_formats: + m.text: "{{ .Sender.Displayname }}: {{ .Message }}" + m.notice: "{{ .Sender.Displayname }}: {{ .Message }}" + m.emote: "* {{ .Sender.Displayname }} {{ .Message }}" + m.file: "{{ .Sender.Displayname }} sent a file" + m.image: "{{ .Sender.Displayname }} sent an image" + m.audio: "{{ .Sender.Displayname }} sent an audio file" + m.video: "{{ .Sender.Displayname }} sent a video" + m.location: "{{ .Sender.Displayname }} sent a location" + +# Logging config. +logging: + # The directory for log files. Will be created if not found. + directory: ./logs + # Available variables: .Date for the file date and .Index for different log files on the same day. + file_name_format: "{{.Date}}-{{.Index}}.log" + # Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants + file_date_format: 2006-01-02 + # Log file permissions. + file_mode: 0600 + # Timestamp format for log entries in the Go time format. + timestamp_format: Jan _2, 2006 15:04:05 + # Minimum severity for log messages. + # Options: debug, info, warn, error, fatal + print_level: debug diff --git a/formatting.go b/formatting.go new file mode 100644 index 0000000..a3ca57d --- /dev/null +++ b/formatting.go @@ -0,0 +1,170 @@ +package main + +import ( + "fmt" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "html" + "regexp" + "strings" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/types" +) + +var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)") +var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)") +var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)") +var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```") +//var mentionRegex = regexp.MustCompile("@[0-9]+") +//var mentionRegex = regexp.MustCompile("@(.*)") +var mentionRegex = regexp.MustCompile("]+\\bid=\"([^\"]+)\"(.*?)*") + +type Formatter struct { + bridge *Bridge + + matrixHTMLParser *format.HTMLParser + + waReplString map[*regexp.Regexp]string + waReplFunc map[*regexp.Regexp]func(string) string + waReplFuncText map[*regexp.Regexp]func(string) string +} + +func NewFormatter(bridge *Bridge) *Formatter { + formatter := &Formatter{ + bridge: bridge, + matrixHTMLParser: &format.HTMLParser{ + TabsToSpaces: 4, + Newline: "\n", + + PillConverter: func(mxid, eventID string) string { + if mxid[0] == '@' { + puppet := bridge.GetPuppetByMXID(id.UserID(mxid)) + if puppet != nil { + return "@" + puppet.PhoneNumber() + } + } + return mxid + }, + BoldConverter: func(text string) string { + return fmt.Sprintf("*%s*", text) + }, + ItalicConverter: func(text string) string { + return fmt.Sprintf("_%s_", text) + }, + StrikethroughConverter: func(text string) string { + return fmt.Sprintf("~%s~", text) + }, + MonospaceConverter: func(text string) string { + return fmt.Sprintf("```%s```", text) + }, + MonospaceBlockConverter: func(text, language string) string { + return fmt.Sprintf("```%s```", text) + }, + }, + waReplString: map[*regexp.Regexp]string{ + italicRegex: "$1$2$3", + boldRegex: "$1$2$3", + strikethroughRegex: "$1$2$3", + }, + } + formatter.waReplFunc = map[*regexp.Regexp]func(string) string{ + codeBlockRegex: func(str string) string { + str = str[3 : len(str)-3] + if strings.ContainsRune(str, '\n') { + return fmt.Sprintf("
%s
", str) + } + return fmt.Sprintf("%s", str) + }, + mentionRegex: func(str string) string { + mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + skypeExt.NewUserSuffix) + return fmt.Sprintf(`%s`, mxid, displayname) + }, + } + formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{ + mentionRegex: func(str string) string { + r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) + matches := r.FindAllStringSubmatch(str, -1) + displayname := "" + var mxid id.UserID + if len(matches) > 0 { + for _, match := range matches { + mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) + } + } + //mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) + return fmt.Sprintf(`%s`, mxid, displayname) + // _, displayname = formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) + //fmt.Println("ParseWhatsAp4", displayname) + //return displayname + }, + } + return formatter +} + +func (formatter *Formatter) getMatrixInfoByJID(jid types.SkypeID) (mxid id.UserID, displayname string) { + if user := formatter.bridge.GetUserByJID(jid); user != nil { + mxid = user.MXID + displayname = string(user.MXID) + } else if puppet := formatter.bridge.GetPuppetByJID(jid); puppet != nil { + mxid = puppet.MXID + displayname = puppet.Displayname + } + return +} + +func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { + output := html.EscapeString(content.Body) + for regex, replacement := range formatter.waReplString { + output = regex.ReplaceAllString(output, replacement) + } + for regex, replacer := range formatter.waReplFunc { + output = regex.ReplaceAllStringFunc(output, replacer) + } + if output != content.Body { + output = strings.Replace(output, "\n", "
", -1) + + // parse @user message + r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) + matches := r.FindAllStringSubmatch(content.Body, -1) + displayname := "" + var mxid id.UserID + if len(matches) > 0 { + for _, match := range matches { + mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) + content.FormattedBody = strings.ReplaceAll(content.Body, match[0], fmt.Sprintf(`%s`, mxid, displayname)) + content.Body = content.FormattedBody + } + } + + // parse quote message + content.Body = strings.ReplaceAll(content.Body, "\n", "") + quoteReg := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)".*>.*?(.*?).*?(.*)`) + quoteMatches := quoteReg.FindAllStringSubmatch(content.Body, -1) + if len(quoteMatches) > 0 { + for _, match := range quoteMatches { + mxid, displayname = formatter.getMatrixInfoByJID("8:" + match[1] + skypeExt.NewUserSuffix) + //href1 := fmt.Sprintf(`https://matrix.to/#/!kpouCkfhzvXgbIJmkP:oliver.matrix.host/$fHQNRydqqqAVS8usHRmXn0nIBM_FC-lo2wI2Uol7wu8?via=oliver.matrix.host`) + href1 := "" + //mxid `@skype&8-live-xxxxxx:name.matrix.server` + href2 := fmt.Sprintf(`https://matrix.to/#/%s`, mxid) + newContent := fmt.Sprintf(`
%s
%s
%s`, + href1, + href2, + mxid, + match[4], + match[5]) + content.FormattedBody = newContent + content.Body = match[4] + "\n" + match[5] + } + } + + content.Format = event.FormatHTML + } +} + +func (formatter *Formatter) ParseMatrix(html string) string { + return formatter.matrixHTMLParser.Parse(html) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f15c092 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/kelaresg/matrix-skype + +go 1.14 + +require ( + github.com/Rhymen/go-whatsapp v0.1.0 + github.com/chai2010/webp v1.1.0 + github.com/gorilla/websocket v1.4.2 + github.com/kelaresg/go-skypeapi v0.1.2-0.20200828122051-fb22fb75dede + github.com/lib/pq v1.5.2 + github.com/mattn/go-sqlite3 v2.0.3+incompatible + github.com/pkg/errors v0.9.1 + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 + golang.org/x/image v0.0.0-20200430140353-33d19683fad8 + gopkg.in/yaml.v2 v2.3.0 + maunium.net/go/mauflag v1.0.0 + maunium.net/go/maulogger/v2 v2.1.1 + maunium.net/go/mautrix v0.5.0-rc.3 +) + +replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8 + +replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4 + diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e5fce9a --- /dev/null +++ b/go.sum @@ -0,0 +1,375 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ= +github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOYvd3Ow= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogf/gf v1.13.0 h1:URnD1CPVAuypmNAUveleraOA9CHRKHdvrn8D67uvKu0= +github.com/gogf/gf v1.13.0/go.mod h1:Ho7d+9F8dHe5LpEnIH+bky0aCtjwc8Gm82rUiCYnk/k= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gqcn/structs v1.1.1 h1:cyzGRwfmn3d1d54fwW3KUNyG9QxR0ldIeqwFGeBt638= +github.com/gqcn/structs v1.1.1/go.mod h1:/aBhTBSsKQ2Ec9pbnYdGphtdWXHFn4KrCL0fXM/Adok= +github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf h1:wIOAyJMMen0ELGiFzlmqxdcV1yGbkyHBAB6PolcNbLA= +github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= +github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4 h1:FkwHv3b/K6jsNo8rFk7VCsdJ7HL61AcjqAS0DqDYLyk= +github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= +github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= +github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= +github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tulir/go-whatsapp v0.2.8 h1:0dpAQ/2ONT6T2//aAl+9IeFGMRtqOZpMEqJlhX9vxos= +github.com/tulir/go-whatsapp v0.2.8/go.mod h1:gyw9zGup1/Y3ZQUueZaqz3iR/WX9a2Lth4aqEbXjkok= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= +maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= +maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= +maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6cd3878 --- /dev/null +++ b/main.go @@ -0,0 +1,400 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + flag "maunium.net/go/mauflag" + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/config" + "github.com/kelaresg/matrix-skype/database" + "github.com/kelaresg/matrix-skype/database/upgrades" + "github.com/kelaresg/matrix-skype/types" +) + +var ( + // These are static + Name = "matrix-skype" + URL = "unknown" + // This is changed when making a release + Version = "0.1.1" + WAVersion = "" + // These are filled at build time with the -X linker flag + Tag = "unknown" + Commit = "unknown" + BuildTime = "unknown" +) + +func init() { + if len(Tag) > 0 && Tag[0] == 'v' { + Tag = Tag[1:] + } + if Tag != Version && !strings.HasSuffix(Version, "+dev") { + Version += "+dev" + } + WAVersion = strings.FieldsFunc(Version, func(r rune) bool { return r == '-' || r == '+' })[0] +} + +var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String() + +//var baseConfigPath = flag.MakeFull("b", "base-config", "The path to the example config file.", "example-config.yaml").String() +var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String() +var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool() +var version = flag.MakeFull("v", "version", "View bridge version and quit.", "false").Bool() +var ignoreUnsupportedDatabase = flag.Make().LongKey("ignore-unsupported-database").Usage("Run even if database is too new").Default("false").Bool() +var migrateFrom = flag.Make().LongKey("migrate-db").Usage("Source database type and URI to migrate from.").Bool() +var wantHelp, _ = flag.MakeHelpFlag() + +func (bridge *Bridge) GenerateRegistration() { + reg, err := bridge.Config.NewRegistration() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to generate registration:", err) + os.Exit(20) + } + + err = reg.Save(*registrationPath) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to save registration:", err) + os.Exit(21) + } + + err = bridge.Config.Save(*configPath) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to save config:", err) + os.Exit(22) + } + fmt.Println("Registration generated. Add the path to the registration to your Synapse config, restart it, then start the bridge.") + os.Exit(0) +} + +func (bridge *Bridge) MigrateDatabase() { + oldDB, err := database.New(flag.Arg(0), flag.Arg(1)) + if err != nil { + fmt.Println("Failed to open old database:", err) + os.Exit(30) + } + err = oldDB.Init() + if err != nil { + fmt.Println("Failed to upgrade old database:", err) + os.Exit(31) + } + + newDB, err := database.New(bridge.Config.AppService.Database.Type, bridge.Config.AppService.Database.URI) + if err != nil { + bridge.Log.Fatalln("Failed to open new database:", err) + os.Exit(32) + } + err = newDB.Init() + if err != nil { + fmt.Println("Failed to upgrade new database:", err) + os.Exit(33) + } + + database.Migrate(oldDB, newDB) +} + +type Bridge struct { + AS *appservice.AppService + EventProcessor *appservice.EventProcessor + MatrixHandler *MatrixHandler + Config *config.Config + DB *database.Database + Log log.Logger + StateStore *database.SQLStateStore + Provisioning *ProvisioningAPI + Bot *appservice.IntentAPI + Formatter *Formatter + Relaybot *User + Crypto Crypto + + usersByMXID map[id.UserID]*User + usersByJID map[types.SkypeID]*User + usersLock sync.Mutex + managementRooms map[id.RoomID]*User + managementRoomsLock sync.Mutex + portalsByMXID map[id.RoomID]*Portal + portalsByJID map[database.PortalKey]*Portal + portalsLock sync.Mutex + puppets map[types.SkypeID]*Puppet + puppetsByCustomMXID map[id.UserID]*Puppet + puppetsLock sync.Mutex +} + +type Crypto interface { + HandleMemberEvent(*event.Event) + Decrypt(*event.Event) (*event.Event, error) + Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error) + Init() error + Start() + Stop() +} + +func NewBridge() *Bridge { + bridge := &Bridge{ + usersByMXID: make(map[id.UserID]*User), + usersByJID: make(map[types.SkypeID]*User), + managementRooms: make(map[id.RoomID]*User), + portalsByMXID: make(map[id.RoomID]*Portal), + portalsByJID: make(map[database.PortalKey]*Portal), + puppets: make(map[types.SkypeID]*Puppet), + puppetsByCustomMXID: make(map[id.UserID]*Puppet), + } + + var err error + bridge.Config, err = config.Load(*configPath) + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to load config:", err) + os.Exit(10) + } + return bridge +} + +func (bridge *Bridge) ensureConnection() { + for { + resp, err := bridge.Bot.Whoami() + if err != nil { + if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_UNKNOWN_ACCESS_TOKEN" { + bridge.Log.Fatalln("Access token invalid. Is the registration installed in your homeserver correctly?") + os.Exit(16) + } + bridge.Log.Errorfln("Failed to connect to homeserver: %v. Retrying in 10 seconds...", err) + time.Sleep(10 * time.Second) + } else if resp.UserID != bridge.Bot.UserID { + bridge.Log.Fatalln("Unexpected user ID in whoami call: got %s, expected %s", resp.UserID, bridge.Bot.UserID) + os.Exit(17) + } else { + break + } + } +} + +func (bridge *Bridge) Init() { + var err error + + bridge.AS, err = bridge.Config.MakeAppService() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to initialize AppService:", err) + os.Exit(11) + } + _, _ = bridge.AS.Init() + bridge.Bot = bridge.AS.BotIntent() + + bridge.Log = log.Create() + bridge.Config.Logging.Configure(bridge.Log) + log.DefaultLogger = bridge.Log.(*log.BasicLogger) + if len(bridge.Config.Logging.FileNameFormat) > 0 { + err = log.OpenFile() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "Failed to open log file:", err) + os.Exit(12) + } + } + bridge.AS.Log = log.Sub("Matrix") + + bridge.Log.Debugln("Initializing database") + bridge.DB, err = database.New(bridge.Config.AppService.Database.Type, bridge.Config.AppService.Database.URI) + if err != nil && (err != upgrades.UnsupportedDatabaseVersion || !*ignoreUnsupportedDatabase) { + bridge.Log.Fatalln("Failed to initialize database:", err) + os.Exit(14) + } + + if len(bridge.Config.AppService.StateStore) > 0 && bridge.Config.AppService.StateStore != "./mx-state.json" { + version, err := upgrades.GetVersion(bridge.DB.DB) + if version < 0 && err == nil { + bridge.Log.Fatalln("Non-standard state store path. Please move the state store to ./mx-state.json " + + "and update the config. The state store will be migrated into the db on the next launch.") + os.Exit(18) + } + } + + bridge.Log.Debugln("Initializing state store") + bridge.StateStore = database.NewSQLStateStore(bridge.DB) + bridge.AS.StateStore = bridge.StateStore + + bridge.DB.SetMaxOpenConns(bridge.Config.AppService.Database.MaxOpenConns) + bridge.DB.SetMaxIdleConns(bridge.Config.AppService.Database.MaxIdleConns) + + ss := bridge.Config.AppService.Provisioning.SharedSecret + if len(ss) > 0 && ss != "disable" { + bridge.Provisioning = &ProvisioningAPI{bridge: bridge} + } + + bridge.Log.Debugln("Initializing Matrix event processor") + bridge.EventProcessor = appservice.NewEventProcessor(bridge.AS) + bridge.Log.Debugln("Initializing Matrix event handler") + bridge.MatrixHandler = NewMatrixHandler(bridge) + bridge.Formatter = NewFormatter(bridge) + bridge.Crypto = NewCryptoHelper(bridge) +} + +func (bridge *Bridge) Start() { + err := bridge.DB.Init() + if err != nil { + bridge.Log.Fatalln("Failed to initialize database:", err) + os.Exit(15) + } + if bridge.Crypto != nil { + err := bridge.Crypto.Init() + if err != nil { + bridge.Log.Fatalln("Error initializing end-to-bridge encryption:", err) + os.Exit(19) + } + } + if bridge.Provisioning != nil { + bridge.Log.Debugln("Initializing provisioning API") + bridge.Provisioning.Init() + } + bridge.LoadRelaybot() + bridge.Log.Debugln("Checking connection to homeserver") + bridge.ensureConnection() + bridge.Log.Debugln("Starting application service HTTP server") + go bridge.AS.Start() + bridge.Log.Debugln("Starting event processor") + go bridge.EventProcessor.Start() + go bridge.UpdateBotProfile() + if bridge.Crypto != nil { + go bridge.Crypto.Start() + } + go bridge.StartUsers() +} + +func (bridge *Bridge) LoadRelaybot() { + if !bridge.Config.Bridge.Relaybot.Enabled { + return + } + bridge.Relaybot = bridge.GetUserByMXID("relaybot") + if bridge.Relaybot.HasSession() { + bridge.Log.Debugln("Relaybot is enabled") + } else { + bridge.Log.Debugln("Relaybot is enabled, but not logged in") + } + bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom + bridge.Relaybot.IsRelaybot = true + bridge.Relaybot.Connect(false) +} + +func (bridge *Bridge) UpdateBotProfile() { + bridge.Log.Debugln("Updating bot profile") + botConfig := bridge.Config.AppService.Bot + + var err error + var mxc id.ContentURI + if botConfig.Avatar == "remove" { + err = bridge.Bot.SetAvatarURL(mxc) + } else if len(botConfig.Avatar) > 0 { + mxc, err = id.ParseContentURI(botConfig.Avatar) + if err == nil { + err = bridge.Bot.SetAvatarURL(mxc) + } + } + if err != nil { + bridge.Log.Warnln("Failed to update bot avatar:", err) + } + + if botConfig.Displayname == "remove" { + err = bridge.Bot.SetDisplayName("") + } else if len(botConfig.Avatar) > 0 { + err = bridge.Bot.SetDisplayName(botConfig.Displayname) + } + if err != nil { + bridge.Log.Warnln("Failed to update bot displayname:", err) + } +} + +func (bridge *Bridge) StartUsers() { + bridge.Log.Debugln("Starting users") + for _, user := range bridge.GetAllUsers() { + go user.Connect(false) + } + bridge.Log.Debugln("Starting custom puppets") + for _, loopuppet := range bridge.GetAllPuppetsWithCustomMXID() { + go func(puppet *Puppet) { + puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID) + err := puppet.StartCustomMXID() + if err != nil { + puppet.log.Errorln("Failed to start custom puppet:", err) + } + }(loopuppet) + } +} + +func (bridge *Bridge) Stop() { + if bridge.Crypto != nil { + bridge.Crypto.Stop() + } + bridge.AS.Stop() + bridge.EventProcessor.Stop() + for _, user := range bridge.usersByJID { + if user.Conn == nil { + continue + } + //bridge.Log.Debugln("Disconnecting", user.MXID) + //sess, err := user.Conn.Disconnect() + //if err != nil { + // bridge.Log.Errorfln("Error while disconnecting %s: %v", user.MXID, err) + //} else if len(sess.Wid) > 0 { + // user.SetSession(&sess) + //} + } +} + +func (bridge *Bridge) Main() { + if *generateRegistration { + bridge.GenerateRegistration() + return + } else if *migrateFrom { + bridge.MigrateDatabase() + return + } + + bridge.Init() + bridge.Log.Infoln("Bridge initialization complete, starting...") + bridge.Start() + bridge.Log.Infoln("Bridge started!") + + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + + bridge.Log.Infoln("Interrupt received, stopping...") + bridge.Stop() + bridge.Log.Infoln("Bridge stopped.") + os.Exit(0) +} + +func main() { + flag.SetHelpTitles( + "matrix-skype - A Matrix-WhatsApp puppeting bridge.", + "matrix-skype [-h] [-c ] [-r ] [-g] [--migrate-db ]") + err := flag.Parse() + if err != nil { + fmt.Fprintln(os.Stderr, err) + flag.PrintHelp() + os.Exit(1) + } else if *wantHelp { + flag.PrintHelp() + os.Exit(0) + } else if *version { + if Tag == Version { + fmt.Printf("%s %s (%s)\n", Name, Tag, BuildTime) + } else if len(Commit) > 8 { + fmt.Printf("%s %s.%s (%s)\n", Name, Version, Commit[:8], BuildTime) + } else { + fmt.Printf("%s %s.unknown\n", Name, Version) + } + return + } + + NewBridge().Main() +} diff --git a/matrix.go b/matrix.go new file mode 100644 index 0000000..6dbbe77 --- /dev/null +++ b/matrix.go @@ -0,0 +1,331 @@ +package main + +import ( + "fmt" + skype "github.com/kelaresg/go-skypeapi" + "strconv" + "strings" + + "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" +) + +type MatrixHandler struct { + bridge *Bridge + as *appservice.AppService + log maulogger.Logger + cmd *CommandHandler +} + +func NewMatrixHandler(bridge *Bridge) *MatrixHandler { + handler := &MatrixHandler{ + bridge: bridge, + as: bridge.AS, + log: bridge.Log.Sub("Matrix"), + cmd: NewCommandHandler(bridge), + } + bridge.EventProcessor.On(event.EventMessage, handler.HandleMessage) + bridge.EventProcessor.On(event.EventEncrypted, handler.HandleEncrypted) + bridge.EventProcessor.On(event.EventSticker, handler.HandleMessage) + bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction) + bridge.EventProcessor.On(event.StateMember, handler.HandleMembership) + bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata) + bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata) + bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata) + bridge.EventProcessor.On(event.StateEncryption, handler.HandleEncryption) + return handler +} + +func (mx *MatrixHandler) HandleEncryption(evt *event.Event) { + if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 { + return + } + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal != nil && !portal.Encrypted { + mx.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID) + portal.Encrypted = true + portal.Update() + } +} + +func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { + intent := mx.as.BotIntent() + + user := mx.bridge.GetUserByMXID(evt.Sender) + if user == nil { + return + } + + resp, err := intent.JoinRoomByID(evt.RoomID) + if err != nil { + mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender) + return + } + + members, err := intent.JoinedMembers(resp.RoomID) + if err != nil { + mx.log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender) + intent.LeaveRoom(resp.RoomID) + return + } + + if len(members.Joined) < 2 { + mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender) + intent.LeaveRoom(resp.RoomID) + return + } + + if !user.Whitelisted { + intent.SendNotice(resp.RoomID, "You are not whitelisted to use this bridge.\n"+ + "If you're the owner of this bridge, see the bridge.permissions section in your config file.") + intent.LeaveRoom(resp.RoomID) + return + } + + if evt.RoomID == mx.bridge.Config.Bridge.Relaybot.ManagementRoom { + intent.SendNotice(evt.RoomID, "This is the relaybot management room. Send `!wa help` to get a list of commands.") + mx.log.Debugln("Joined relaybot management room", evt.RoomID, "after invite from", evt.Sender) + return + } + + hasPuppets := false + for mxid, _ := range members.Joined { + if mxid == intent.UserID || mxid == evt.Sender { + continue + } else if _, ok := mx.bridge.ParsePuppetMXID(mxid); ok { + hasPuppets = true + continue + } + mx.log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender) + intent.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.") + intent.LeaveRoom(resp.RoomID) + return + } + + if !hasPuppets { + user := mx.bridge.GetUserByMXID(evt.Sender) + user.SetManagementRoom(resp.RoomID) + intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.") + mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) + } +} + +func (mx *MatrixHandler) HandleMembership(evt *event.Event) { + if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { + return + } + + if mx.bridge.Crypto != nil { + mx.bridge.Crypto.HandleMemberEvent(evt) + } + + content := evt.Content.AsMember() + if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() { + mx.HandleBotInvite(evt) + } + + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal == nil { + return + } + + user := mx.bridge.GetUserByMXID(evt.Sender) + if user == nil || !user.Whitelisted || !user.IsConnected() { + return + } + + if content.Membership == event.MembershipLeave { + if id.UserID(evt.GetStateKey()) == evt.Sender { + if evt.Unsigned.PrevContent != nil { + _ = evt.Unsigned.PrevContent.ParseRaw(evt.Type) + prevContent, ok := evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent) + if ok { + if portal.IsPrivateChat() || prevContent.Membership == "join" { + portal.HandleMatrixLeave(user) + } + } + } + } else { + portal.HandleMatrixKick(user, evt) + } + } +} + +func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { + user := mx.bridge.GetUserByMXID(evt.Sender) + if user == nil || !user.Whitelisted || !user.IsConnected() { + return + } + + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal == nil || portal.IsPrivateChat() { + return + } + + //var resp <-chan string + var resp string + var err error + switch content := evt.Content.Parsed.(type) { + case *event.RoomNameEventContent: + resp, err = user.Conn.SetConversationThreads(portal.Key.JID, map[string]string{ + "topic": content.Name, + }) + case *event.TopicEventContent: + //resp, err = user.Conn.SetConversationThreads(portal.Key.JID, map[string]string{ + // "topic": content.Topic, + //}) + return + case *event.RoomAvatarEventContent: + data, err := portal.MainIntent().DownloadBytes(content.URL) + if err != nil { + portal.log.Errorfln("Failed to download media in %v", err) + return + } + _, fileId, _, err := user.Conn.UploadFile(portal.Key.JID, &skype.SendMessage{ + Jid: portal.Key.JID, + ClientMessageId: "", + Type: "avatar/group", + SendMediaMessage: &skype.SendMediaMessage{ + FileName: "avatar", + RawData: data, + FileSize: strconv.Itoa(len(data)), // strconv.FormatUint(fileSize, 10), + Duration: 0, + }, + }) + if err != nil { + mx.log.Errorln(err) + return + } + resp, err = user.Conn.SetConversationThreads(portal.Key.JID, map[string]string{ + "picture": fmt.Sprintf("URL@https://api.asm.skype.com/v1/objects/%s", fileId), + }) + } + if err != nil { + mx.log.Errorln(err) + } else { + //out := <-resp + mx.log.Infoln(resp) + } +} + +func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { + if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { + fmt.Println() + fmt.Printf("shouldIgnoreEvent: isPuppet%+v", isPuppet) + fmt.Println() + fmt.Printf("shouldIgnoreEvent: isPuppet%+v", evt.Sender) + fmt.Println() + return true + } + isCustomPuppet, ok := evt.Content.Raw["net.maunium.whatsapp.puppet"].(bool) + if ok && isCustomPuppet && mx.bridge.GetPuppetByCustomMXID(evt.Sender) != nil { + return true + } + user := mx.bridge.GetUserByMXID(evt.Sender) + fmt.Println() + fmt.Printf("shouldIgnoreEvent: user%+v", *user) + fmt.Println() + if !user.RelaybotWhitelisted { + fmt.Println("user.RelaybotWhitelisted true", user.RelaybotWhitelisted) + return true + } + fmt.Println("shouldIgnoreEvent: false") + return false +} + +func (mx *MatrixHandler) HandleEncrypted(evt *event.Event) { + if mx.shouldIgnoreEvent(evt) || mx.bridge.Crypto == nil { + fmt.Println("HandleEncrypted return 1") + return + } + + decrypted, err := mx.bridge.Crypto.Decrypt(evt) + if err != nil { + mx.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err) + return + } + mx.bridge.EventProcessor.Dispatch(decrypted) +} + +func (mx *MatrixHandler) HandleMessage(evt *event.Event) { + fmt.Println() + fmt.Printf("HandleMessage : %+v", evt) + fmt.Println() + if mx.shouldIgnoreEvent(evt) { + return + } + + user := mx.bridge.GetUserByMXID(evt.Sender) + fmt.Println() + fmt.Printf("HandleMessage user: %+v", user) + fmt.Println() + content := evt.Content.AsMessage() + if user.Whitelisted && content.MsgType == event.MsgText { + commandPrefix := mx.bridge.Config.Bridge.CommandPrefix + fmt.Println() + fmt.Printf("HandleMessage commandPrefix: %+v", commandPrefix) + fmt.Println() + hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix) + if hasCommandPrefix { + content.Body = strings.TrimLeft(content.Body[len(commandPrefix):], " ") + } + fmt.Println() + fmt.Printf("HandleMessage hasCommandPrefix: %+v", hasCommandPrefix) + fmt.Println() + fmt.Println() + fmt.Printf("HandleMessage evt.RoomID0: %+v", evt.RoomID) + fmt.Println() + fmt.Println() + fmt.Printf("HandleMessage user.ManagementRoom: %+v", user.ManagementRoom) + fmt.Println() + if hasCommandPrefix || evt.RoomID == user.ManagementRoom { + fmt.Println() + fmt.Printf("HandleMessage commandPrefix: %+v", commandPrefix) + fmt.Println() + mx.cmd.Handle(evt.RoomID, user, content.Body) + return + } + } + fmt.Println() + fmt.Printf("HandleMessage evt.RoomID1: %+v", evt.RoomID) + fmt.Println() + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + fmt.Println() + fmt.Printf("HandleMessage portal: %+v", portal) + fmt.Println() + if portal != nil && (user.Whitelisted || portal.HasRelaybot()) { + portal.HandleMatrixMessage(user, evt) + } +} + +func (mx *MatrixHandler) HandleRedaction(evt *event.Event) { + if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { + return + } + + user := mx.bridge.GetUserByMXID(evt.Sender) + + if !user.Whitelisted { + return + } + + if !user.HasSession() { + return + } else if !user.IsConnected() { + msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+ + "You are not connected to skype, so your redaction was not bridged. "+ + "Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false) + msg.MsgType = event.MsgNotice + _, _ = mx.bridge.Bot.SendMessageEvent(evt.RoomID, event.EventMessage, msg) + return + } + + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal != nil { + portal.HandleMatrixRedaction(user, evt) + } +} diff --git a/no-cgo.go b/no-cgo.go new file mode 100644 index 0000000..4016c38 --- /dev/null +++ b/no-cgo.go @@ -0,0 +1,22 @@ +// +build !cgo + +package main + +import ( + "image" + "io" + + "golang.org/x/image/webp" +) + +func NewCryptoHelper(bridge *Bridge) Crypto { + if !bridge.Config.Bridge.Encryption.Allow { + bridge.Log.Warnln("Bridge built without end-to-bridge encryption, but encryption is enabled in config") + } + bridge.Log.Debugln("Bridge built without end-to-bridge encryption") + return nil +} + +func decodeWebp(r io.Reader) (image.Image, error) { + return webp.Decode(r) +} diff --git a/portal.go b/portal.go new file mode 100644 index 0000000..8b9b05f --- /dev/null +++ b/portal.go @@ -0,0 +1,2411 @@ +package main + +import ( + "bytes" + "encoding/gob" + "encoding/hex" + "encoding/json" + "encoding/xml" + "fmt" + skype "github.com/kelaresg/go-skypeapi" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "html" + "image" + "image/gif" + "image/jpeg" + "image/png" + "math/rand" + "net/http" + "reflect" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/crypto/attachment" + + "github.com/Rhymen/go-whatsapp" + waProto "github.com/Rhymen/go-whatsapp/binary/proto" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + "maunium.net/go/mautrix/pushrules" + + "github.com/kelaresg/matrix-skype/database" + "github.com/kelaresg/matrix-skype/types" + "github.com/kelaresg/matrix-skype/whatsapp-ext" +) + +func (bridge *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal { + bridge.portalsLock.Lock() + defer bridge.portalsLock.Unlock() + portal, ok := bridge.portalsByMXID[mxid] + if !ok { + fmt.Println("loadDBPortal1") + return bridge.loadDBPortal(bridge.DB.Portal.GetByMXID(mxid), nil) + } + return portal +} + +func (bridge *Bridge) GetPortalByJID(key database.PortalKey) *Portal { + bridge.portalsLock.Lock() + defer bridge.portalsLock.Unlock() + portal, ok := bridge.portalsByJID[key] + if !ok { + fmt.Println("loadDBPortal2") + return bridge.loadDBPortal(bridge.DB.Portal.GetByJID(key), &key) + } + return portal +} + +func (bridge *Bridge) GetAllPortals() []*Portal { + return bridge.dbPortalsToPortals(bridge.DB.Portal.GetAll()) +} + +func (bridge *Bridge) GetAllPortalsByJID(jid types.SkypeID) []*Portal { + return bridge.dbPortalsToPortals(bridge.DB.Portal.GetAllByJID(jid)) +} + +func (bridge *Bridge) dbPortalsToPortals(dbPortals []*database.Portal) []*Portal { + bridge.portalsLock.Lock() + defer bridge.portalsLock.Unlock() + output := make([]*Portal, len(dbPortals)) + for index, dbPortal := range dbPortals { + if dbPortal == nil { + continue + } + portal, ok := bridge.portalsByJID[dbPortal.Key] + if !ok { + fmt.Println("loadDBPortal3") + portal = bridge.loadDBPortal(dbPortal, nil) + } + output[index] = portal + } + return output +} + +func (bridge *Bridge) loadDBPortal(dbPortal *database.Portal, key *database.PortalKey) *Portal { + fmt.Println("loadDBPortal: ", dbPortal) + if dbPortal == nil { + if key == nil { + return nil + } + dbPortal = bridge.DB.Portal.New() + dbPortal.Key = *key + dbPortal.Insert() + } + portal := bridge.NewPortal(dbPortal) + bridge.portalsByJID[portal.Key] = portal + fmt.Println("loadDBPortal portal.MXID", portal.MXID) + if len(portal.MXID) > 0 { + bridge.portalsByMXID[portal.MXID] = portal + } + return portal +} + +func (portal *Portal) GetUsers() []*User { + return nil +} + +func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal { + portal := &Portal{ + Portal: dbPortal, + bridge: bridge, + log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)), + + recentlyHandled: [recentlyHandledLength]types.SkypeMessageID{}, + + messages: make(chan PortalMessage, 128), + } + fmt.Println("NewPortal: ") + go portal.handleMessageLoop() + return portal +} + +const recentlyHandledLength = 100 + +type PortalMessage struct { + chat string + source *User + data interface{} + timestamp uint64 +} + +type Portal struct { + *database.Portal + + bridge *Bridge + log log.Logger + + roomCreateLock sync.Mutex + + recentlyHandled [recentlyHandledLength]types.SkypeMessageID + recentlyHandledLock sync.Mutex + recentlyHandledIndex uint8 + + backfillLock sync.Mutex + backfilling bool + lastMessageTs uint64 + + privateChatBackfillInvitePuppet func() + + messages chan PortalMessage + + isPrivate *bool + hasRelaybot *bool +} + +const MaxMessageAgeToCreatePortal = 5 * 60 // 5 minutes + +func (portal *Portal) handleMessageLoop() { + for msg := range portal.messages { + fmt.Println() + fmt.Printf("portal handleMessageLoop: %+v", msg) + if len(portal.MXID) == 0 { + if msg.timestamp+MaxMessageAgeToCreatePortal < uint64(time.Now().Unix()) { + portal.log.Debugln("Not creating portal room for incoming message as the message is too old.") + continue + } + portal.log.Debugln("Creating Matrix room from incoming message") + err := portal.CreateMatrixRoom(msg.source) + if err != nil { + portal.log.Errorln("Failed to create portal room:", err) + fmt.Println() + fmt.Printf("portal handleMessageLoop2: %+v", msg) + return + } + } + fmt.Println() + fmt.Printf("portal handleMessageLoop3: %+v", msg) + portal.backfillLock.Lock() + portal.handleMessage(msg) + portal.backfillLock.Unlock() + } +} + +func (portal *Portal) handleMessage(msg PortalMessage) { + fmt.Println() + fmt.Printf("portal handleMessage: %+v", msg) + if len(portal.MXID) == 0 { + portal.log.Warnln("handleMessage called even though portal.MXID is empty") + return + } + + data, ok := msg.data.(skype.Resource) + if ok { + switch data.MessageType { + case "RichText", "Text": + portal.HandleTextMessage(msg.source, data) + case "RichText/UriObject": + //portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, 0, false) + portal.HandleMediaMessageSkype(msg.source, data.Download, data.MessageType,nil, data,false) + case "RichText/Media_Video": + //portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, 0, false) + portal.HandleMediaMessageSkype(msg.source, data.Download, data.MessageType,nil, data,false) + case "RichText/Media_AudioMsg": + //portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, 0, false) + portal.HandleMediaMessageSkype(msg.source, data.Download, data.MessageType,nil, data,false) + case "RichText/Media_GenericFile": + //portal.HandleMediaMessage(msg.source, data.Download, data.Thumbnail, data.Info, data.ContextInfo, data.Type, data.Caption, 0, false) + portal.HandleMediaMessageSkype(msg.source, data.Download, data.MessageType,nil, data,false) + case "RichText/Contacts": + portal.HandleContactMessageSkype(msg.source, data) + case "RichText/Location": + portal.HandleLocationMessageSkype(msg.source, data) + default: + portal.log.Warnln("Unknown message type:", reflect.TypeOf(msg.data)) + } + if data.MessageType == "RichText" || data.MessageType == "Text" { + portal.HandleTextMessage(msg.source, data) + } + } else { + portal.log.Warnln("Unknown message type:", reflect.TypeOf(msg.data)) + } +} + +func (portal *Portal) isRecentlyHandled(id types.SkypeMessageID) bool { + start := portal.recentlyHandledIndex + for i := start; i != start; i = (i - 1) % recentlyHandledLength { + if portal.recentlyHandled[i] == id { + return true + } + } + return false +} + +func (portal *Portal) isDuplicate(clientMessageId types.SkypeMessageID, id string) bool { + msg := portal.bridge.DB.Message.GetByJID(portal.Key, clientMessageId) + if msg != nil && len(msg.ID) < 1 { + msg.UpdateIDByJID(id) + } + if msg != nil { + return true + } + return false +} + +func init() { + gob.Register(&waProto.Message{}) +} + +func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { + msg := portal.bridge.DB.Message.New() + msg.Chat = portal.Key + msg.JID = message.GetKey().GetId() + msg.MXID = mxid + msg.Timestamp = message.GetMessageTimestamp() + if message.GetKey().GetFromMe() { + msg.Sender = source.JID + } else if portal.IsPrivateChat() { + msg.Sender = portal.Key.JID + } else { + msg.Sender = message.GetKey().GetParticipant() + if len(msg.Sender) == 0 { + msg.Sender = message.GetParticipant() + } + } + //msg.Content = message.Message + msg.Content = &skype.Resource{} + msg.Insert() + + portal.recentlyHandledLock.Lock() + index := portal.recentlyHandledIndex + portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength + portal.recentlyHandledLock.Unlock() + portal.recentlyHandled[index] = msg.JID +} + +func (portal *Portal) markHandledSkype(source *User, message *skype.Resource, mxid id.EventID) { + msg := portal.bridge.DB.Message.New() + msg.Chat = portal.Key + msg.JID = message.ClientMessageId + msg.MXID = mxid + msg.Timestamp = uint64(message.Timestamp) + if message.GetFromMe(source.Conn.Conn) { + msg.Sender = source.JID + } else if portal.IsPrivateChat() { + msg.Sender = portal.Key.JID + } else { + msg.Sender = source.JID + //if len(msg.Sender) == 0 { + // msg.Sender = message.Jid + //} + } + msg.Content = message + if len(message.Id)>0 { + msg.ID = message.Id + } + msg.Insert() +fmt.Println("markHandledSkype1", msg.Chat.JID) +fmt.Println("markHandledSkype2", msg.JID) + portal.recentlyHandledLock.Lock() + index := portal.recentlyHandledIndex + portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength + portal.recentlyHandledLock.Unlock() + portal.recentlyHandled[index] = msg.JID +} + +func (portal *Portal) getMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { + if info.FromMe { + return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) + } else if portal.IsPrivateChat() { + return portal.MainIntent() + } else if len(info.SenderJid) == 0 { + if len(info.Source.GetParticipant()) != 0 { + info.SenderJid = info.Source.GetParticipant() + } else { + return nil + } + } + return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal) +} + +func (portal *Portal) getMessageIntentSkype(user *User, info skype.Resource) *appservice.IntentAPI { + if info.GetFromMe(user.Conn.Conn) { + return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) + } else if portal.IsPrivateChat() { + return portal.MainIntent() + } else if len(info.SendId) == 0 { + //if len(info.Source.GetParticipant()) != 0 { + // info.SenderJid = info.Source.GetParticipant() + //} else { + // return nil + //} + return nil + } + fmt.Println() + fmt.Println("getMessageIntentSkype") + fmt.Println() + return portal.bridge.GetPuppetByJID(info.SendId+skypeExt.NewUserSuffix).IntentFor(portal) +} + +func (portal *Portal) handlePrivateChatFromMe(fromMe bool) func() { + if portal.IsPrivateChat() && fromMe { + var privateChatPuppet *Puppet + var privateChatPuppetInvited bool + privateChatPuppet = portal.bridge.GetPuppetByJID(portal.Key.Receiver) + if privateChatPuppetInvited { + return nil + } + privateChatPuppetInvited = true + _, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: privateChatPuppet.MXID}) + _ = privateChatPuppet.DefaultIntent().EnsureJoined(portal.MXID) + + return func() { + if privateChatPuppet != nil && privateChatPuppetInvited { + _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) + } + } + } + return nil +} + +func (portal *Portal) startHandlingSkype(source *User, info skype.Resource) (*appservice.IntentAPI, func()) { + // TODO these should all be trace logs + if portal.lastMessageTs > uint64(info.Timestamp)+1 { + portal.log.Debugfln("Not handling %s: message is older (%d) than last bridge message (%d)", info.Id, info.Timestamp, portal.lastMessageTs) + } else if portal.isRecentlyHandled(info.Id) { + portal.log.Debugfln("Not handling %s: message was recently handled", info.Id) + } else if portal.isDuplicate(info.ClientMessageId, info.Id) { + portal.log.Debugfln("Not handling %s: message is duplicate", info.ClientMessageId) + } else { + portal.log.Debugfln("Starting handling of %s (ts: %d)", info.Id, info.Timestamp) + portal.lastMessageTs = uint64(info.Timestamp) + return portal.getMessageIntentSkype(source, info), portal.handlePrivateChatFromMe(info.GetFromMe(source.Conn.Conn)) + } + fmt.Println() + fmt.Printf("portal startHandling: %+v", "but nil") + return nil, nil +} + +func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { + portal.markHandled(source, message, mxid) + portal.sendDeliveryReceipt(mxid) + portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid) +} + +func (portal *Portal) finishHandlingSkype(source *User, message *skype.Resource, mxid id.EventID) { + portal.markHandledSkype(source, message, mxid) + portal.sendDeliveryReceipt(mxid) + portal.log.Debugln("Handled message", message.Jid, "->", mxid) +} + +func (portal *Portal) SyncParticipants(metadata *skypeExt.GroupInfo) { + changed := false + fmt.Println("SyncParticipants: 0") + levels, err := portal.MainIntent().PowerLevels(portal.MXID) + if err != nil { + fmt.Println("SyncParticipants: 1") + levels = portal.GetBasePowerLevels() + changed = true + } + for _, participant := range metadata.Participants { + fmt.Println("SyncParticipants: participant.JID= ", participant.JID) + user := portal.bridge.GetUserByJID(participant.JID) + portal.userMXIDAction(user, portal.ensureMXIDInvited) + + puppet := portal.bridge.GetPuppetByJID(participant.JID) + fmt.Println("SyncParticipants: portal.MXID = ", portal.MXID) + err := puppet.IntentFor(portal).EnsureJoined(portal.MXID) + if err != nil { + portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.JID, portal.MXID, err) + } + + expectedLevel := 0 + if participant.IsSuperAdmin { + expectedLevel = 95 + } else if participant.IsAdmin { + expectedLevel = 50 + } + changed = levels.EnsureUserLevel(puppet.MXID, expectedLevel) || changed + if user != nil { + changed = levels.EnsureUserLevel(user.MXID, expectedLevel) || changed + } + } + if changed { + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels1:", err) + } + } +} + +func (portal *Portal) UpdateAvatar(user *User, avatar *skypeExt.ProfilePicInfo) bool { + if avatar == nil || strings.Count(avatar.URL, "")-1 < 1 { + //var err error + //avatar, err = user.Conn.GetProfilePicThumb(portal.Key.JID) + //if err != nil { + // portal.log.Errorln(err) + // return false + //} + return false + } + avatar.Authorization = "skype_token " + user.Conn.LoginInfo.SkypeToken + if avatar.Status != 0 { + return false + } + + if portal.Avatar == avatar.Tag { + return false + } + + data, err := avatar.DownloadBytes() + + if err != nil { + portal.log.Warnln("Failed to download avatar:", err) + return false + } + + mimeType := http.DetectContentType(data) + resp, err := portal.MainIntent().UploadBytes(data, mimeType) + if err != nil { + portal.log.Warnln("Failed to upload avatar:", err) + return false + } + + portal.AvatarURL = resp.ContentURI + if len(portal.MXID) > 0 { + _, err = portal.MainIntent().SetRoomAvatar(portal.MXID, resp.ContentURI) + if err != nil { + portal.log.Warnln("Failed to set room topic:", err) + return false + } + } + portal.Avatar = avatar.Tag + return true +} + +func (portal *Portal) UpdateName(name string, setBy types.SkypeID) bool { + if portal.Name != name { + intent := portal.MainIntent() + if len(setBy) > 0 { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + } + _, err := intent.SetRoomName(portal.MXID, name) + if err == nil { + portal.Name = name + return true + } + portal.log.Warnln("Failed to set room name:", err) + } + return false +} + +func (portal *Portal) UpdateTopic(topic string, setBy types.SkypeID) bool { + if portal.Topic != topic { + intent := portal.MainIntent() + if len(setBy) > 0 { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + } + _, err := intent.SetRoomTopic(portal.MXID, topic) + if err == nil { + portal.Topic = topic + return true + } + portal.log.Warnln("Failed to set room topic:", err) + } + return false +} + +func (portal *Portal) UpdateMetadata(user *User) bool { + if portal.IsPrivateChat() { + return false + } else if portal.IsStatusBroadcastRoom() { + update := false + update = portal.UpdateName("skype Status Broadcast", "") || update + update = portal.UpdateTopic("skype status updates from your contacts", "") || update + return update + } + metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID) + if err != nil { + portal.log.Errorln(err) + fmt.Println() + fmt.Println("UpdateMetadata0: ", err) + fmt.Println() + return false + } + + portalName := "" + names := strings.Split(metadata.Name, ", ") + for _, name := range names { + if strings.Index(name, ":") > 0 { + key := "8:" + name + skypeExt.NewUserSuffix + if key == user.JID { + continue + } + fmt.Println("CreateMatrixRoom3.1: ", key) + if contact, ok := user.Conn.Store.Contacts[key]; ok { + portalName += contact.DisplayName + } + } + } + if len(portalName) < 1 { + portalName = metadata.Name + } + // portal.Topic = "" + //if metadata.Status != 0 { + // 401: access denied + // 404: group does (no longer) exist + // 500: ??? happens with status@broadcast + + // TODO: update the room, e.g. change priority level + // to send messages to moderator + // return false + //} + + portal.SyncParticipants(metadata) + update := false + update = portal.UpdateName(portalName, metadata.NameSetBy) || update + // update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy) || update + return update +} + +func (portal *Portal) userMXIDAction(user *User, fn func(mxid id.UserID)) { + if user == nil { + return + } + + if user == portal.bridge.Relaybot { + for _, mxid := range portal.bridge.Config.Bridge.Relaybot.InviteUsers { + fn(mxid) + } + } else { + fn(user.MXID) + } +} + +func (portal *Portal) ensureMXIDInvited(mxid id.UserID) { + err := portal.MainIntent().EnsureInvited(portal.MXID, mxid) + if err != nil { + portal.log.Warnfln("Failed to ensure %s is invited to %s: %v", mxid, portal.MXID, err) + } +} + +func (portal *Portal) ensureUserInvited(user *User) { + portal.userMXIDAction(user, portal.ensureMXIDInvited) + + customPuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) + if customPuppet != nil && customPuppet.CustomIntent() != nil { + _ = customPuppet.CustomIntent().EnsureJoined(portal.MXID) + } +} + +func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { + portal.log.Infoln("Syncing portal for", user.MXID) + + if user.IsRelaybot { + yes := true + portal.hasRelaybot = &yes + } + + newPortal := false + if len(portal.MXID) == 0 { + if !portal.IsPrivateChat() { + portal.Name = chat.ThreadProperties.Topic + } + //todo + fmt.Println("SyncSkype portal.MXID", portal.MXID) + err := portal.CreateMatrixRoom(user) + if err != nil { + portal.log.Errorln("Failed to create portal room:", err) + return + } + newPortal = true + } else { + fmt.Println("SyncSkype ensureUserInvited", portal.MXID) + portal.ensureUserInvited(user) + } + + if portal.IsPrivateChat() { + return + } + + fmt.Println("SyncSkype portal") + + update := false + if !newPortal { + update = portal.UpdateMetadata(user) || update + } + // if !portal.IsStatusBroadcastRoom() { + //fmt.Println("SyncSkype portal.UpdateAvatar", portal.MXID) + // update = portal.UpdateAvatar(user, nil) || update + // } + if update { + fmt.Println("SyncSkype portal.Update", portal.MXID) + portal.Update() + } +} + +//func (portal *Portal) Sync(user *User, contact whatsapp.Contact) { +// portal.log.Infoln("Syncing portal for", user.MXID) +// +// if user.IsRelaybot { +// yes := true +// portal.hasRelaybot = &yes +// } +// +// if len(portal.MXID) == 0 { +// if !portal.IsPrivateChat() { +// portal.Name = contact.Name +// } +// err := portal.CreateMatrixRoom(user) +// if err != nil { +// portal.log.Errorln("Failed to create portal room:", err) +// return +// } +// } else { +// portal.ensureUserInvited(user) +// } +// +// if portal.IsPrivateChat() { +// return +// } +// +// update := false +// update = portal.UpdateMetadata(user) || update +// if !portal.IsStatusBroadcastRoom() { +// update = portal.UpdateAvatar(user, nil) || update +// } +// if update { +// portal.Update() +// } +//} + +func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent { + anyone := 0 + nope := 99 + invite := 99 + if portal.bridge.Config.Bridge.AllowUserInvite { + invite = 0 + } + return &event.PowerLevelsEventContent{ + UsersDefault: anyone, + EventsDefault: anyone, + RedactPtr: &anyone, + StateDefaultPtr: &nope, + BanPtr: &nope, + InvitePtr: &invite, + Users: map[id.UserID]int{ + portal.MainIntent().UserID: 100, + }, + Events: map[string]int{ + event.StateRoomName.Type: anyone, + event.StateRoomAvatar.Type: anyone, + event.StateTopic.Type: anyone, + }, + } +} + +func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { + levels, err := portal.MainIntent().PowerLevels(portal.MXID) + if err != nil { + levels = portal.GetBasePowerLevels() + } + newLevel := 0 + if setAdmin { + newLevel = 50 + } + changed := false + for _, jid := range jids { + puppet := portal.bridge.GetPuppetByJID(jid) + changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed + + user := portal.bridge.GetUserByJID(jid) + if user != nil { + changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed + } + } + if changed { + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels2:", err) + } + } +} + +//func (portal *Portal) membershipRemove(jids []string, action skypeExt.ChatActionType) { +// for _, jid := range jids { +// jidArr := strings.Split(jid, "@c.") +// jid = jidArr[0] +// member := portal.bridge.GetPuppetByJID(jid) +// if member == nil { +// portal.log.Errorln("%s is not exist", jid) +// continue +// } +// _, err := portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ +// UserID: member.MXID, +// }) +// if err != nil { +// portal.log.Errorln("Error %s member from whatsapp: %v", action, err) +// } +// } +//} + +func (portal *Portal) membershipRemove(content string) { + xmlFormat := skype.XmlContent{} + err := xml.Unmarshal([]byte(content), &xmlFormat) + + member := portal.bridge.GetPuppetByJID(xmlFormat.Target) + + memberMaxid := strings.Replace(string(member.MXID), "@skype&8:", "@skype&8-", 1) + _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ + UserID: id.UserID(memberMaxid), + }) + if err != nil { + portal.log.Errorln("Error %v member from whatsapp:", err) + } + //for _, chat := range user.Conn.Store.Chats { + // group := portal.bridge.GetPuppetByJID(chat.Id.(string)) + // fmt.Println("member") + // fmt.Println(group) + // fmt.Println("用户信息:") + // fmt.Println(chat.Id.(string)) + // + // if group == nil { + // portal.log.Errorln("%s is not exist", jid) + // continue + // } + // if group.JID == jid { + // _, err := portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ + // UserID: group.MXID, + // }) + // if err != nil { + // portal.log.Errorln("Error %v member from whatsapp:", err) + // } + // } + //} +} + +func (portal *Portal) membershipAdd(user *User, jid string) { + chatMap := make(map[string]skype.Conversation) + for _, chat := range user.Conn.Store.Chats { + if chat.Id == jid { + cid, _ := chat.Id.(string) + chatMap[cid] = chat + } + } + fmt.Println("membershipAddzsl:", chatMap) + user.syncPortals(chatMap, false) +} + +func (portal *Portal) membershipCreate(user *User, cmd skypeExt.ChatUpdate) { + //contact := skype.Contact{ + // Jid: cmd.Data.SenderJID, + // Notify: "", + // Name: cmd.Data.Create.Name, + // Short: "", + //} + //portal.Sync(user, contact) + //contact.Jid = cmd.JID + //user.Conn.Store.Contacts[cmd.JID] = contact +} + +func (portal *Portal) RestrictMessageSending(restrict bool) { + levels, err := portal.MainIntent().PowerLevels(portal.MXID) + if err != nil { + levels = portal.GetBasePowerLevels() + } + if restrict { + levels.EventsDefault = 50 + } else { + levels.EventsDefault = 0 + } + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels3:", err) + } +} + +func (portal *Portal) RestrictMetadataChanges(restrict bool) { + levels, err := portal.MainIntent().PowerLevels(portal.MXID) + if err != nil { + levels = portal.GetBasePowerLevels() + } + newLevel := 0 + if restrict { + newLevel = 50 + } + changed := false + changed = levels.EnsureEventLevel(event.StateRoomName, newLevel) || changed + changed = levels.EnsureEventLevel(event.StateRoomAvatar, newLevel) || changed + changed = levels.EnsureEventLevel(event.StateTopic, newLevel) || changed + if changed { + _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels4:", err) + } + } +} + +func (portal *Portal) BackfillHistory(user *User, lastMessageTime uint64) error { + if !portal.bridge.Config.Bridge.RecoverHistory { + return nil + } + + endBackfill := portal.beginBackfill() + defer endBackfill() + + lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key) + if lastMessage == nil { + return nil + } + if lastMessage.Timestamp >= lastMessageTime { + portal.log.Debugln("Not backfilling: no new messages") + return nil + } + + lastMessageID := lastMessage.JID + //lastMessageFromMe := lastMessage.Sender == user.JID + portal.log.Infoln("Backfilling history since", lastMessageID, "for", user.MXID) + for len(lastMessageID) > 0 { + portal.log.Debugln("Backfilling history: 50 messages after", lastMessageID) + //resp, err := user.Conn.LoadMessagesAfter(portal.Key.JID, lastMessageID, lastMessageFromMe, 50) + //if err != nil { + // return err + //} + //messages, ok := resp.Content.([]interface{}) + //if !ok || len(messages) == 0 { + // break + //} + + //portal.handleHistory(user, messages) + // + //lastMessageProto, ok := messages[len(messages)-1].(*waProto.WebMessageInfo) + //if ok { + // lastMessageID = lastMessageProto.GetKey().GetId() + // lastMessageFromMe = lastMessageProto.GetKey().GetFromMe() + //} + } + portal.log.Infoln("Backfilling finished") + return nil +} + +func (portal *Portal) beginBackfill() func() { + portal.backfillLock.Lock() + portal.backfilling = true + var privateChatPuppetInvited bool + var privateChatPuppet *Puppet + if portal.IsPrivateChat() && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling { + receiverId := portal.Key.Receiver + if strings.Index(receiverId, skypeExt.NewUserSuffix) > 0 { + receiverId = strings.ReplaceAll(receiverId, skypeExt.NewUserSuffix, "") + } + privateChatPuppet = portal.bridge.GetPuppetByJID(receiverId) + portal.privateChatBackfillInvitePuppet = func() { + if privateChatPuppetInvited { + return + } + privateChatPuppetInvited = true + _, _ = portal.MainIntent().InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: privateChatPuppet.MXID}) + _ = privateChatPuppet.DefaultIntent().EnsureJoined(portal.MXID) + } + } + return func() { + portal.backfilling = false + portal.privateChatBackfillInvitePuppet = nil + portal.backfillLock.Unlock() + if privateChatPuppet != nil && privateChatPuppetInvited { + _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) + } + } +} + +func (portal *Portal) disableNotifications(user *User) { + if !portal.bridge.Config.Bridge.HistoryDisableNotifs { + return + } + puppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) + if puppet == nil || puppet.customIntent == nil { + return + } + portal.log.Debugfln("Disabling notifications for %s for backfilling", user.MXID) + ruleID := fmt.Sprintf("net.maunium.silence_while_backfilling.%s", portal.MXID) + err := puppet.customIntent.PutPushRule("global", pushrules.OverrideRule, ruleID, &mautrix.ReqPutPushRule{ + Actions: []pushrules.PushActionType{pushrules.ActionDontNotify}, + Conditions: []pushrules.PushCondition{{ + Kind: pushrules.KindEventMatch, + Key: "room_id", + Pattern: string(portal.MXID), + }}, + }) + if err != nil { + portal.log.Warnfln("Failed to disable notifications for %s while backfilling: %v", user.MXID, err) + } +} + +func (portal *Portal) enableNotifications(user *User) { + if !portal.bridge.Config.Bridge.HistoryDisableNotifs { + return + } + puppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) + if puppet == nil || puppet.customIntent == nil { + return + } + portal.log.Debugfln("Re-enabling notifications for %s after backfilling", user.MXID) + ruleID := fmt.Sprintf("net.maunium.silence_while_backfilling.%s", portal.MXID) + err := puppet.customIntent.DeletePushRule("global", pushrules.OverrideRule, ruleID) + if err != nil { + portal.log.Warnfln("Failed to re-enable notifications for %s after backfilling: %v", user.MXID, err) + } +} + +func (portal *Portal) FillInitialHistory(user *User) error { + if portal.bridge.Config.Bridge.InitialHistoryFill == 0 { + return nil + } + endBackfill := portal.beginBackfill() + defer endBackfill() + if portal.privateChatBackfillInvitePuppet != nil { + portal.privateChatBackfillInvitePuppet() + } + + n := portal.bridge.Config.Bridge.InitialHistoryFill + portal.log.Infoln("Filling initial history, maximum", n, "messages") + resp, err := user.Conn.GetMessages(portal.Key.JID, "", strconv.Itoa(n)) + if err != nil { + return err + } + portal.disableNotifications(user) + portal.handleHistory(user, resp.Messages) + portal.enableNotifications(user) + portal.log.Infoln("Initial history fill complete") + return nil +} + +func (portal *Portal) handleHistory(user *User, messages []skype.Resource) { + portal.log.Infoln("Handling", len(messages), "messages of history") + for i, message := range messages { + message = messages[len(messages)-i-1] + if message.Content == "" { + portal.log.Warnln("Message", message, "has no content") + continue + } + if portal.privateChatBackfillInvitePuppet != nil && message.GetFromMe(user.Conn.Conn) && portal.IsPrivateChat() { + portal.privateChatBackfillInvitePuppet() + } + t, _ := time.Parse(time.RFC3339, message.ComposeTime) + message.Timestamp = t.Unix() + portal.handleMessage(PortalMessage{ portal.Key.JID, user, message, uint64(message.Timestamp)}) + } +} + +type BridgeInfoSection struct { + ID string `json:"id"` + DisplayName string `json:"display_name,omitempty"` + AvatarURL id.ContentURIString `json:"avatar_url,omitempty"` + ExternalURL string `json:"external_url,omitempty"` +} + +type BridgeInfoContent struct { + BridgeBot id.UserID `json:"bridgebot"` + Creator id.UserID `json:"creator,omitempty"` + Protocol BridgeInfoSection `json:"protocol"` + Network *BridgeInfoSection `json:"network,omitempty"` + Channel BridgeInfoSection `json:"channel"` +} + +var ( + StateBridgeInfo = event.Type{Type: "m.bridge", Class: event.StateEventType} + StateHalfShotBridgeInfo = event.Type{Type: "uk.half-shot.bridge", Class: event.StateEventType} +) + +func (portal *Portal) CreateMatrixRoom(user *User) error { + portal.roomCreateLock.Lock() + defer portal.roomCreateLock.Unlock() + if len(portal.MXID) > 0 { + return nil + } + + intent := portal.MainIntent() + if err := intent.EnsureRegistered(); err != nil { + fmt.Println() + fmt.Println("CreateMatrixRoom0: ", err) + fmt.Println() + return err + } + + portal.log.Infoln("Creating Matrix room. Info source user.MXID:", user.MXID) + portal.log.Infoln("Creating Matrix room. Info source portal.Key.JID:", portal.Key.JID) + + var metadata *skypeExt.GroupInfo + if portal.IsPrivateChat() { + fmt.Println() + fmt.Println("CreateMatrixRoom1: ") + fmt.Println() + puppet := portal.bridge.GetPuppetByJID(portal.Key.JID+skypeExt.NewUserSuffix) + if portal.bridge.Config.Bridge.PrivateChatPortalMeta { + portal.Name = puppet.Displayname + portal.AvatarURL = puppet.AvatarURL + portal.Avatar = puppet.Avatar + } else { + portal.Name = "" + } + portal.Topic = "skype private chat" + } else if portal.IsStatusBroadcastRoom() { + fmt.Println() + fmt.Println("CreateMatrixRoom2: ") + fmt.Println() + portal.Name = "skype Status Broadcast" + portal.Topic = "skype status updates from your contacts" + } else { + fmt.Println() + fmt.Println("CreateMatrixRoom3: ") + fmt.Println() + var err error + metadata, err = user.Conn.GetGroupMetaData(portal.Key.JID) + if err == nil { + portalName := "" + names := strings.Split(metadata.Name, ", ") + for _, name := range names { + if strings.Index(name, ":") > 0 { + key := "8:" + name + skypeExt.NewUserSuffix + if key == user.JID { + continue + } + fmt.Println("CreateMatrixRoom3.1: ", key) + if contact, ok := user.Conn.Store.Contacts[key]; ok { + if len(portalName) > 0 { + portalName = portalName + ", " + contact.DisplayName + } else { + portalName = contact.DisplayName + } + } + } else if name == "..." { + portalName = portalName + ", ..." + } + } + if len(portalName) > 0 { + portal.Name = portalName + } else { + portal.Name = metadata.Name + } + // portal.Topic = metadata.Topic + portal.Topic = "" + } + portal.UpdateAvatar(user, nil) + } + + bridgeInfo := event.Content{ + Parsed: BridgeInfoContent{ + BridgeBot: portal.bridge.Bot.UserID, + Creator: portal.MainIntent().UserID, + Protocol: BridgeInfoSection{ + ID: "skype", + DisplayName: "Skype", + AvatarURL: id.ContentURIString(portal.bridge.Config.AppService.Bot.Avatar), + ExternalURL: "https://www.skype.com/", + }, + Channel: BridgeInfoSection{ + ID: portal.Key.JID, + }, + }, + } + content := portal.GetBasePowerLevels() + //if portal.IsPrivateChat() { + // // 创建单人会话时,使对方权限等级降低 + // for userID, _ := range content.Users { + // content.Users[userID] = 100 + // } + // When creating a room, make user self the highest level of authority + // content.Users[user.MXID] = 100 + //} + // When creating a room, make user self the highest level of authority + content.Users[user.MXID] = 100 + initialState := []*event.Event{{ + Type: event.StatePowerLevels, + Content: event.Content{ + Parsed: content, + }, + }, { + Type: StateBridgeInfo, + Content: bridgeInfo, + }, { + // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec + Type: StateHalfShotBridgeInfo, + Content: bridgeInfo, + }} + if !portal.AvatarURL.IsEmpty() { + initialState = append(initialState, &event.Event{ + Type: event.StateRoomAvatar, + Content: event.Content{ + Parsed: event.RoomAvatarEventContent{URL: portal.AvatarURL}, + }, + }) + } + + invite := []id.UserID{user.MXID} + if user.IsRelaybot { + invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers + } + + if portal.bridge.Config.Bridge.Encryption.Default { + initialState = append(initialState, &event.Event{ + Type: event.StateEncryption, + Content: event.Content{ + Parsed: event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}, + }, + }) + portal.Encrypted = true + if portal.IsPrivateChat() { + invite = append(invite, portal.bridge.Bot.UserID) + } + } + + resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{ + Visibility: "private", + Name: portal.Name, + Topic: portal.Topic, + Invite: invite, + Preset: "private_chat", + IsDirect: portal.IsPrivateChat(), + InitialState: initialState, + }) + if err != nil { + return err + } + portal.MXID = resp.RoomID + portal.Update() + portal.bridge.portalsLock.Lock() + portal.bridge.portalsByMXID[portal.MXID] = portal + portal.bridge.portalsLock.Unlock() + + // We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here. + for _, user := range invite { + portal.bridge.StateStore.SetMembership(portal.MXID, user, event.MembershipInvite) + } + + if metadata != nil { + portal.SyncParticipants(metadata) + } else { + fmt.Println("GetPuppetByCustomMXID: ", user.MXID) + customPuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) + if customPuppet != nil && customPuppet.CustomIntent() != nil { + _ = customPuppet.CustomIntent().EnsureJoined(portal.MXID) + } + } + user.addPortalToCommunity(portal) + if portal.IsPrivateChat() { + puppet := user.bridge.GetPuppetByJID(portal.Key.JID) + user.addPuppetToCommunity(puppet) + + if portal.bridge.Config.Bridge.Encryption.Default { + err = portal.bridge.Bot.EnsureJoined(portal.MXID) + if err != nil { + portal.log.Errorln("Failed to join created portal with bridge bot for e2be:", err) + } + } + } + err = portal.FillInitialHistory(user) + if err != nil { + portal.log.Errorln("Failed to fill history:", err) + } + return nil +} + +func (portal *Portal) IsPrivateChat() bool { + if portal.isPrivate == nil { + val := !strings.HasSuffix(portal.Key.JID, skypeExt.GroupSuffix) + portal.isPrivate = &val + } + return *portal.isPrivate +} + +func (portal *Portal) HasRelaybot() bool { + if portal.bridge.Relaybot == nil { + return false + } else if portal.hasRelaybot == nil { + val := portal.bridge.Relaybot.IsInPortal(portal.Key) + portal.hasRelaybot = &val + } + return *portal.hasRelaybot +} + +func (portal *Portal) IsStatusBroadcastRoom() bool { + return portal.Key.JID == "status@broadcast" +} + +func (portal *Portal) MainIntent() *appservice.IntentAPI { + if portal.IsPrivateChat() { + fmt.Println("IsPrivateChat") + return portal.bridge.GetPuppetByJID(portal.Key.JID+skypeExt.NewUserSuffix).DefaultIntent() + } + fmt.Println("not IsPrivateChat") + return portal.bridge.Bot +} + +func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp.ContextInfo) { + if len(info.QuotedMessageID) == 0 { + return + } + message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID) + if message != nil { + evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) + if err != nil { + portal.log.Warnln("Failed to get reply target:", err) + return + } + content.SetReply(evt) + } + return +} + +func (portal *Portal) SetReplySkype(content *event.MessageEventContent, info skype.Resource) { + if len(info.Id) == 0 { + return + } + message := portal.bridge.DB.Message.GetByJID(portal.Key, info.Id) + if message != nil { + evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) + if err != nil { + portal.log.Warnln("Failed to get reply target:", err) + return + } + content.SetReply(evt) + } + return +} + +func (portal *Portal) HandleMessageRevokeSkype(user *User, message skype.Resource) { + msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.SkypeEditedId) + if msg == nil { + return + } + var intent *appservice.IntentAPI + if message.GetFromMe(user.Conn.Conn) { + if portal.IsPrivateChat() { + intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent() + } else { + intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) + } + } + if intent == nil { + intent = portal.MainIntent() + } + _, err := intent.RedactEvent(portal.MXID, msg.MXID) + if err != nil { + portal.log.Errorln("Failed to redact %s: %v", msg.JID, err) + return + } + msg.Delete() +} + + +func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.MessageRevocation) { + msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id) + if msg == nil { + return + } + var intent *appservice.IntentAPI + if message.FromMe { + if portal.IsPrivateChat() { + intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent() + } else { + intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) + } + } else if len(message.Participant) > 0 { + intent = portal.bridge.GetPuppetByJID(message.Participant).IntentFor(portal) + } + if intent == nil { + intent = portal.MainIntent() + } + _, err := intent.RedactEvent(portal.MXID, msg.MXID) + if err != nil { + portal.log.Errorln("Failed to redact %s: %v", msg.JID, err) + return + } + msg.Delete() +} + +func (portal *Portal) HandleFakeMessage(source *User, message FakeMessage) { + if portal.isRecentlyHandled(message.ID) { + return + } + + content := event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: message.Text, + } + if message.Alert { + content.MsgType = event.MsgText + } + _, err := portal.sendMainIntentMessage(content) + if err != nil { + portal.log.Errorfln("Failed to handle fake message %s: %v", message.ID, err) + return + } + + portal.recentlyHandledLock.Lock() + index := portal.recentlyHandledIndex + portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength + portal.recentlyHandledLock.Unlock() + portal.recentlyHandled[index] = message.ID +} + +func (portal *Portal) sendMainIntentMessage(content interface{}) (*mautrix.RespSendEvent, error) { + return portal.sendMessage(portal.MainIntent(), event.EventMessage, content, 0) +} + +func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.Type, content interface{}, timestamp int64) (*mautrix.RespSendEvent, error) { + wrappedContent := event.Content{Parsed: content} + if timestamp != 0 && intent.IsCustomPuppet { + wrappedContent.Raw = map[string]interface{}{ + "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, + } + } + fmt.Println() + fmt.Printf("portal sendMessage0: %+v", content) + if portal.Encrypted && portal.bridge.Crypto != nil { + encrypted, err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, wrappedContent) + if err != nil { + return nil, errors.Wrap(err, "failed to encrypt event") + } + eventType = event.EventEncrypted + wrappedContent.Parsed = encrypted + } + if timestamp == 0 { + fmt.Println() + fmt.Printf("portal sendMessage1: %+v", content) + return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent) + } else { + fmt.Println() + fmt.Printf("portal sendMessage2: %+v", content) + return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp) + } +} + +func (portal *Portal) HandleTextMessage(source *User, message skype.Resource) { + if message.ClientMessageId == "" && message.Content == "" && len(message.SkypeEditedId) > 0 { + portal.HandleMessageRevokeSkype(source, message) + } else { + intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, message) + if endHandlePrivateChatFromMe != nil { + defer endHandlePrivateChatFromMe() + } + if intent == nil { + fmt.Println("portal HandleTextMessage0: ", intent) + return + } + content := &event.MessageEventContent{ + Body: message.Content, + MsgType: event.MsgText, + } + + portal.bridge.Formatter.ParseSkype(content) + portal.SetReplySkype(content, message) + + fmt.Println() + fmt.Printf("portal HandleTextMessage2: %+v", content) + _, _ = intent.UserTyping(portal.MXID, false, 0) + resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + return + } + fmt.Println() + fmt.Printf("portal HandleTextMessage3: %+v", content) + portal.finishHandlingSkype(source, &message, resp.EventID) + } +} + +func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Resource) { + intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, message) + if endHandlePrivateChatFromMe != nil { + defer endHandlePrivateChatFromMe() + } + if intent == nil { + return + } + locationMessage, err:= message.ParseLocation() + if err != nil { + portal.log.Errorfln("Failed to parse contact message of %s: %v", message, err) + return + } + + latitude, _ := strconv.Atoi(locationMessage.Latitude) + longitude, _:= strconv.Atoi(locationMessage.Longitude) + geo := fmt.Sprintf("geo:%.6f,%.6f", float32(latitude)/1000000, float32(longitude)/1000000) + content := &event.MessageEventContent{ + MsgType: event.MsgText, //event.MsgLocation, + Body: fmt.Sprintf("Location: %s%s
", locationMessage.A.Href, locationMessage.Address, geo), + Format: event.FormatHTML, + FormattedBody: fmt.Sprintf("Location: %s%s
", locationMessage.A.Href, locationMessage.Address, geo), + GeoURI: geo, + } + + //portal.SetReply(content, message.ContextInfo) + portal.SetReplySkype(content, message) + + _, _ = intent.UserTyping(portal.MXID, false, 0) + t, _ := time.Parse(time.RFC3339,message.ComposeTime) + resp, err := portal.sendMessage(intent, event.EventMessage, content, t.Unix()) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + return + } + portal.finishHandlingSkype(source, &message, resp.EventID) +} + +func (portal *Portal) HandleContactMessageSkype(source *User, message skype.Resource) { + intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, message) + if endHandlePrivateChatFromMe != nil { + defer endHandlePrivateChatFromMe() + } + if intent == nil { + return + } + contactMessage, err:= message.ParseContact() + if err != nil { + portal.log.Errorfln("Failed to parse contact message of %s: %v", message, err) + return + } + + content := &event.MessageEventContent{ + Body: fmt.Sprintf("%s\n%s", contactMessage.C.F, contactMessage.C.S), + MsgType: event.MsgText, + } + + //portal.SetReply(content, message.ContextInfo) + portal.SetReplySkype(content, message) + + _, _ = intent.UserTyping(portal.MXID, false, 0) + t, _ := time.Parse(time.RFC3339,message.ComposeTime) + resp, err := portal.sendMessage(intent, event.EventMessage, content, t.Unix()) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + return + } + portal.finishHandlingSkype(source, &message, resp.EventID) +} + +func (portal *Portal) sendMediaBridgeFailureSkype(source *User, intent *appservice.IntentAPI, info skype.Resource, downloadErr error) { + portal.log.Errorfln("Failed to download media for %s: %v", info.Id, downloadErr) + resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: "Failed to bridge media", + }, int64(info.Timestamp*1000)) + if err != nil { + portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) + } else { + portal.finishHandlingSkype(source, &info, resp.EventID) + } +} + +func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, info whatsapp.MessageInfo, downloadErr error) { + portal.log.Errorfln("Failed to download media for %s: %v", info.Id, downloadErr) + resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: "Failed to bridge media", + }, int64(info.Timestamp*1000)) + if err != nil { + portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) + } else { + portal.finishHandling(source, info.Source, resp.EventID) + } +} + +func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) { + if !portal.Encrypted { + return data, mimeType, nil + } + + file := &event.EncryptedFileInfo{ + EncryptedFile: *attachment.NewEncryptedFile(), + URL: "", + } + return file.Encrypt(data), "application/octet-stream", file + +} + +func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn *skype.Conn, mediaType string) (data []byte, mediaMessage *skype.MediaMessageContent, err error), mediaType string, thumbnail []byte, info skype.Resource, sendAsSticker bool) { + intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, info) + if endHandlePrivateChatFromMe != nil { + defer endHandlePrivateChatFromMe() + } + if intent == nil { + return + } + + data, mediaMessage, err := download(source.Conn.Conn, mediaType) + if err == whatsapp.ErrMediaDownloadFailedWith404 || err == whatsapp.ErrMediaDownloadFailedWith410 { + portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", info.Id, err) + //_, err = source.Conn.LoadMediaInfo(info.RemoteJid, info.Id, info.FromMe) + //if err != nil { + // portal.sendMediaBridgeFailure(source, intent, info, errors.Wrap(err, "failed to load media info")) + // return + //} + data, mediaMessage, err = download(source.Conn.Conn, mediaType) + } + if err == whatsapp.ErrNoURLPresent { + portal.log.Debugfln("No URL present error for media message %s, ignoring...", info.Id) + return + } else if err != nil { + portal.sendMediaBridgeFailureSkype(source, intent, info, err) + return + } + + // synapse doesn't handle webp well, so we convert it. This can be dropped once https://github.com/matrix-org/synapse/issues/4382 is fixed + mimeType := http.DetectContentType(data) + //length := len(data) + if mimeType == "image/webp" { + img, err := decodeWebp(bytes.NewReader(data)) + if err != nil { + portal.log.Errorfln("Failed to decode media for %s: %v", err) + return + } + + var buf bytes.Buffer + err = png.Encode(&buf, img) + if err != nil { + portal.log.Errorfln("Failed to convert media for %s: %v", err) + return + } + data = buf.Bytes() + mimeType = "image/png" + } + + var width, height int + if strings.HasPrefix(mimeType, "image/") { + cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) + width, height = cfg.Width, cfg.Height + } + + data, uploadMimeType, file := portal.encryptFile(data, mimeType) + + uploaded, err := intent.UploadBytes(data, uploadMimeType) + if err != nil { + portal.log.Errorfln("Failed to upload media for %s: %v", err) + return + } + + fileName := mediaMessage.OriginalName.V + //exts, _ := mime.ExtensionsByType(mimeType) + //if exts != nil && len(exts) > 0 { + // fileName += exts[0] + //} + duration, err := strconv.Atoi(mediaMessage.DurationMs) + if err != nil { + duration = 0 + } + if mediaType == "RichText/Media_AudioMsg" { + mimeType = "audio" + } + content := &event.MessageEventContent{ + Body: fileName, + File: file, + Info: &event.FileInfo{ + Size: len(data), + MimeType: mimeType, + Width: width, + Height: height, + Duration: duration, + }, + } + if content.File != nil { + content.File.URL = uploaded.ContentURI.CUString() + } else { + content.URL = uploaded.ContentURI.CUString() + } + portal.SetReplySkype(content, info) + + fmt.Println() + fmt.Println("mediaMessage.UrlThumbnail", mediaMessage.UrlThumbnail) + fmt.Println() + fmt.Printf("%+v", mediaMessage) + fmt.Println() + thumbnail, err = skype.Download(mediaMessage.UrlThumbnail, source.Conn.Conn, 0) + if err != nil { + portal.log.Errorfln("Failed to download thumbnail for %s: %v", err) + } + + if thumbnail != nil && portal.bridge.Config.Bridge.WhatsappThumbnail && err == nil { + thumbnailMime := http.DetectContentType(thumbnail) + thumbnailCfg, _, _ := image.DecodeConfig(bytes.NewReader(thumbnail)) + thumbnailSize := len(thumbnail) + thumbnail, thumbnailUploadMime, thumbnailFile := portal.encryptFile(thumbnail, thumbnailMime) + uploadedThumbnail, err := intent.UploadBytes(thumbnail, thumbnailUploadMime) + if err != nil { + portal.log.Warnfln("Failed to upload thumbnail for %s: %v", info.Id, err) + } else if uploadedThumbnail != nil { + if thumbnailFile != nil { + thumbnailFile.URL = uploadedThumbnail.ContentURI.CUString() + content.Info.ThumbnailFile = thumbnailFile + } else { + content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString() + } + content.Info.ThumbnailInfo = &event.FileInfo{ + Size: thumbnailSize, + Width: thumbnailCfg.Width, + Height: thumbnailCfg.Height, + MimeType: thumbnailMime, + } + fmt.Println("content.Info") + fmt.Printf("%+v", content) + fmt.Println() + fmt.Printf("%+v", *content.Info.ThumbnailInfo) + fmt.Println() + fmt.Println() + fmt.Printf("%+v", content.Info.ThumbnailInfo) + fmt.Println() + } + } + + switch strings.ToLower(strings.Split(mimeType, "/")[0]) { + case "image": + if !sendAsSticker { + content.MsgType = event.MsgImage + } + case "video": + content.MsgType = event.MsgVideo + case "audio": + content.MsgType = event.MsgAudio + default: + content.MsgType = event.MsgFile + } + + _, _ = intent.UserTyping(portal.MXID, false, 0) + eventType := event.EventMessage + if sendAsSticker { + eventType = event.EventSticker + } + resp, err := portal.sendMessage(intent, eventType, content, info.Timestamp * 1000) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err) + return + } + + //if len(caption) > 0 { + // captionContent := &event.MessageEventContent{ + // Body: caption, + // MsgType: event.MsgNotice, + // } + // + // portal.bridge.Formatter.ParseSkype(captionContent) + // + // _, err := portal.sendMessage(intent, event.EventMessage, captionContent, ts) + // if err != nil { + // portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err) + // } + // // TODO store caption mxid? + //} + portal.finishHandlingSkype(source, &info, resp.EventID) +} + +func makeMessageID() *string { + b := make([]byte, 10) + rand.Read(b) + str := strings.ToUpper(hex.EncodeToString(b)) + return &str +} + +func (portal *Portal) downloadThumbnail(content *event.MessageEventContent, id id.EventID) []byte { + if len(content.GetInfo().ThumbnailURL) == 0 { + return nil + } + mxc, err := content.GetInfo().ThumbnailURL.Parse() + if err != nil { + portal.log.Errorln("Malformed thumbnail URL in %s: %v", id, err) + } + thumbnail, err := portal.MainIntent().DownloadBytes(mxc) + if err != nil { + portal.log.Errorln("Failed to download thumbnail in %s: %v", id, err) + return nil + } + thumbnailType := http.DetectContentType(thumbnail) + var img image.Image + switch thumbnailType { + case "image/png": + img, err = png.Decode(bytes.NewReader(thumbnail)) + case "image/gif": + img, err = gif.Decode(bytes.NewReader(thumbnail)) + case "image/jpeg": + return thumbnail + default: + return nil + } + var buf bytes.Buffer + err = jpeg.Encode(&buf, img, &jpeg.Options{ + Quality: jpeg.DefaultQuality, + }) + if err != nil { + portal.log.Errorln("Failed to re-encode thumbnail in %s: %v", id, err) + return nil + } + return buf.Bytes() +} + +func (portal *Portal) preprocessMatrixMediaSkype(relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID) (string, uint64, []byte) { + var caption string + if relaybotFormatted { + caption = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) + } + + var file *event.EncryptedFileInfo + rawMXC := content.URL + if content.File != nil { + file = content.File + rawMXC = file.URL + } + mxc, err := rawMXC.Parse() + if err != nil { + portal.log.Errorln("Malformed content URL in %s: %v", eventID, err) + return "", 0, nil + } + data, err := portal.MainIntent().DownloadBytes(mxc) + if err != nil { + portal.log.Errorfln("Failed to download media in %s: %v", eventID, err) + return "", 0, nil + } + if file != nil { + data, err = file.Decrypt(data) + if err != nil { + portal.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err) + return "", 0, nil + } + } + + return caption, uint64(len(data)), data +} + +func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsapp.MediaType) *MediaUpload { + //var caption string + //if relaybotFormatted { + // caption = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) + //} + + var file *event.EncryptedFileInfo + rawMXC := content.URL + if content.File != nil { + file = content.File + rawMXC = file.URL + } + mxc, err := rawMXC.Parse() + if err != nil { + portal.log.Errorln("Malformed content URL in %s: %v", eventID, err) + return nil + } + data, err := portal.MainIntent().DownloadBytes(mxc) + if err != nil { + portal.log.Errorfln("Failed to download media in %s: %v", eventID, err) + return nil + } + if file != nil { + data, err = file.Decrypt(data) + if err != nil { + portal.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err) + return nil + } + } + + //url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(data), mediaType) + //if err != nil { + // portal.log.Errorfln("Failed to upload media in %s: %v", eventID, err) + // return nil + //} + // + //return &MediaUpload{ + // Caption: caption, + // URL: url, + // MediaKey: mediaKey, + // FileEncSHA256: fileEncSHA256, + // FileSHA256: fileSHA256, + // FileLength: fileLength, + // Thumbnail: portal.downloadThumbnail(content, eventID), + //} + return nil +} + +type MediaUpload struct { + Caption string + URL string + MediaKey []byte + FileEncSHA256 []byte + FileSHA256 []byte + FileLength uint64 + Thumbnail []byte +} + +func (portal *Portal) sendMatrixConnectionError(sender *User, eventID id.EventID) bool { + if !sender.HasSession() { + portal.log.Debugln("Ignoring event", eventID, "from", sender.MXID, "as user has no session") + return true + } else if !sender.IsConnected() { + portal.log.Debugln("Ignoring event", eventID, "from", sender.MXID, "as user is not connected") + inRoom := "" + if portal.IsPrivateChat() { + inRoom = " in your management room" + } + reconnect := fmt.Sprintf("Use `%s reconnect`%s to reconnect.", portal.bridge.Config.Bridge.CommandPrefix, inRoom) + if sender.IsLoginInProgress() { + reconnect = "You have a login attempt in progress, please wait." + } + msg := format.RenderMarkdown("\u26a0 You are not connected to skype, so your message was not bridged. "+reconnect, true, false) + msg.MsgType = event.MsgNotice + _, err := portal.sendMainIntentMessage(msg) + if err != nil { + portal.log.Errorln("Failed to send bridging failure message:", err) + } + return true + } + return false +} + +func (portal *Portal) addRelaybotFormat(sender *User, content *event.MessageEventContent) bool { + member := portal.MainIntent().Member(portal.MXID, sender.MXID) + if len(member.Displayname) == 0 { + member.Displayname = string(sender.MXID) + } + + if content.Format != event.FormatHTML { + content.FormattedBody = strings.Replace(html.EscapeString(content.Body), "\n", "
", -1) + content.Format = event.FormatHTML + } + data, err := portal.bridge.Config.Bridge.Relaybot.FormatMessage(content, sender.MXID, member) + if err != nil { + portal.log.Errorln("Failed to apply relaybot format:", err) + } + content.FormattedBody = data + return true +} + +func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) (*skype.SendMessage, *User, *event.MessageEventContent) { + content, ok := evt.Content.Parsed.(*event.MessageEventContent) + if !ok { + portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed) + return nil, sender, content + } + + //ts := uint64(evt.Timestamp / 1000) + //status := waProto.WebMessageInfo_ERROR + //fromMe := true + currentTimeNanoStr := strconv.FormatInt(time.Now().UnixNano(), 10) + currentTimeNanoStr = currentTimeNanoStr[:len(currentTimeNanoStr)-3] + clientMessageId := currentTimeNanoStr + fmt.Sprintf("%04v", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000)) + info := &skype.SendMessage{ + ClientMessageId: clientMessageId, + Jid: portal.Key.JID,//receiver id(conversation id) + Timestamp: time.Now().Unix(), + } + //ctxInfo := &waProto.ContextInfo{} + //replyToID := content.GetReplyTo() + var newContent string + //if len(replyToID) > 0 { + rQuote := regexp.MustCompile(`
]+\bhref="(.*?)://matrix\.to/#/@([^"]+):(.*?)">(.*?)
([^"]+)
(.*)`) + quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) + fmt.Println("matches0: ", content.FormattedBody) + fmt.Println("matches1: ", quoteMatches) + if len(quoteMatches) > 0 { + for _, match := range quoteMatches { + if len(match) > 2 { + var skyId string + if strings.Index(match[4], "@skype") > -1 { + skyId = strings.ReplaceAll(match[2], "skype&", "") + skyId = strings.ReplaceAll(skyId, "skype&", "") + skyId = strings.ReplaceAll(skyId, "-", ":") + } else { + skyId = strings.ReplaceAll(sender.JID, skypeExt.NewUserSuffix, "") + } + if len(skyId) < 2 { + continue + } + + skypeUsername := strings.Replace(skyId, "8:", "", 1) + puppet := sender.bridge.GetPuppetByJID(skyId + skypeExt.NewUserSuffix) + time.Now().Unix() + newContent = fmt.Sprintf(`[%s] %s↵: %s↵↵<<< %s`, + skypeUsername, + puppet.Displayname, + strconv.Itoa(int(time.Now().Unix())), + portal.Key.JID, + time.Now().UnixNano() / 1e6, + strconv.Itoa(int(time.Now().UnixNano())) + "1", + time.Now().Unix(), + puppet.Displayname, + match[5], + match[6]) + content.FormattedBody = newContent + } + } + } + content.RemoveReplyFallback() + //if len(newContent) > 0 { + // newContent = content.Body + //} + //content.FormattedBody = newContent + //msg := portal.bridge.DB.Message.GetByMXID(replyToID) + //} + relaybotFormatted := false + if sender.NeedsRelaybot(portal) { + if !portal.HasRelaybot() { + if sender.HasSession() { + portal.log.Debugln("Database says", sender.MXID, "not in chat and no relaybot, but trying to send anyway") + } else { + portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot") + return nil, sender, content + } + } else { + relaybotFormatted = portal.addRelaybotFormat(sender, content) + sender = portal.bridge.Relaybot + } + } + if evt.Type == event.EventSticker { + content.MsgType = event.MsgImage + } + fmt.Println("convertMatrixMessage content.MsgType: ", content.MsgType) + fmt.Println("convertMatrixMessage content.Body: ", content.Body) + fmt.Println("convertMatrixMessage content.FormattedBody: ", content.FormattedBody) + info.Type = string(content.MsgType) + switch content.MsgType { + case event.MsgText, event.MsgEmote, event.MsgNotice: + text := content.Body + if content.Format == event.FormatHTML { + text = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) + } + if content.MsgType == event.MsgEmote && !relaybotFormatted { + text = "/me " + text + } + if len(content.FormattedBody) > 0 { + r := regexp.MustCompile(`]+\bhref="(.*?)://matrix\.to/#/@skype&([^"]+):(.*?)">(.*?)*`) + matches := r.FindAllStringSubmatch(content.FormattedBody, -1) + fmt.Println("matches: ", matches) + if len(matches) > 0 { + for _, match := range matches { + if len(match) > 2 { + skyId := strings.ReplaceAll(match[2], "-", ":") + content.FormattedBody = strings.ReplaceAll(content.FormattedBody, match[0], fmt.Sprintf(`%s`, skyId, match[4])) + } + } + } + + info.SendTextMessage = &skype.SendTextMessage{ + Content : content.FormattedBody, + } + } else { + info.SendTextMessage = &skype.SendTextMessage{ + Content : content.Body, + } + } + //ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1) + //for index, mention := range ctxInfo.MentionedJid { + // ctxInfo.MentionedJid[index] = mention[1:] + whatsappExt.NewUserSuffix + //} + //if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil { + // info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{ + // Text: &text, + // ContextInfo: ctxInfo, + // } + //} else { + // info.Message.Conversation = &text + //} + case event.MsgImage: + caption, fileSize , data := portal.preprocessMatrixMediaSkype(relaybotFormatted, content, evt.ID) + //if media == nil { + // return nil, sender, content + //} + fmt.Println("caption: ", caption) + info.SendMediaMessage = &skype.SendMediaMessage{ + FileName: content.Body, + FileType: content.GetInfo().MimeType, + RawData: data, + FileSize: strconv.FormatUint(fileSize, 10), // strconv.FormatUint(fileSize, 10), + Duration: 0, + } + case event.MsgVideo: + _, fileSize , data := portal.preprocessMatrixMediaSkype(relaybotFormatted, content, evt.ID) + duration := uint32(content.GetInfo().Duration) + info.SendMediaMessage = &skype.SendMediaMessage{ + FileName: content.Body, + FileType: content.GetInfo().MimeType, + RawData: data, + FileSize: strconv.FormatUint(fileSize, 10), // strconv.FormatUint(fileSize, 10), + Duration: int(duration), + } + case event.MsgAudio: + _, fileSize , data := portal.preprocessMatrixMediaSkype(relaybotFormatted, content, evt.ID) + duration := uint32(content.GetInfo().Duration) + info.SendMediaMessage = &skype.SendMediaMessage{ + FileName: content.Body, + FileType: content.GetInfo().MimeType, + RawData: data, + FileSize: strconv.FormatUint(fileSize, 10), // strconv.FormatUint(fileSize, 10), + Duration: int(duration), + } + case event.MsgFile: + _, fileSize , data := portal.preprocessMatrixMediaSkype(relaybotFormatted, content, evt.ID) + info.SendMediaMessage = &skype.SendMediaMessage{ + FileName: content.Body, + FileType: content.GetInfo().MimeType, + RawData: data, + FileSize: strconv.FormatUint(fileSize, 10), // strconv.FormatUint(fileSize, 10), + Duration: 0, + } + default: + portal.log.Debugln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType) + return nil, sender, content + } + return info, sender, content +} + +func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.WebMessageInfo, *User, *event.MessageEventContent) { + content, ok := evt.Content.Parsed.(*event.MessageEventContent) + if !ok { + portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed) + return nil, sender, content + } + + ts := uint64(evt.Timestamp / 1000) + status := waProto.WebMessageInfo_ERROR + fromMe := true + info := &waProto.WebMessageInfo{ + Key: &waProto.MessageKey{ + FromMe: &fromMe, + Id: makeMessageID(), + RemoteJid: &portal.Key.JID, + }, + MessageTimestamp: &ts, + Message: &waProto.Message{}, + Status: &status, + } + //ctxInfo := &waProto.ContextInfo{} + replyToID := content.GetReplyTo() + if len(replyToID) > 0 { + content.RemoveReplyFallback() + msg := portal.bridge.DB.Message.GetByMXID(replyToID) + //if msg != nil && msg.Content != nil { + if msg != nil { + //ctxInfo.StanzaId = &msg.JID + //ctxInfo.Participant = &msg.Sender + //ctxInfo.QuotedMessage = msg.Content + } + } + relaybotFormatted := false + if sender.NeedsRelaybot(portal) { + if !portal.HasRelaybot() { + if sender.HasSession() { + portal.log.Debugln("Database says", sender.MXID, "not in chat and no relaybot, but trying to send anyway") + } else { + portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot") + return nil, sender, content + } + } else { + relaybotFormatted = portal.addRelaybotFormat(sender, content) + sender = portal.bridge.Relaybot + } + } + if evt.Type == event.EventSticker { + content.MsgType = event.MsgImage + } +fmt.Println("convertMatrixMessage content.MsgType: ", content.MsgType) + switch content.MsgType { + case event.MsgText, event.MsgEmote, event.MsgNotice: + text := content.Body + if content.Format == event.FormatHTML { + text = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) + } + if content.MsgType == event.MsgEmote && !relaybotFormatted { + text = "/me " + text + } + //ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1) + //for index, mention := range ctxInfo.MentionedJid { + // ctxInfo.MentionedJid[index] = mention[1:] + whatsappExt.NewUserSuffix + //} + //if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil { + // info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{ + // Text: &text, + // ContextInfo: ctxInfo, + // } + //} else { + // info.Message.Conversation = &text + //} + case event.MsgImage: + media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaImage) + if media == nil { + return nil, sender, content + } + info.Message.ImageMessage = &waProto.ImageMessage{ + Caption: &media.Caption, + JpegThumbnail: media.Thumbnail, + Url: &media.URL, + MediaKey: media.MediaKey, + Mimetype: &content.GetInfo().MimeType, + FileEncSha256: media.FileEncSHA256, + FileSha256: media.FileSHA256, + FileLength: &media.FileLength, + } + case event.MsgVideo: + media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaVideo) + if media == nil { + return nil, sender, content + } + duration := uint32(content.GetInfo().Duration) + info.Message.VideoMessage = &waProto.VideoMessage{ + Caption: &media.Caption, + JpegThumbnail: media.Thumbnail, + Url: &media.URL, + MediaKey: media.MediaKey, + Mimetype: &content.GetInfo().MimeType, + Seconds: &duration, + FileEncSha256: media.FileEncSHA256, + FileSha256: media.FileSHA256, + FileLength: &media.FileLength, + } + case event.MsgAudio: + media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaAudio) + if media == nil { + return nil, sender, content + } + duration := uint32(content.GetInfo().Duration) + info.Message.AudioMessage = &waProto.AudioMessage{ + Url: &media.URL, + MediaKey: media.MediaKey, + Mimetype: &content.GetInfo().MimeType, + Seconds: &duration, + FileEncSha256: media.FileEncSHA256, + FileSha256: media.FileSHA256, + FileLength: &media.FileLength, + } + case event.MsgFile: + media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaDocument) + if media == nil { + return nil, sender, content + } + info.Message.DocumentMessage = &waProto.DocumentMessage{ + Url: &media.URL, + FileName: &content.Body, + MediaKey: media.MediaKey, + Mimetype: &content.GetInfo().MimeType, + FileEncSha256: media.FileEncSHA256, + FileSha256: media.FileSHA256, + FileLength: &media.FileLength, + } + default: + portal.log.Debugln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType) + return nil, sender, content + } + return info, sender, content +} + +func (portal *Portal) wasMessageSent(sender *User, id string) bool { + //_, err := sender.Conn.LoadMessagesAfter(portal.Key.JID, id, true, 0) + //if err != nil { + // if err != whatsapp.ErrServerRespondedWith404 { + // portal.log.Warnfln("Failed to check if message was bridged without response: %v", err) + // } + // return false + //} + return true +} + +func (portal *Portal) sendErrorMessage(sendErr error) id.EventID { + resp, err := portal.sendMainIntentMessage(event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: fmt.Sprintf("\u26a0 Your message may not have been bridged: %v", sendErr), + }) + if err != nil { + portal.log.Warnfln("Failed to send bridging error message:", err) + return "" + } + return resp.EventID +} + +func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) { + if portal.bridge.Config.Bridge.DeliveryReceipts { + err := portal.bridge.Bot.MarkRead(portal.MXID, eventID) + if err != nil { + portal.log.Debugfln("Failed to send delivery receipt for %s: %v", eventID, err) + } + } +} + +var timeout = errors.New("message sending timed out") + +func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { + fmt.Println("portal HandleMatrixMessage sender.JID: ", sender.JID) + fmt.Println("portal HandleMatrixMessage portal.Key.Receiver: ", portal.Key.Receiver) + fmt.Println("portal HandleMatrixMessage portal.Key.JID: ", portal.Key.JID) + if !portal.HasRelaybot() && ( + (portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) || + portal.sendMatrixConnectionError(sender, evt.ID)) { + return + } + portal.log.Debugfln("Received event %s", evt.ID) + info, sender, _ := portal.convertMatrixMessageSkype(sender, evt) + if info == nil { + fmt.Println("portal HandleMatrixMessage info is nil: ") + return + } + infoRaw, err := json.Marshal(info) + if err != nil { + fmt.Println("portal HandleMatrixMessage Marshal info err: ", err) + return + } + fmt.Println("portal HandleMatrixMessage start markHandledSkype: ") + portal.markHandledSkype(sender, &skype.Resource{ + ClientMessageId: info.ClientMessageId, + Jid: portal.Key.JID,//receiver id(conversation id) + Timestamp: time.Now().Unix(), + Content: string(infoRaw), + }, evt.ID) + portal.log.Debugln("Sending event", evt.ID, "to Skype") + + errChan := make(chan error, 1) + //go sender.Conn.Conn.SendMsg(portal.Key.JID, info.Content, info.ClientMessageId, errChan) + go SendMsg(sender, portal.Key.JID, info, errChan) + + var errorEventID id.EventID + select { + case err = <-errChan: + case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second): + if portal.bridge.Config.Bridge.FetchMessageOnTimeout && portal.wasMessageSent(sender, info.ClientMessageId) { + portal.log.Debugln("Matrix event %s was bridged, but response didn't arrive within timeout") + portal.sendDeliveryReceipt(evt.ID) + } else { + portal.log.Warnfln("Response when bridging Matrix event %s is taking long to arrive", evt.ID) + errorEventID = portal.sendErrorMessage(timeout) + } + err = <-errChan + } + if err != nil { + portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) + portal.sendErrorMessage(err) + } else { + portal.log.Debugfln("Handled Matrix event %s", evt.ID) + portal.sendDeliveryReceipt(evt.ID) + } + if errorEventID != "" { + _, err = portal.MainIntent().RedactEvent(portal.MXID, errorEventID) + if err != nil { + portal.log.Warnfln("Failed to redact timeout warning message %s: %v", errorEventID, err) + } + } +} + +func SendMsg(sender *User, chatThreadId string, content *skype.SendMessage, output chan<- error) (err error) { + fmt.Println("message SendMsg type: ", content.Type) + switch event.MessageType(content.Type) { + case event.MsgText, event.MsgEmote, event.MsgNotice: + err = sender.Conn.SendText(chatThreadId, content) + case event.MsgImage: + fmt.Println("message SendMsg type m.image: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgVideo: + fmt.Println("message SendMsg type m.video: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgAudio: + fmt.Println("message SendMsg type m.audio: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgFile: + fmt.Println("message SendMsg type m.file: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgLocation: + fmt.Println("message SendMsg type m.location: ", content.Type) + //err = c.SendFile(chatThreadId, content) + default: + err = errors.New("send to skype(unknown message type)") + } + + if err != nil { + output <- fmt.Errorf("message sending responded with %d", err) + } else { + output <- nil + } + return +} + +func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) { + if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver { + return + } + + msg := portal.bridge.DB.Message.GetByMXID(evt.Redacts) + if msg == nil || msg.Sender != sender.JID { + return + } + + errChan := make(chan error, 1) + //todo return errChan here + go sender.Conn.DeleteMessage(msg.Chat.JID, msg.ID) + + var err error + select { + case err = <-errChan: + case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second): + portal.log.Warnfln("Response when bridging Matrix redaction %s is taking long to arrive", evt.ID) + err = <-errChan + } + if err != nil { + portal.log.Errorfln("Error handling Matrix redaction %s: %v", evt.ID, err) + } else { + portal.log.Debugln("Handled Matrix redaction %s of %s", evt.ID, evt.Redacts) + portal.sendDeliveryReceipt(evt.ID) + } +} + +func (portal *Portal) Delete() { + portal.Portal.Delete() + portal.bridge.portalsLock.Lock() + delete(portal.bridge.portalsByJID, portal.Key) + if len(portal.MXID) > 0 { + delete(portal.bridge.portalsByMXID, portal.MXID) + } + portal.bridge.portalsLock.Unlock() +} + +func (portal *Portal) Cleanup(puppetsOnly bool) { + if len(portal.MXID) == 0 { + return + } + if portal.IsPrivateChat() { + _, err := portal.MainIntent().LeaveRoom(portal.MXID) + if err != nil { + portal.log.Warnln("Failed to leave private chat portal with main intent:", err) + } + return + } + intent := portal.MainIntent() + members, err := intent.JoinedMembers(portal.MXID) + if err != nil { + portal.log.Errorln("Failed to get portal members for cleanup:", err) + return + } + for member, _ := range members.Joined { + if member == intent.UserID { + continue + } + puppet := portal.bridge.GetPuppetByMXID(member) + if puppet != nil { + _, err = puppet.DefaultIntent().LeaveRoom(portal.MXID) + if err != nil { + portal.log.Errorln("Error leaving as puppet while cleaning up portal:", err) + } + } else if !puppetsOnly { + _, err = intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"}) + if err != nil { + portal.log.Errorln("Error kicking user while cleaning up portal:", err) + } + } + } + _, err = intent.LeaveRoom(portal.MXID) + if err != nil { + portal.log.Errorln("Error leaving with main intent while cleaning up portal:", err) + } +} + +func (portal *Portal) HandleMatrixLeave(sender *User) { + if portal.IsPrivateChat() { + portal.log.Debugln("User left private chat portal, cleaning up and deleting...") + portal.Delete() + portal.Cleanup(false) + return + } +} + +func (portal *Portal) HandleMatrixKick(sender *User, event *event.Event) { + // TODO +} diff --git a/provisioning.go b/provisioning.go new file mode 100644 index 0000000..a56fb97 --- /dev/null +++ b/provisioning.go @@ -0,0 +1,345 @@ +package main + +import ( + "context" + "encoding/json" + "github.com/gorilla/websocket" + log "maunium.net/go/maulogger/v2" + "net/http" + + "maunium.net/go/mautrix/id" +) + +type ProvisioningAPI struct { + bridge *Bridge + log log.Logger +} + +func (prov *ProvisioningAPI) Init() { + prov.log = prov.bridge.Log.Sub("Provisioning") + prov.log.Debugln("Enabling provisioning API at", prov.bridge.Config.AppService.Provisioning.Prefix) + r := prov.bridge.AS.Router.PathPrefix(prov.bridge.Config.AppService.Provisioning.Prefix).Subrouter() + r.Use(prov.AuthMiddleware) + r.HandleFunc("/ping", prov.Ping).Methods(http.MethodGet) + r.HandleFunc("/login", prov.Login) + r.HandleFunc("/logout", prov.Logout).Methods(http.MethodPost) + r.HandleFunc("/delete_session", prov.DeleteSession).Methods(http.MethodPost) + r.HandleFunc("/delete_connection", prov.DeleteConnection).Methods(http.MethodPost) + r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost) + r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost) +} + +func (prov *ProvisioningAPI) AuthMiddleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + auth = auth[len("Bearer "):] + if auth != prov.bridge.Config.AppService.Provisioning.SharedSecret { + jsonResponse(w, http.StatusForbidden, map[string]interface{}{ + "error": "Invalid auth token", + "errcode": "M_FORBIDDEN", + }) + return + } + userID := r.URL.Query().Get("user_id") + user := prov.bridge.GetUserByMXID(id.UserID(userID)) + h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "user", user))) + }) +} + +type Error struct { + Success bool `json:"success"` + Error string `json:"error"` + ErrCode string `json:"errcode"` +} + +type Response struct { + Success bool `json:"success"` + Status string `json:"status"` +} + +func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value("user").(*User) + if user.Session == nil && user.Conn == nil { + jsonResponse(w, http.StatusNotFound, Error{ + Error: "Nothing to purge: no session information stored and no active connection.", + ErrCode: "no session", + }) + return + } + user.SetSession(nil) + if user.Conn != nil { + //_, _ = user.Conn.Disconnect() + user.Conn.RemoveHandlers() + user.Conn = nil + } + jsonResponse(w, http.StatusOK, Response{true, "Session information purged"}) +} + +func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Request) { + //user := r.Context().Value("user").(*User) + //if user.Conn == nil { + // jsonResponse(w, http.StatusNotFound, Error{ + // Error: "You don't have a WhatsApp connection.", + // ErrCode: "not connected", + // }) + // return + //} + //sess, err := user.Conn.Disconnect() + //if err == nil && len(sess.Wid) > 0 { + // user.SetSession(&sess) + //} + //user.Conn.RemoveHandlers() + //user.Conn = nil + //jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"}) +} + +func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) { + //user := r.Context().Value("user").(*User) + //if user.Conn == nil { + // jsonResponse(w, http.StatusNotFound, Error{ + // Error: "You don't have a WhatsApp connection.", + // ErrCode: "no connection", + // }) + // return + //} + //sess, err := user.Conn.Disconnect() + //if err == whatsapp.ErrNotConnected { + // jsonResponse(w, http.StatusNotFound, Error{ + // Error: "You were not connected", + // ErrCode: "not connected", + // }) + // return + //} else if err != nil { + // user.log.Warnln("Error while disconnecting:", err) + // jsonResponse(w, http.StatusInternalServerError, Error{ + // Error: fmt.Sprintf("Unknown error while disconnecting: %v", err), + // ErrCode: err.Error(), + // }) + // return + //} else if len(sess.Wid) > 0 { + // user.SetSession(&sess) + //} + //jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"}) +} + +func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) { + //user := r.Context().Value("user").(*User) + //if user.Conn == nil { + // if user.Session == nil { + // jsonResponse(w, http.StatusForbidden, Error{ + // Error: "No existing connection and no session. Please log in first.", + // ErrCode: "no session", + // }) + // } else { + // user.Connect(false) + // jsonResponse(w, http.StatusOK, Response{true, "Created connection to WhatsApp."}) + // } + // return + //} + // + //wasConnected := true + //sess, err := user.Conn.Disconnect() + //if err == whatsapp.ErrNotConnected { + // wasConnected = false + //} else if err != nil { + // user.log.Warnln("Error while disconnecting:", err) + //} else if len(sess.Wid) > 0 { + // user.SetSession(&sess) + //} + // + //err = user.Conn.Restore() + //if err == whatsapp.ErrInvalidSession { + // if user.Session != nil { + // user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...") + // var sess whatsapp.Session + // sess, err = user.Conn.RestoreWithSession(*user.Session) + // if err == nil { + // user.SetSession(&sess) + // } + // } else { + // jsonResponse(w, http.StatusForbidden, Error{ + // Error: "You're not logged in", + // ErrCode: "not logged in", + // }) + // return + // } + //} else if err == whatsapp.ErrLoginInProgress { + // jsonResponse(w, http.StatusConflict, Error{ + // Error: "A login or reconnection is already in progress.", + // ErrCode: "login in progress", + // }) + // return + //} else if err == whatsapp.ErrAlreadyLoggedIn { + // jsonResponse(w, http.StatusConflict, Error{ + // Error: "You were already connected.", + // ErrCode: err.Error(), + // }) + // return + //} + //if err != nil { + // user.log.Warnln("Error while reconnecting:", err) + // if err.Error() == "restore session connection timed out" { + // jsonResponse(w, http.StatusForbidden, Error{ + // Error: "Reconnection timed out. Is WhatsApp on your phone reachable?", + // ErrCode: err.Error(), + // }) + // } else { + // jsonResponse(w, http.StatusForbidden, Error{ + // Error: fmt.Sprintf("Unknown error while reconnecting: %v", err), + // ErrCode: err.Error(), + // }) + // } + // user.log.Debugln("Disconnecting due to failed session restore in reconnect command...") + // sess, err := user.Conn.Disconnect() + // if err != nil { + // user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err) + // } else if len(sess.Wid) > 0 { + // user.SetSession(&sess) + // } + // return + //} + //user.ConnectionErrors = 0 + //user.PostLogin() + // + //var msg string + //if wasConnected { + // msg = "Reconnected successfully." + //} else { + // msg = "Connected successfully." + //} + // + //jsonResponse(w, http.StatusOK, Response{true, msg}) +} + +func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) { + //user := r.Context().Value("user").(*User) + //wa := map[string]interface{}{ + // "has_session": user.Session != nil, + // "management_room": user.ManagementRoom, + // "jid": user.JID, + // "conn": nil, + // "ping": nil, + //} + //if user.Conn != nil { + // wa["conn"] = map[string]interface{}{ + // "is_connected": user.Conn.IsConnected(), + // "is_logged_in": user.Conn.IsLoggedIn(), + // "is_login_in_progress": user.Conn.IsLoginInProgress(), + // } + // ok, err := user.Conn.AdminTest() + // wa["ping"] = map[string]interface{}{ + // "ok": ok, + // "err": err, + // } + //} + //resp := map[string]interface{}{ + // "mxid": user.MXID, + // "admin": user.Admin, + // "whitelisted": user.Whitelisted, + // "relaybot_whitelisted": user.RelaybotWhitelisted, + // "whatsapp": wa, + //} + //jsonResponse(w, http.StatusOK, resp) +} + +func jsonResponse(w http.ResponseWriter, status int, response interface{}) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(response) +} + +func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) { + //user := r.Context().Value("user").(*User) + //if user.Session == nil { + // jsonResponse(w, http.StatusNotFound, Error{ + // Error: "You're not logged in", + // ErrCode: "not logged in", + // }) + // return + //} + // + //err := user.Conn.Logout() + //if err != nil { + // user.log.Warnln("Error while logging out:", err) + // jsonResponse(w, http.StatusInternalServerError, Error{ + // Error: fmt.Sprintf("Unknown error while logging out: %v", err), + // ErrCode: err.Error(), + // }) + // return + //} + //_, err = user.Conn.Disconnect() + //if err != nil { + // user.log.Warnln("Error while disconnecting after logout:", err) + //} + //user.Conn.RemoveHandlers() + //user.Conn = nil + //user.removeFromJIDMap() + //// TODO this causes a foreign key violation, which should be fixed + ////ce.User.JID = "" + //user.SetSession(nil) + //jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."}) +} + +var upgrader = websocket.Upgrader{} + +func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) { + //userID := r.URL.Query().Get("user_id") + //user := prov.bridge.GetUserByMXID(id.UserID(userID)) + // + //c, err := upgrader.Upgrade(w, r, nil) + //if err != nil { + // prov.log.Errorfln("Failed to upgrade connection to websocket:", err) + // return + //} + //defer c.Close() + // + //if !user.Connect(true) { + // user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.") + // _ = c.WriteJSON(Error{ + // Error: "Failed to connect to WhatsApp", + // ErrCode: "connection error", + // }) + // return + //} + // + //qrChan := make(chan string, 3) + //go func() { + // for code := range qrChan { + // if code == "stop" { + // return + // } + // _ = c.WriteJSON(map[string]interface{}{ + // "code": code, + // }) + // } + //}() + //session, err := user.Conn.LoginWithRetry(qrChan, user.bridge.Config.Bridge.LoginQRRegenCount) + //qrChan <- "stop" + //if err != nil { + // var msg string + // if err == whatsapp.ErrAlreadyLoggedIn { + // msg = "You're already logged in" + // } else if err == whatsapp.ErrLoginInProgress { + // msg = "You have a login in progress already." + // } else if err == whatsapp.ErrLoginTimedOut { + // msg = "QR code scan timed out. Please try again." + // } else { + // user.log.Warnln("Failed to log in:", err) + // msg = fmt.Sprintf("Unknown error while logging in: %v", err) + // } + // _ = c.WriteJSON(Error{ + // Error: msg, + // ErrCode: err.Error(), + // }) + // return + //} + //user.ConnectionErrors = 0 + //user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) + //user.addToJIDMap() + //user.SetSession(&session) + //_ = c.WriteJSON(map[string]interface{}{ + // "success": true, + // "jid": user.JID, + //}) + //user.PostLogin() +} diff --git a/puppet.go b/puppet.go new file mode 100644 index 0000000..02623ba --- /dev/null +++ b/puppet.go @@ -0,0 +1,323 @@ +package main + +import ( + "fmt" + skype "github.com/kelaresg/go-skypeapi" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "net/http" + "regexp" + "strings" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/database" + "github.com/kelaresg/matrix-skype/types" + "github.com/kelaresg/matrix-skype/whatsapp-ext" +) + +func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.SkypeID, bool) { + fmt.Println("ParsePuppetMXID: ", bridge.Config.Bridge.FormatUsername("(.*)")) + userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", + bridge.Config.Bridge.FormatUsername("(.*)"), + bridge.Config.Homeserver.Domain)) + if err != nil { + bridge.Log.Warnln("Failed to compile puppet user ID regex:", err) + return "", false + } + match := userIDRegex.FindStringSubmatch(string(mxid)) + if match == nil || len(match) != 2 { + return "", false + } + + jid := types.SkypeID(match[1] + whatsappExt.NewUserSuffix) + return jid, true +} + +func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet { + jid, ok := bridge.ParsePuppetMXID(mxid) + if !ok { + return nil + } + + return bridge.GetPuppetByJID(jid) +} + +func (bridge *Bridge) GetPuppetByJID(jid types.SkypeID) *Puppet { + jid = strings.Trim(jid, " ") + if len(jid) < 1 { + return nil + } + if strings.Index(jid, skypeExt.NewUserSuffix) < 0 { + jid = jid + skypeExt.NewUserSuffix + } + bridge.puppetsLock.Lock() + defer bridge.puppetsLock.Unlock() + puppet, ok := bridge.puppets[jid] + if !ok { + bridge.Log.Debugln("GetPuppetByJID(NewPuppet):", jid) + dbPuppet := bridge.DB.Puppet.Get(jid) + if dbPuppet == nil { + dbPuppet = bridge.DB.Puppet.New() + dbPuppet.JID = jid + dbPuppet.Insert() + } + puppet = bridge.NewPuppet(dbPuppet) + bridge.puppets[puppet.JID] = puppet + if len(puppet.CustomMXID) > 0 { + bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } + } + return puppet +} + +func (bridge *Bridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet { + bridge.puppetsLock.Lock() + defer bridge.puppetsLock.Unlock() + puppet, ok := bridge.puppetsByCustomMXID[mxid] + if !ok { + dbPuppet := bridge.DB.Puppet.GetByCustomMXID(mxid) + if dbPuppet == nil { + return nil + } + bridge.Log.Debugln("GetPuppetByCustomMXID(NewPuppet):", dbPuppet.JID) + puppet = bridge.NewPuppet(dbPuppet) + bridge.puppets[puppet.JID] = puppet + bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet + } + return puppet +} + +func (bridge *Bridge) GetAllPuppetsWithCustomMXID() []*Puppet { + return bridge.dbPuppetsToPuppets(bridge.DB.Puppet.GetAllWithCustomMXID()) +} + +func (bridge *Bridge) GetAllPuppets() []*Puppet { + return bridge.dbPuppetsToPuppets(bridge.DB.Puppet.GetAll()) +} + +func (bridge *Bridge) dbPuppetsToPuppets(dbPuppets []*database.Puppet) []*Puppet { + bridge.puppetsLock.Lock() + defer bridge.puppetsLock.Unlock() + output := make([]*Puppet, len(dbPuppets)) + for index, dbPuppet := range dbPuppets { + if dbPuppet == nil { + continue + } + puppet, ok := bridge.puppets[dbPuppet.JID] + if !ok { + bridge.Log.Debugln("dbPuppetsToPuppets(NewPuppet):", dbPuppet.JID) + puppet = bridge.NewPuppet(dbPuppet) + bridge.puppets[dbPuppet.JID] = puppet + if len(dbPuppet.CustomMXID) > 0 { + bridge.puppetsByCustomMXID[dbPuppet.CustomMXID] = puppet + } + } + output[index] = puppet + } + return output +} + +func (bridge *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet { + return &Puppet{ + Puppet: dbPuppet, + bridge: bridge, + log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), + + MXID: id.NewUserID( + bridge.Config.Bridge.FormatUsername( + // dbPuppet.JID, + //), + strings.Replace( + strings.Replace(dbPuppet.JID, whatsappExt.NewUserSuffix, "", 1), + ":", + "-", + -1, + ), + ), + bridge.Config.Homeserver.Domain), + } +} + +type Puppet struct { + *database.Puppet + + bridge *Bridge + log log.Logger + + typingIn id.RoomID + typingAt int64 + + MXID id.UserID + + customIntent *appservice.IntentAPI + customTypingIn map[id.RoomID]bool + customUser *User +} + +func (puppet *Puppet) PhoneNumber() string { + return strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1) +} + +func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI { + fmt.Println() + fmt.Printf("puppent IntentFor: %+v", puppet) + fmt.Println() + if (!portal.IsPrivateChat() && puppet.customIntent == nil) || + (portal.backfilling && portal.bridge.Config.Bridge.InviteOwnPuppetForBackfilling) || + portal.Key.JID + skypeExt.NewUserSuffix == puppet.JID { + fmt.Println() + fmt.Println("puppent IntentFor0:", portal.Key.JID, puppet.JID) + fmt.Println("puppent IntentFor0:", portal.Key.JID, puppet.JID) + fmt.Println() + return puppet.DefaultIntent() + } + fmt.Println() + fmt.Printf("puppent IntentFor2: %+v", puppet.customIntent) + fmt.Println() + if portal.IsPrivateChat() && puppet.customIntent == nil{ + return puppet.DefaultIntent() + } + return puppet.customIntent +} + +func (puppet *Puppet) CustomIntent() *appservice.IntentAPI { + return puppet.customIntent +} + +func (puppet *Puppet) DefaultIntent() *appservice.IntentAPI { + fmt.Println() + fmt.Println("DefaultIntent puppet.MXID: ", puppet.MXID) + fmt.Println() + return puppet.bridge.AS.Intent(puppet.MXID) +} + +func (puppet *Puppet) UpdateAvatar(source *User, avatar *skypeExt.ProfilePicInfo) bool { + if avatar == nil { + return false + //var err error + //avatar, err = source.Conn.GetProfilePicThumb(puppet.JID) + //if err != nil { + // puppet.log.Warnln("Failed to get avatar:", err) + // return false + //} + } + + if avatar.Status != 0 { + return false + } + + if avatar.Tag == puppet.Avatar { + return false + } + + if len(avatar.URL) == 0 { + err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{}) + if err != nil { + puppet.log.Warnln("Failed to remove avatar:", err) + } + puppet.AvatarURL = id.ContentURI{} + puppet.Avatar = avatar.Tag + go puppet.updatePortalAvatar() + return true + } + + data, err := avatar.DownloadBytes() + if err != nil { + puppet.log.Warnln("Failed to download avatar:", err) + return false + } + + mime := http.DetectContentType(data) + resp, err := puppet.DefaultIntent().UploadBytes(data, mime) + if err != nil { + puppet.log.Warnln("Failed to upload avatar:", err) + return false + } + + puppet.AvatarURL = resp.ContentURI + err = puppet.DefaultIntent().SetAvatarURL(puppet.AvatarURL) + if err != nil { + puppet.log.Warnln("Failed to set avatar:", err) + } + puppet.Avatar = avatar.Tag + go puppet.updatePortalAvatar() + return true +} + +func (puppet *Puppet) UpdateName(source *User, contact skype.Contact) bool { + newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact) + if puppet.Displayname != newName && quality >= puppet.NameQuality { + err := puppet.DefaultIntent().SetDisplayName(newName) + if err == nil { + puppet.Displayname = newName + puppet.NameQuality = quality + go puppet.updatePortalName() + puppet.Update() + } else { + puppet.log.Warnln("Failed to set display name:", err) + } + return true + } + return false +} + +func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) { + if puppet.bridge.Config.Bridge.PrivateChatPortalMeta { + for _, portal := range puppet.bridge.GetAllPortalsByJID(puppet.JID) { + meta(portal) + } + } +} + +func (puppet *Puppet) updatePortalAvatar() { + puppet.updatePortalMeta(func(portal *Portal) { + if len(portal.MXID) > 0 { + _, err := portal.MainIntent().SetRoomAvatar(portal.MXID, puppet.AvatarURL) + if err != nil { + portal.log.Warnln("Failed to set avatar:", err) + } + } + portal.AvatarURL = puppet.AvatarURL + portal.Avatar = puppet.Avatar + portal.Update() + }) +} + +func (puppet *Puppet) updatePortalName() { + puppet.updatePortalMeta(func(portal *Portal) { + if len(portal.MXID) > 0 { + _, err := portal.MainIntent().SetRoomName(portal.MXID, puppet.Displayname) + if err != nil { + portal.log.Warnln("Failed to set name:", err) + } + } + portal.Name = puppet.Displayname + portal.Update() + }) +} + +func (puppet *Puppet) Sync(source *User, contact skype.Contact) { + fmt.Println("sync") + err := puppet.DefaultIntent().EnsureRegistered() + if err != nil { + puppet.log.Errorln("Failed to ensure registered:", err) + } + + //if contact.Jid == source.JID { + // contact.Notify = source.Conn.Info.Pushname + //} + avatar := &skypeExt.ProfilePicInfo{ + URL: contact.Profile.AvatarUrl, + Tag: contact.Profile.AvatarUrl, + Status: 0, + } + update := false + update = puppet.UpdateName(source, contact) || update + update = puppet.UpdateAvatar(source, avatar) || update + if update { + puppet.Update() + } +} diff --git a/skype-ext/chat.go b/skype-ext/chat.go new file mode 100644 index 0000000..5a41d6b --- /dev/null +++ b/skype-ext/chat.go @@ -0,0 +1,181 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package skypeExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type ChatUpdateCommand string + +const ( + ChatUpdateCommandAction ChatUpdateCommand = "action" +) + +type ChatUpdate struct { + JID string `json:"id"` + Command ChatUpdateCommand `json:"cmd"` + Data ChatUpdateData `json:"data"` +} + +type ChatActionType string + +const ( + ChatActionNameChange ChatActionType = "subject" + ChatActionAddTopic ChatActionType = "desc_add" + ChatActionRemoveTopic ChatActionType = "desc_remove" + ChatActionRestrict ChatActionType = "restrict" + ChatActionAnnounce ChatActionType = "announce" + ChatActionPromote ChatActionType = "promote" + ChatActionDemote ChatActionType = "demote" + ChatActionRemove ChatActionType = "remove" + ChatActionAdd ChatActionType = "add" + ChatActionIntroduce ChatActionType = "introduce" + ChatActionCreate ChatActionType = "create" +) + +const ( + ChatTopicUpdate ChatActionType = "ThreadActivity/TopicUpdate" + ChatPictureUpdate ChatActionType = "ThreadActivity/PictureUpdate" + ChatMemberAdd ChatActionType = "ThreadActivity/AddMember" + ChatMemberDelete ChatActionType = "ThreadActivity/DeleteMember" +) + +type ChatUpdateData struct { + Action ChatActionType + SenderJID string + + NameChange struct { + Name string `json:"subject"` + SetAt int64 `json:"s_t"` + SetBy string `json:"s_o"` + } + + AddTopic struct { + Topic string `json:"desc"` + ID string `json:"descId"` + SetAt int64 `json:"descTime"` + } + + RemoveTopic struct { + ID string `json:"descId"` + } + + Restrict bool + + Announce bool + + PermissionChange struct { + JIDs []string `json:"participants"` + } + + MemberAction struct { + JIDs []string `json:"participants"` + } + + Create struct { + Creation int64 `json:"creation"` + Name string `json:"subject"` + SetAt int64 `json:"s_t"` + SetBy string `json:"s_o"` + Admins []string `json:"admins"` + SuperAdmins []string `json:"superadmins"` + Regulars []string `json:"regulars"` + } +} + +func (cud *ChatUpdateData) UnmarshalJSON(data []byte) error { + var arr []json.RawMessage + err := json.Unmarshal(data, &arr) + if err != nil { + return err + } else if len(arr) < 3 { + return nil + } + + err = json.Unmarshal(arr[0], &cud.Action) + if err != nil { + return err + } + + err = json.Unmarshal(arr[1], &cud.SenderJID) + if err != nil { + return err + } + cud.SenderJID = strings.Replace(cud.SenderJID, OldUserSuffix, NewUserSuffix, 1) + + var unmarshalTo interface{} + switch cud.Action { + case ChatActionNameChange: + unmarshalTo = &cud.NameChange + case ChatActionAddTopic: + unmarshalTo = &cud.AddTopic + case ChatActionRemoveTopic: + unmarshalTo = &cud.RemoveTopic + case ChatActionRestrict: + unmarshalTo = &cud.Restrict + case ChatActionAnnounce: + unmarshalTo = &cud.Announce + case ChatActionPromote, ChatActionDemote: + unmarshalTo = &cud.PermissionChange + case ChatActionAdd, ChatActionRemove: + unmarshalTo = &cud.MemberAction + case ChatActionCreate: + unmarshalTo = &cud.Create + default: + return nil + } + err = json.Unmarshal(arr[2], unmarshalTo) + if err != nil { + return err + } + cud.NameChange.SetBy = strings.Replace(cud.NameChange.SetBy, OldUserSuffix, NewUserSuffix, 1) + for index, jid := range cud.PermissionChange.JIDs { + cud.PermissionChange.JIDs[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1) + } + return nil +} + +type ChatUpdateHandler interface { + whatsapp.Handler + HandleChatUpdate(ChatUpdate) +} + +func (ext *ExtendedConn) handleMessageChatUpdate(message []byte) { + var event ChatUpdate + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) + for _, handler := range ext.handlers { + chatUpdateHandler, ok := handler.(ChatUpdateHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(chatUpdateHandler) { + chatUpdateHandler.HandleChatUpdate(event) + } else { + go chatUpdateHandler.HandleChatUpdate(event) + } + } +} diff --git a/skype-ext/cmd.go b/skype-ext/cmd.go new file mode 100644 index 0000000..fcbda17 --- /dev/null +++ b/skype-ext/cmd.go @@ -0,0 +1,69 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package skypeExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type CommandType string + +const ( + CommandPicture CommandType = "picture" + CommandDisconnect CommandType = "disconnect" +) + +type Command struct { + Type CommandType `json:"type"` + JID string `json:"jid"` + + *ProfilePicInfo + Kind string `json:"kind"` + + Raw json.RawMessage `json:"-"` +} + +type CommandHandler interface { + whatsapp.Handler + HandleCommand(Command) +} + +func (ext *ExtendedConn) handleMessageCommand(message []byte) { + var event Command + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.Raw = message + event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) + for _, handler := range ext.handlers { + commandHandler, ok := handler.(CommandHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(commandHandler) { + commandHandler.HandleCommand(event) + } else { + go commandHandler.HandleCommand(event) + } + } +} diff --git a/skype-ext/jsonmessage.go b/skype-ext/jsonmessage.go new file mode 100644 index 0000000..c8fb351 --- /dev/null +++ b/skype-ext/jsonmessage.go @@ -0,0 +1,104 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package skypeExt + +import ( + "encoding/json" + "github.com/kelaresg/go-skypeapi" +) + +type JSONMessage []json.RawMessage + +type JSONMessageType string + +const ( + MessageMsgInfo JSONMessageType = "MsgInfo" + MessageMsg JSONMessageType = "Msg" + MessagePresence JSONMessageType = "Presence" + MessageStream JSONMessageType = "Stream" + MessageConn JSONMessageType = "Conn" + MessageProps JSONMessageType = "Props" + MessageCmd JSONMessageType = "Cmd" + MessageChat JSONMessageType = "Chat" + MessageCall JSONMessageType = "Call" +) + +func (ext *ExtendedConn) HandleError(error) {} + +type UnhandledJSONMessageHandler interface { + skype.Handler + HandleUnhandledJSONMessage(string) +} + +type JSONParseErrorHandler interface { + skype.Handler + HandleJSONParseError(error) +} + +func (ext *ExtendedConn) jsonParseError(err error) { + for _, handler := range ext.handlers { + errorHandler, ok := handler.(JSONParseErrorHandler) + if !ok { + continue + } + errorHandler.HandleJSONParseError(err) + } +} + +func (ext *ExtendedConn) HandleJsonMessage(message string) { + msg := JSONMessage{} + err := json.Unmarshal([]byte(message), &msg) + if err != nil || len(msg) < 2 { + ext.jsonParseError(err) + return + } + + var msgType JSONMessageType + json.Unmarshal(msg[0], &msgType) + + switch msgType { + //case MessagePresence: + // ext.handleMessagePresence(msg[1]) + //case MessageStream: + // ext.handleMessageStream(msg[1:]) + //case MessageConn: + // ext.handleMessageConn(msg[1]) + //case MessageProps: + // ext.handleMessageProps(msg[1]) + //case MessageMsgInfo, MessageMsg: + // ext.handleMessageMsgInfo(msgType, msg[1]) + //case MessageCmd: + // ext.handleMessageCommand(msg[1]) + //case MessageChat: + // ext.handleMessageChatUpdate(msg[1]) + //case MessageCall: + // ext.handleMessageCall(msg[1]) + default: + for _, handler := range ext.handlers { + ujmHandler, ok := handler.(UnhandledJSONMessageHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(ujmHandler) { + ujmHandler.HandleUnhandledJSONMessage(message) + } else { + go ujmHandler.HandleUnhandledJSONMessage(message) + } + } + } +} diff --git a/skype-ext/presence.go b/skype-ext/presence.go new file mode 100644 index 0000000..debca25 --- /dev/null +++ b/skype-ext/presence.go @@ -0,0 +1,9 @@ +package skypeExt + +import skype "github.com/kelaresg/go-skypeapi" + +type Presence struct { + Id string + Availability string + Status skype.Presence +} \ No newline at end of file diff --git a/skype-ext/skype.go b/skype-ext/skype.go new file mode 100644 index 0000000..7b4be96 --- /dev/null +++ b/skype-ext/skype.go @@ -0,0 +1,304 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package skypeExt + +import ( + "encoding/json" + "fmt" + "github.com/kelaresg/go-skypeapi" + "io" + "io/ioutil" + "net/http" + "strings" + "time" +) + +const ( + OldUserSuffix = "" + NewUserSuffix = "@s.skype.net" + GroupSuffix = "@thread.skype" +) + +type ExtendedConn struct { + *skype.Conn + + handlers []skype.Handler +} + +func ExtendConn(conn *skype.Conn) *ExtendedConn { + ext := &ExtendedConn{ + Conn: conn, + } + ext.Conn.AddHandler(ext) + return ext +} + +func (ext *ExtendedConn) AddHandler(handler skype.Handler) { + ext.Conn.AddHandler(handler) + ext.handlers = append(ext.handlers, handler) +} + + +func (ext *ExtendedConn) RemoveHandler(handler skype.Handler) bool { + ext.Conn.RemoveHandler(handler) + for i, v := range ext.handlers { + if v == handler { + ext.handlers = append(ext.handlers[:i], ext.handlers[i+1:]...) + return true + } + } + return false +} + +func (ext *ExtendedConn) RemoveHandlers() { + ext.Conn.RemoveHandlers() + ext.handlers = make([]skype.Handler, 0) +} + +func (ext *ExtendedConn) shouldCallSynchronously(handler skype.Handler) bool { + //sh, ok := handler.(skype.SyncHandler) + //return ok && sh.ShouldCallSynchronously() + return true +} + +func (ext *ExtendedConn) ShouldCallSynchronously() bool { + return true +} + +type GroupInfo struct { + JID string `json:"jid"` + OwnerJID string `json:"owner"` + + Name string `json:"subject"` + NameSetTime int64 `json:"subjectTime"` + NameSetBy string `json:"subjectOwner"` + + Topic string `json:"desc"` + TopicID string `json:"descId"` + TopicSetAt int64 `json:"descTime"` + TopicSetBy string `json:"descOwner"` + + GroupCreated int64 `json:"creation"` + + Status int16 `json:"status"` + + Participants []struct { + JID string `json:"id"` + IsAdmin bool `json:"isAdmin"` + IsSuperAdmin bool `json:"isSuperAdmin"` + } `json:"participants"` +} + +func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) { + data, err := ext.Conn.GetConversation(jid) + if err != nil { + return nil, fmt.Errorf("failed to get group metadata: %v", err) + } + //content := <-data + //info := data + membersStr := data.ThreadProperties.Members + var members []string + if len(membersStr) < 1 { + resp, _ := ext.Conn.GetConsumptionHorizons(jid) + for _, con := range resp.ConsumptionHorizons { + members = append(members, con.Id) + } + } else { + err = json.Unmarshal([]byte(membersStr), &members) + } + info := &GroupInfo{} + fmt.Println() + fmt.Println("GetGroupMetaData: ", members) + fmt.Println() + for _, participant := range members { + type a struct { + JID string `json:"id"` + IsAdmin bool `json:"isAdmin"` + IsSuperAdmin bool `json:"isSuperAdmin"` + } + isSuperAdmin := false + if "8:" + ext.Conn.UserProfile.Username == participant { + isSuperAdmin = true + } + info.Participants = append(info.Participants, a{ + participant + NewUserSuffix, + false, + isSuperAdmin, + }) + + personId := participant + NewUserSuffix + if _, ok := ext.Conn.Store.Contacts[personId]; !ok { + participantArr := strings.Split(participant, "8:") + if participantArr[1] != "" { + ext.Conn.NameSearch(participantArr[1]) + } + } + } + info.Topic = data.ThreadProperties.Topic + info.Name = data.ThreadProperties.Topic + fmt.Println() + fmt.Println("GetGroupMetaData:3 ") + fmt.Println() + //info.NameSetBy = info.NameSetBy + NewUserSuffix + //info.TopicSetBy = info.TopicSetBy + NewUserSuffix + + return info, nil +} + +type ProfilePicInfo struct { + URL string `json:"eurl"` + Tag string `json:"tag"` + + Status int16 `json:"status"` + Authorization string `json:"authorization"` +} + +func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) { + if ppi.Authorization != "" { + client := &http.Client{ + Timeout: 20 * time.Second, + } + headers := map[string]string{ + "X-Client-Version": "0/0.0.0.0", + "Authorization": ppi.Authorization, // "skype_token " + Conn.LoginInfo.SkypeToken, + } + req, err := http.NewRequest("GET", ppi.URL, nil) + if err != nil { + return nil, err + } + for k, v := range headers { + req.Header.Set(k, v) + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + return resp.Body, nil + }else { + resp, err := http.Get(ppi.URL) + if err != nil { + return nil, err + } + return resp.Body, nil + } + +} + +func (ppi *ProfilePicInfo) DownloadBytes() ([]byte, error) { + body, err := ppi.Download() + if err != nil { + return nil, err + } + defer body.Close() + data, err := ioutil.ReadAll(body) + return data, err +} + +func (ext *ExtendedConn) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) { + //data, err := ext.Conn.GetProfilePicThumb(jid) + //if err != nil { + // return nil, fmt.Errorf("failed to get avatar: %v", err) + //} + //content := <-data + //info := &ProfilePicInfo{} + //err = json.Unmarshal([]byte(content), info) + //if err != nil { + // return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) + //} + //return info, nil + return nil, nil +} + +func (ext *ExtendedConn) HandleGroupInvite(groupJid string, numbers[]string) (err error) { + var parts []string + parts = append(parts, numbers...) + members := skype.Members{} + members = skype.Members{} + for _, memberId := range numbers { + members.Members = append(members.Members, skype.Member{ + Id: "8:"+memberId, + Role: "Admin", + }) + } + err = ext.Conn.AddMember(members, groupJid) + if err != nil { + fmt.Printf("%s Handle Invite err", err) + } + return +} + +func (ext *ExtendedConn) HandleGroupJoin(code string) (err error, codeinfo skype.JoinToConInfo) { + err, codeinfo = ext.Conn.JoinConByCode(code) + member := skype.Member{ + Id: "8:"+ext.UserProfile.Username, + Role: "Admin", + } + Members := skype.Members{} + Members.Members = append(Members.Members, member) + //cli.AddMember(cli.LoginInfo.LocationHost, cli.LoginInfo.SkypeToken, cli.LoginInfo.RegistrationTokenStr, Members, rsp.Resource) + err = ext.AddMember(Members, codeinfo.Resource) + + member2 := skype.Member{ + Id: "8:"+ext.UserProfile.Username, + Role: "User", + } + mewMembers := skype.Members{} + mewMembers.Members = append(mewMembers.Members, member2) + err = ext.AddMember( mewMembers, codeinfo.Resource) + return +} + +func (ext *ExtendedConn) HandleGroupShare(groupJid string) (err error, link string) { + + res, err := ext.Conn.GetConJoinUrl(groupJid) + if err != nil { + return err, "" + } + link = res.Url + return +} + + +func (ext *ExtendedConn) HandleGroupKick(groupJid string, numbers[]string) (err error) { + for _, number := range numbers{ + err = ext.Conn.RemoveMember(groupJid, number) + if err != nil { + fmt.Printf("%s Handle kick err", err) + } + } + return +} + +func (ext *ExtendedConn) HandleGroupCreate(numbers skype.Members) (err error) { + //var parts []string + //parts = append(parts, numbers...) + err = ext.Conn.CreateConversationGroup(numbers) + //if err != nil { + // fmt.Printf("%s HandleGroupCreate err", err) + //} + return +} + +func (ext *ExtendedConn) HandleGroupLeave(groupJid string) (err error) { + fmt.Println("groyp id", groupJid) + fmt.Println("ext.UserProfile.Username", ext.UserProfile.Username) + err = ext.Conn.RemoveMember(groupJid, "8:"+ext.UserProfile.Username) + if err != nil { + fmt.Printf("%s HandleGroupLeave err", err) + } + return +} diff --git a/skype-ext/userid.go b/skype-ext/userid.go new file mode 100644 index 0000000..2150b97 --- /dev/null +++ b/skype-ext/userid.go @@ -0,0 +1,154 @@ +package skypeExt + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" +) + +// UserID represents a Matrix user ID. +// https://matrix.org/docs/spec/appendices#user-identifiers +type UserID string + +func NewUserID(localpart, homeserver string) UserID { + return UserID(fmt.Sprintf("@%s:%s", localpart, homeserver)) +} + +func NewEncodedUserID(localpart, homeserver string) UserID { + return NewUserID(EncodeUserLocalpart(localpart), homeserver) +} + +// Parse parses the user ID into the localpart and server name. +// See http://matrix.org/docs/spec/intro.html#user-identifiers +func (userID UserID) Parse() (localpart, homeserver string, err error) { + if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') { + err = fmt.Errorf("%s is not a valid user id", userID) + return + } + parts := strings.SplitN(string(userID), ":", 2) + localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1] + return +} + +func (userID UserID) ParseAndDecode() (localpart, homeserver string, err error) { + localpart, homeserver, err = userID.Parse() + if err == nil { + localpart, err = DecodeUserLocalpart(localpart) + } + return +} + +func (userID UserID) String() string { + return string(userID) +} + +const lowerhex = "0123456789abcdef" + +// encode the given byte using quoted-printable encoding (e.g "=2f") +// and writes it to the buffer +// See https://golang.org/src/mime/quotedprintable/writer.go +func encode(buf *bytes.Buffer, b byte) { + buf.WriteByte('=') + buf.WriteByte(lowerhex[b>>4]) + buf.WriteByte(lowerhex[b&0x0f]) +} + +// escape the given alpha character and writes it to the buffer +func escape(buf *bytes.Buffer, b byte) { + buf.WriteByte('_') + if b == '_' { + buf.WriteByte('_') // another _ + } else { + buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z + } +} + +func shouldEncode(b byte) bool { + return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z') +} + +func shouldEscape(b byte) bool { + return (b >= 'A' && b <= 'Z') || b == '_' +} + +func isValidByte(b byte) bool { + return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-' +} + +func isValidEscapedChar(b byte) bool { + return b == '_' || (b >= 'a' && b <= 'z') +} + +// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form. +// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets +// +// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z +// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges +// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs) +// and converted to lower-case hex with a leading "=". For example: +// Alph@Bet_50up => _alph=40_bet=5f50up +func EncodeUserLocalpart(str string) string { + strBytes := []byte(str) + var outputBuffer bytes.Buffer + for _, b := range strBytes { + if shouldEncode(b) { + encode(&outputBuffer, b) + } else if shouldEscape(b) { + escape(&outputBuffer, b) + } else { + outputBuffer.WriteByte(b) + } + } + return outputBuffer.String() +} + +// DecodeUserLocalpart decodes the given string back into the original input string. +// Returns an error if the given string is not a valid user ID localpart encoding. +// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets +// +// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For +// example: +// _alph=40_bet=5f50up => Alph@Bet_50up +// Returns an error if the input string contains characters outside the +// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has +// an invalid _ escaped byte (e.g. "_5"). +func DecodeUserLocalpart(str string) (string, error) { + strBytes := []byte(str) + var outputBuffer bytes.Buffer + for i := 0; i < len(strBytes); i++ { + b := strBytes[i] + if !isValidByte(b) { + return "", fmt.Errorf("Byte pos %d: Invalid byte", i) + } + + if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _ + if i+1 >= len(strBytes) { + return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i) + } + if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping + return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i) + } + if strBytes[i+1] == '_' { + outputBuffer.WriteByte('_') + } else { + outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z + } + i++ // skip next byte since we just handled it + } else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8 + if i+2 >= len(strBytes) { + return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i) + } + dst := make([]byte, 1) + _, err := hex.Decode(dst, strBytes[i+1:i+3]) + if err != nil { + return "", err + } + outputBuffer.WriteByte(dst[0]) + i += 2 // skip next 2 bytes since we just handled it + } else { // pass through + outputBuffer.WriteByte(b) + } + } + return outputBuffer.String(), nil +} diff --git a/test/main.go b/test/main.go new file mode 100644 index 0000000..ce4ec5d --- /dev/null +++ b/test/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/base64" + "fmt" + "regexp" + "strings" + "time" +) + +func find(htm string, re *regexp.Regexp) [][]string { + imgs := re.FindAllStringSubmatch(htm, -1) + //fmt.Println(re.FindAllStringIndex(htm, -1)) + //return imgs + //out := make([]string, len(imgs)) + for _, img := range imgs { + for _, img2 := range img { + fmt.Println(img2) + } + } + return imgs +} +func main () { + a := "2020-07-15T03:47:51.217Z" + t, _ := time.Parse(time.RFC3339, a) + fmt.Println(t.Unix()) + return + //str := `1lyle21211lyle2121` + // str := `
In reply to @skype&8-live-1163765691:oliver.matrix.host
qqqqqqq
9999999` + //str := `[1594719165] Oliver1 Zhao2↵: 3333333↵↵<<< 1111111` + str := `[1594808528] Oliver1 Zhao2 +: 00000000 +<<< 1111111111` + //r,_:=regexp.Compile(".*") + //r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) + //r := regexp.MustCompile(`]+\bhref="(.*?)://matrix\.to/#/@skype&([^"]+):(.*?)">(.*?)*`) + str = strings.ReplaceAll(str, "\n", "") + r := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)".*>.*?(.*?).*?(.*)`) + //patten := `` + find(str, r) + //fmt.Println(find(str, r)) +} + //cli, _ := skype.NewConn() + //_ = cli.Login("1", "3") + //avatar := &skypeExt.ProfilePicInfo{ + // URL: "https://api.asm.skype.com/v1/objects/0-ea-d6-ee876d1872e567ed85d89efae9b05971/views/swx_avatar", + // Tag: "https://api.asm.skype.com/v1/objects/0-ea-d6-ee876d1872e567ed85d89efae9b05971/views/swx_avatar", + // Status: 0, + // Authorization: "skype_token " + cli.LoginInfo.SkypeToken, + //} + + //data, _ := avatar.DownloadBytes() + //fmt.Println("DownloadBytes: ", string(data)) + //type user struct { + // Presences map[string]*skypeExt.Presence + //} + //a := user{Presences:make(map[string]*skypeExt.Presence)} + //a.Presences["1"] = &skypeExt.Presence{ + // Id: "1", + // Availability: "313", + // Status: "31312", + //} + //fmt.Printf("%+v", a) + //body := `{"eventMessages":[{"id":1005,"type":"EventMessage","resourceType":"NewMessage","time":"2020-06-19T11:19:57Z","resourceLink":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/conversations/19:77d9cf34f8d6419fbb3542bd6304ac33@thread.skype/messages/1592565597315","resource":{"contentformat":"FN=MS%20Shell%20Dlg; EF=; CO=0; CS=0; PF=0","messagetype":"ThreadActivity/PictureUpdate","originalarrivaltime":"2020-06-19T11:19:57.315Z","ackrequired":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/conversations/ALL/messages/1592565597315/ack","type":"Message","version":"1592565597315","contenttype":"text/plain; charset=UTF-8","origincontextid":"8571783950420663070","isactive":false,"from":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/contacts/19:77d9cf34f8d6419fbb3542bd6304ac33@thread.skype","id":"1592565597315","conversationLink":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/conversations/19:77d9cf34f8d6419fbb3542bd6304ac33@thread.skype","counterpartymessageid":"1592565597315","threadtopic":"gteat4","content":"15925655974408:live:.cid.d3feb90dceeb51ccURL@https://api.asm.skype.com/v1/objects/0-ea-d1-df4643685906b8826aaf6faddbbd572d/views/avatar_fullsize","composetime":"2020-06-19T11:19:57.315Z"}}]}` + //var bodyContent struct { + // EventMessages []skype.Conversation `json:"eventMessages"` + //} + //_ = json.Unmarshal([]byte(body), &bodyContent) + //if len(bodyContent.EventMessages) > 0 { + // for _, message := range bodyContent.EventMessages { + // if message.Type == "EventMessage" { + // messageType := skypeExt.ChatActionType(message.Resource.MessageType) + // switch messageType { + // case skypeExt.TopicUpdate: + // topicContent := skype.TopicContent{} + // //把xml数据解析成bs对象 + // xml.Unmarshal([]byte(message.Resource.Content), &topicContent) + // message.Resource.SendId = topicContent.Initiator + skypeExt.NewUserSuffix + // //go portal.UpdateName(cmd.ThreadTopic, cmd.SendId) + // case skypeExt.PictureUpdate: + // topicContent := skype.PictureContent{} + // //把xml数据解析成bs对象 + // xml.Unmarshal([]byte(message.Resource.Content), &topicContent) + // message.Resource.SendId = topicContent.Initiator + skypeExt.NewUserSuffix + // url := strings.TrimPrefix(topicContent.Value, "URL@") + // fmt.Println(url) + // //avatar := &skypeExt.ProfilePicInfo{ + // // URL: url, + // // Tag: topicContent.Value, + // // Status: 0, + // //} + // //go portal.UpdateAvatar(user, avatar) + // } + // } + // } + //} + + //fmt.Println("hello https://tool.lu/") + //membersStr := "[\"8:live:1163765691\",\"8:live:zhaosl_4\"]" + //var members []string + //_ = json.Unmarshal([]byte(membersStr), &members) + //fmt.Println(members) + //for _, participant := range members { + // fmt.Println(participant) + //} + //type a string + //type b struct { + // C a + //} + //v := b{ + // C: a("8:live:1163765691"), + //} + //fmt.Println(strings.HasSuffix("28:0d5d6cff-595d-49d7-9cf8-973173f5233b@s.skype.net", skypeExt.NewUserSuffix)) + //return + //rand.Seed(time.Now().UnixNano()) + //fmt.Sprintf("%04v", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000)) + //currentTimeNanoStr := strconv.FormatInt(time.Now().UnixNano(), 10) + //currentTimeNanoStr = currentTimeNanoStr[:len(currentTimeNanoStr)-3] + ////clientmessageid := currentTimeNanoStr + randomStr + //fmt.Println(fmt.Sprintf("%04v", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000))) + //return + ////Parse("@whatsapp_8:live:1163765691:oliver.matrix.host") + //userIDRegex, _ := regexp.Compile("@whatsapp_8:live:1163765691:oliver.matrix.host") + //match := userIDRegex.FindStringSubmatch(string("@whatsapp_8:live:1163765691:oliver.matrix.host")) + //fmt.Println(match) +//} + +func Parse(userID string)(localpart, homeserver string, err error) { + if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') { + err = fmt.Errorf("%s is not a valid user id", userID) + return + } + parts := strings.Split(string(userID), ":") + localpart, homeserver = strings.TrimPrefix(strings.Join(parts[:len(parts)-1], ":"), "@"), parts[len(parts)-1] + localpart = base64.StdEncoding.EncodeToString([]byte(localpart)) + fmt.Println(localpart, homeserver) + return +} diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..7bd5dae --- /dev/null +++ b/types/types.go @@ -0,0 +1,7 @@ +package types + +// SkypeID is a skype useID. +type SkypeID = string + +// SkypeMessageID is the internal Client Message ID of a skype message. +type SkypeMessageID = string diff --git a/user.go b/user.go new file mode 100644 index 0000000..5f9774f --- /dev/null +++ b/user.go @@ -0,0 +1,1158 @@ +package main + +import ( + "encoding/json" + "encoding/xml" + "fmt" + skype "github.com/kelaresg/go-skypeapi" + skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "sort" + //"strconv" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/skip2/go-qrcode" + log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix" + + "github.com/Rhymen/go-whatsapp" + waProto "github.com/Rhymen/go-whatsapp/binary/proto" + + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/database" + "github.com/kelaresg/matrix-skype/types" + "github.com/kelaresg/matrix-skype/whatsapp-ext" +) + +type User struct { + *database.User + Conn *skypeExt.ExtendedConn + + bridge *Bridge + log log.Logger + + Admin bool + Whitelisted bool + RelaybotWhitelisted bool + + IsRelaybot bool + + ConnectionErrors int + CommunityID string + + cleanDisconnection bool + + chatListReceived chan struct{} + syncPortalsDone chan struct{} + + messages chan PortalMessage + syncLock sync.Mutex + + mgmtCreateLock sync.Mutex + + contactsPresence map[string]*skypeExt.Presence +} + +func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User { + _, isPuppet := bridge.ParsePuppetMXID(userID) + fmt.Println("GetUserByMXID0", userID) + fmt.Println("GetUserByMXID1", bridge.Bot.UserID) + if isPuppet || userID == bridge.Bot.UserID { + fmt.Println("GetUserByMXID2", userID) + fmt.Println("GetUserByMXID3", bridge.Bot.UserID) + return nil + } + bridge.usersLock.Lock() + defer bridge.usersLock.Unlock() + user, ok := bridge.usersByMXID[userID] + if !ok { + return bridge.loadDBUser(bridge.DB.User.GetByMXID(userID), &userID) + } + return user +} + +func (bridge *Bridge) GetUserByJID(userID types.SkypeID) *User { + bridge.usersLock.Lock() + defer bridge.usersLock.Unlock() + user, ok := bridge.usersByJID[userID] + if !ok { + return bridge.loadDBUser(bridge.DB.User.GetByJID(userID), nil) + } + return user +} + +func (user *User) getSkypeIdByMixId() (skypeId string){ + mixIdArr := strings.Split(string(user.MXID), "&") + idArr := strings.Split(mixIdArr[1], ":"+user.bridge.Config.Homeserver.Domain) + skypeId = strings.Replace(idArr[0], "-",":",2) + return skypeId +} + +func (user *User) addToJIDMap() { + user.bridge.usersLock.Lock() + user.bridge.usersByJID[user.JID] = user + user.bridge.usersLock.Unlock() +} + +func (user *User) removeFromJIDMap() { + user.bridge.usersLock.Lock() + delete(user.bridge.usersByJID, user.JID) + user.bridge.usersLock.Unlock() +} + +func (bridge *Bridge) GetAllUsers() []*User { + bridge.usersLock.Lock() + defer bridge.usersLock.Unlock() + dbUsers := bridge.DB.User.GetAll() + output := make([]*User, len(dbUsers)) + for index, dbUser := range dbUsers { + user, ok := bridge.usersByMXID[dbUser.MXID] + if !ok { + user = bridge.loadDBUser(dbUser, nil) + } + user.contactsPresence = make(map[string]*skypeExt.Presence) + output[index] = user + } + return output +} + +func (bridge *Bridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User { + if dbUser == nil { + if mxid == nil { + return nil + } + dbUser = bridge.DB.User.New() + dbUser.MXID = *mxid + dbUser.Insert() + } + user := bridge.NewUser(dbUser) + bridge.usersByMXID[user.MXID] = user + if len(user.JID) > 0 { + bridge.usersByJID[user.JID] = user + } + if len(user.ManagementRoom) > 0 { + bridge.managementRooms[user.ManagementRoom] = user + } + return user +} + +func (user *User) GetPortals() []*Portal { + keys := user.User.GetPortalKeys() + portals := make([]*Portal, len(keys)) + + user.bridge.portalsLock.Lock() + for i, key := range keys { + portal, ok := user.bridge.portalsByJID[key] + if !ok { + fmt.Println("loadDBPortal4") + portal = user.bridge.loadDBPortal(user.bridge.DB.Portal.GetByJID(key), &key) + } + portals[i] = portal + } + user.bridge.portalsLock.Unlock() + return portals +} + +func (bridge *Bridge) NewUser(dbUser *database.User) *User { + user := &User{ + User: dbUser, + bridge: bridge, + log: bridge.Log.Sub("User").Sub(string(dbUser.MXID)), + + IsRelaybot: false, + + chatListReceived: make(chan struct{}, 1), + syncPortalsDone: make(chan struct{}, 1), + messages: make(chan PortalMessage, 256), + } + user.RelaybotWhitelisted = user.bridge.Config.Bridge.Permissions.IsRelaybotWhitelisted(user.MXID) + user.Whitelisted = user.bridge.Config.Bridge.Permissions.IsWhitelisted(user.MXID) + user.Admin = user.bridge.Config.Bridge.Permissions.IsAdmin(user.MXID) + go user.handleMessageLoop() + return user +} + +func (user *User) GetManagementRoom() id.RoomID { + if len(user.ManagementRoom) == 0 { + user.mgmtCreateLock.Lock() + defer user.mgmtCreateLock.Unlock() + if len(user.ManagementRoom) > 0 { + return user.ManagementRoom + } + resp, err := user.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{ + Topic: "WhatsApp bridge notices", + IsDirect: true, + }) + if err != nil { + user.log.Errorln("Failed to auto-create management room:", err) + } else { + user.SetManagementRoom(resp.RoomID) + } + } + return user.ManagementRoom +} + +func (user *User) SetManagementRoom(roomID id.RoomID) { + existingUser, ok := user.bridge.managementRooms[roomID] + if ok { + existingUser.ManagementRoom = "" + existingUser.Update() + } + + user.ManagementRoom = roomID + user.bridge.managementRooms[user.ManagementRoom] = user + user.Update() +} + +func (user *User) SetSession(session *skype.Session) { + user.Session = session + if session == nil { + user.LastConnection = 0 + } + user.Update() +} + +//func (user *User) Connect(evenIfNoSession bool) bool { +// if user.Conn != nil { +// return true +// } else if !evenIfNoSession && user.Session == nil { +// return false +// } +// user.log.Debugln("Connecting to WhatsApp") +// timeout := time.Duration(user.bridge.Config.Bridge.ConnectionTimeout) +// if timeout == 0 { +// timeout = 20 +// } +// conn, err := whatsapp.NewConn(timeout * time.Second) +// if err != nil { +// user.log.Errorln("Failed to connect to WhatsApp:", err) +// msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp server. "+ +// "This indicates a network problem on the bridge server. See bridge logs for more info.", true, false) +// _, _ = user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, msg) +// return false +// } +// user.Conn = whatsappExt.ExtendConn(conn) +// _ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", WAVersion) +// user.log.Debugln("WhatsApp connection successful") +// user.Conn.AddHandler(user) +// return user.RestoreSession() +//} + +func (user *User) Connect(evenIfNoSession bool) bool { + if user.Conn != nil { + return true + } else if !evenIfNoSession && user.Session == nil { + return false + } + user.log.Debugln("Connecting to skype") + timeout := time.Duration(user.bridge.Config.Bridge.ConnectionTimeout) + if timeout == 0 { + timeout = 20 + } + conn, err := skype.NewConn() + if err != nil { + user.log.Errorln("Failed to connect to skype:", err) + //msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp server. "+ + // "This indicates a network problem on the bridge server. See bridge logs for more info.", true, false) + //_, _ = user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, msg) + return false + } + user.Conn = skypeExt.ExtendConn(conn) + //_ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", WAVersion) + user.log.Debugln("skype connection successful") + user.Conn.AddHandler(user) + return user.RestoreSession() +} + +func (user *User) RestoreSession() bool { + if user.Session != nil { + //sess, err := user.Conn.RestoreWithSession(*user.Session) + //if err == whatsapp.ErrAlreadyLoggedIn { + // return true + //} else if err != nil { + // user.log.Errorln("Failed to restore session:", err) + // msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp "+ + // "on your phone is reachable and use `reconnect` to try connecting again.", true, false) + // _, _ = user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, msg) + // user.log.Debugln("Disconnecting due to failed session restore...") + // _, err := user.Conn.Disconnect() + // if err != nil { + // user.log.Errorln("Failed to disconnect after failed session restore:", err) + // } + // return false + //} + //user.ConnectionErrors = 0 + //user.SetSession(&sess) + //user.log.Debugln("Session restored successfully") + //user.PostLogin() + } + return true +} + +func (user *User) HasSession() bool { + //return user.Session != nil + return true +} + +func (user *User) IsConnected() bool { + //return user.Conn != nil && user.Conn.IsConnected() && user.Conn.IsLoggedIn() + return true +} + +func (user *User) IsLoginInProgress() bool { + //return user.Conn != nil && user.Conn.IsLoginInProgress() + return false +} + +func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- id.EventID) { + var qrEventID id.EventID + for code := range qrChan { + if code == "stop" { + return + } + qrCode, err := qrcode.Encode(code, qrcode.Low, 256) + if err != nil { + user.log.Errorln("Failed to encode QR code:", err) + ce.Reply("Failed to encode QR code: %v", err) + return + } + + bot := user.bridge.AS.BotClient() + + resp, err := bot.UploadBytes(qrCode, "image/png") + if err != nil { + user.log.Errorln("Failed to upload QR code:", err) + ce.Reply("Failed to upload QR code: %v", err) + return + } + + if qrEventID == "" { + sendResp, err := bot.SendImage(ce.RoomID, code, resp.ContentURI) + if err != nil { + user.log.Errorln("Failed to send QR code to user:", err) + return + } + qrEventID = sendResp.EventID + eventIDChan <- qrEventID + } else { + _, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{ + MsgType: event.MsgImage, + Body: code, + URL: resp.ContentURI.CUString(), + NewContent: &event.MessageEventContent{ + MsgType: event.MsgImage, + Body: code, + URL: resp.ContentURI.CUString(), + }, + RelatesTo: &event.RelatesTo{ + Type: event.RelReplace, + EventID: qrEventID, + }, + }) + if err != nil { + user.log.Errorln("Failed to send edited QR code to user:", err) + } + } + } +} + +func (user *User) Login(ce *CommandEvent, name string, password string) { + if user.contactsPresence == nil { + user.contactsPresence = make(map[string]*skypeExt.Presence) + } + session, err := user.Conn.Login(name, password) + if err != nil { + user.log.Errorln("Failed to login:", err) + ce.Reply("Please confirm that your account password is entered correctly") + return + } + fmt.Println("user.MXID", user.MXID) + ce.Reply("Successfully logged in") + + // subscribe basic + user.Conn.Subscribes() + // subscribe conta + err = user.Conn.Conn.ContactList(user.Conn.UserProfile.Username) + if err == nil{ + var userIds []string + for _, contact := range user.Conn.Store.Contacts { + //fmt.Println(contact.PersonId) + if strings.Index(contact.PersonId, "28:") > -1 { + continue + } + userId := strings.Replace(contact.PersonId, skypeExt.NewUserSuffix, "", 1) + userIds = append(userIds, userId) + } + ce.User.Conn.SubscribeUsers(userIds) + //fmt.Println("Login user0: ", user.JID) + go loopPresence(user) + } + go user.Conn.Poll() + + user.ConnectionErrors = 0 + user.SetSession(&session) + user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix + user.addToJIDMap() + //user.PostLogin() + ce.User.Conn.GetConversations("") +} +func loopPresence(user *User) { +Loop: + for i := 0; i <= 1000; i++ { + if i > 1000 { + goto Loop + } + for cid, contact := range user.contactsPresence { + puppet := user.bridge.GetPuppetByJID(cid) + _ = puppet.DefaultIntent().SetPresence(event.Presence(strings.ToLower(contact.Availability))) + } + time.Sleep(39 * time.Second) + } +} +func (user *User) Login1(ce *CommandEvent) { + //qrChan := make(chan string, 3) + //eventIDChan := make(chan id.EventID, 1) + //go user.loginQrChannel(ce, qrChan, eventIDChan) + //session, err := user.Conn.LoginWithRetry(qrChan, user.bridge.Config.Bridge.LoginQRRegenCount) + //qrChan <- "stop" + //if err != nil { + // var eventID id.EventID + // select { + // case eventID = <-eventIDChan: + // default: + // } + // reply := event.MessageEventContent{ + // MsgType: event.MsgText, + // } + // if err == whatsapp.ErrAlreadyLoggedIn { + // reply.Body = "You're already logged in" + // } else if err == whatsapp.ErrLoginInProgress { + // reply.Body = "You have a login in progress already." + // } else if err == whatsapp.ErrLoginTimedOut { + // reply.Body = "QR code scan timed out. Please try again." + // } else { + // user.log.Warnln("Failed to log in:", err) + // reply.Body = fmt.Sprintf("Unknown error while logging in: %v", err) + // } + // msg := reply + // if eventID != "" { + // msg.NewContent = &reply + // msg.RelatesTo = &event.RelatesTo{ + // Type: event.RelReplace, + // EventID: eventID, + // } + // } + // _, _ = ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &msg) + // return + //} + //// TODO there's a bit of duplication between this and the provisioning API login method + //// Also between the two logout methods (commands.go and provisioning.go) + //user.ConnectionErrors = 0 + //user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) + //user.addToJIDMap() + //user.SetSession(&session) + //ce.Reply("Successfully logged in, synchronizing chats...") + //user.PostLogin() +} + +type Chat struct { + Portal *Portal + LastMessageTime uint64 + Contact skype.Conversation +} + +type ChatList []Chat + +func (cl ChatList) Len() int { + return len(cl) +} + +func (cl ChatList) Less(i, j int) bool { + return cl[i].LastMessageTime > cl[j].LastMessageTime +} + +func (cl ChatList) Swap(i, j int) { + cl[i], cl[j] = cl[j], cl[i] +} + +func (user *User) PostLogin() { + user.log.Debugln("Locking processing of incoming messages and starting post-login sync") + user.syncLock.Lock() + go user.intPostLogin() +} + +func (user *User) tryAutomaticDoublePuppeting() { + fmt.Println("tryAutomaticDoublePuppeting") + if len(user.bridge.Config.Bridge.LoginSharedSecret) == 0 { + fmt.Println("tryAutomaticDoublePuppeting-1", user.bridge.Config.Bridge.LoginSharedSecret) + // Automatic login not enabled + return + } else if _, homeserver, _ := user.MXID.Parse(); homeserver != user.bridge.Config.Homeserver.Domain { + fmt.Println("tryAutomaticDoublePuppeting-2", user.MXID) + fmt.Println("tryAutomaticDoublePuppeting-21", homeserver) + fmt.Println("tryAutomaticDoublePuppeting--3", user.bridge.Config.Homeserver.Domain) + // user is on another homeserver + return + } + fmt.Println("tryAutomaticDoublePuppeting1") + puppet := user.bridge.GetPuppetByJID(user.JID) + if len(puppet.CustomMXID) > 0 { + // Custom puppet already enabled + return + } + fmt.Println("tryAutomaticDoublePuppeting2", user.MXID) + accessToken, err := puppet.loginWithSharedSecret(user.MXID) + if err != nil { + user.log.Warnln("Failed to login with shared secret:", err) + return + } + err = puppet.SwitchCustomMXID(accessToken, user.MXID) + if err != nil { + puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err) + return + } + user.log.Infoln("Successfully automatically enabled custom puppet") +} + +func (user *User) intPostLogin() { + defer user.syncLock.Unlock() + user.createCommunity() + user.tryAutomaticDoublePuppeting() + + select { + case <-user.chatListReceived: + user.log.Debugln("Chat list receive confirmation received in PostLogin") + case <-time.After(time.Duration(user.bridge.Config.Bridge.ChatListWait) * time.Second): + user.log.Warnln("Timed out waiting for chat list to arrive! Unlocking processing of incoming messages.") + return + } + select { + case <-user.syncPortalsDone: + user.log.Debugln("Post-login portal sync complete, unlocking processing of incoming messages.") + case <-time.After(time.Duration(user.bridge.Config.Bridge.PortalSyncWait) * time.Second): + user.log.Warnln("Timed out waiting for chat list to arrive! Unlocking processing of incoming messages.") + } +} + +func (user *User) HandleChatList(chats []skype.Conversation) { + user.log.Infoln("Chat list received") + chatMap := make(map[string]skype.Conversation) + //for _, chat := range user.Conn.Store.Chats { + // chatMap[chat.Jid] = chat + //} + for _, chat := range chats { + cid, _ := chat.Id.(string) + chatMap[cid] = chat + } + select { + case user.chatListReceived <- struct{}{}: + default: + } + go user.syncPortals(chatMap, false) +} + +func (user *User) syncPortals(chatMap map[string]skype.Conversation, createAll bool) { + if chatMap == nil { + chatMap = user.Conn.Store.Chats + } + user.log.Infoln("Reading chat list") + chats := make(ChatList, 0, len(chatMap)) + existingKeys := user.GetInCommunityMap() + portalKeys := make([]database.PortalKeyWithMeta, 0, len(chatMap)) + for _, chat := range chatMap { + t, err := time.Parse(time.RFC3339,chat.LastMessage.ComposeTime) + if err != nil { + t = time.Now() + if chat.Properties.ConversationStatus != "Accepted" && len(chat.ThreadProperties.Lastjoinat) < 1 { + continue + } + // user.log.Warnfln("Non-integer last message time in %s: %s", chat.Id, t) + //continue + } + ts := uint64(t.UnixNano()) + cid, _ := chat.Id.(string) + portal := user.GetPortalByJID(cid) + + chats = append(chats, Chat{ + Portal: portal, + Contact: user.Conn.Store.Chats[cid], + LastMessageTime: ts, + }) + var inCommunity, ok bool + if inCommunity, ok = existingKeys[portal.Key]; !ok || !inCommunity { + inCommunity = user.addPortalToCommunity(portal) + if portal.IsPrivateChat() { + puppet := user.bridge.GetPuppetByJID(portal.Key.JID + skypeExt.NewUserSuffix) + user.addPuppetToCommunity(puppet) + } + } + portalKeys = append(portalKeys, database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity}) + } + user.log.Infoln("Read chat list, updating user-portal mapping") + err := user.SetPortalKeys(portalKeys) + if err != nil { + user.log.Warnln("Failed to update user-portal mapping:", err) + } + sort.Sort(chats) + limit := user.bridge.Config.Bridge.InitialChatSync + if limit < 0 { + limit = len(chats) + } + now := uint64(time.Now().Unix()) + user.log.Infoln("Syncing portals") + for i, chat := range chats { + if chat.LastMessageTime+user.bridge.Config.Bridge.SyncChatMaxAge < now { + break + } + create := (chat.LastMessageTime >= user.LastConnection && user.LastConnection > 0) || i < limit + if len(chat.Portal.MXID) > 0 || create || createAll { + chat.Portal.SyncSkype(user, chat.Contact) + //err := chat.Portal.BackfillHistory(user, chat.LastMessageTime) + if err != nil { + chat.Portal.log.Errorln("Error backfilling history:", err) + } + } + } + user.log.Infoln("Finished syncing portals") + select { + case user.syncPortalsDone <- struct{}{}: + default: + } +} + +func (user *User) HandleContactList(contacts []whatsapp.Contact) { + contactMap := make(map[string]whatsapp.Contact) + for _, contact := range contacts { + contactMap[contact.Jid] = contact + } + // go user.syncPuppets(contactMap) +} + +func (user *User) syncPuppets(contacts map[string]skype.Contact) { + if contacts == nil { + contacts = user.Conn.Store.Contacts + } + if len(contacts) < 1 { + user.log.Infoln("No contacts to sync") + return + } + user.log.Infoln("Syncing puppet info from contacts") + //for jid, contact := range contacts { + username := user.Conn.UserProfile.FirstName + if user.Conn.UserProfile.LastName != "" { + username = user.Conn.UserProfile.FirstName + " " + user.Conn.UserProfile.LastName + } + contacts["8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix] = skype.Contact{ + Profile: skype.UserInfoProfile{ + AvatarUrl: user.Conn.UserProfile.AvatarUrl, + }, + DisplayName: username, + PersonId: user.Conn.UserProfile.Username, + } + for personId, contact := range contacts { + fmt.Println("Syncing puppet info from contacts", personId) + user.log.Infoln("Syncing puppet info from contacts", strings.HasSuffix(personId, skypeExt.NewUserSuffix)) + fmt.Println("Syncing puppet info from contacts", skypeExt.NewUserSuffix) + if strings.HasSuffix(personId, skypeExt.NewUserSuffix) { + fmt.Println("Syncing puppet info from contacts i am coming") + puppet := user.bridge.GetPuppetByJID(personId) + puppet.Sync(user, contact) + } + } + user.log.Infoln("Finished syncing puppet info from contacts") +} + +func (user *User) updateLastConnectionIfNecessary() { + if user.LastConnection+60 < uint64(time.Now().Unix()) { + user.UpdateLastConnection() + } +} + +func (user *User) HandleError(err error) { + if errors.Cause(err) != whatsapp.ErrInvalidWsData { + user.log.Errorfln("WhatsApp error: %v", err) + } + if closed, ok := err.(*whatsapp.ErrConnectionClosed); ok { + if closed.Code == 1000 && user.cleanDisconnection { + user.cleanDisconnection = false + user.log.Infoln("Clean disconnection by server") + return + } + go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection was closed with websocket status code %d", closed.Code)) + } else if failed, ok := err.(*whatsapp.ErrConnectionFailed); ok { + user.ConnectionErrors++ + go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection failed: %v", failed.Err)) + } + // Otherwise unknown error, probably mostly harmless +} + +func (user *User) tryReconnect(msg string) { + //if user.ConnectionErrors > user.bridge.Config.Bridge.MaxConnectionAttempts { + // content := format.RenderMarkdown(fmt.Sprintf("%s. Use the `reconnect` command to reconnect.", msg), true, false) + // _, _ = user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, content) + // return + //} + //if user.bridge.Config.Bridge.ReportConnectionRetry { + // _, _ = user.bridge.Bot.SendNotice(user.GetManagementRoom(), fmt.Sprintf("%s. Reconnecting...", msg)) + // // Don't want the same error to be repeated + // msg = "" + //} + //var tries uint + //var exponentialBackoff bool + //baseDelay := time.Duration(user.bridge.Config.Bridge.ConnectionRetryDelay) + //if baseDelay < 0 { + // exponentialBackoff = true + // baseDelay = -baseDelay + 1 + //} + //delay := baseDelay + //for user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts { + // err := user.Conn.Restore() + // if err == nil { + // user.ConnectionErrors = 0 + // if user.bridge.Config.Bridge.ReportConnectionRetry { + // _, _ = user.bridge.Bot.SendNotice(user.GetManagementRoom(), "Reconnected successfully") + // } + // user.PostLogin() + // return + // } else if err.Error() == "init responded with 400" { + // user.log.Infoln("Got init 400 error when trying to reconnect, resetting connection...") + // sess, err := user.Conn.Disconnect() + // if err != nil { + // user.log.Debugln("Error while disconnecting for connection reset:", err) + // } + // if len(sess.Wid) > 0 { + // user.SetSession(&sess) + // } + // } + // user.log.Errorln("Error while trying to reconnect after disconnection:", err) + // tries++ + // user.ConnectionErrors++ + // if user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts { + // if exponentialBackoff { + // delay = (1 << tries) + baseDelay + // } + // if user.bridge.Config.Bridge.ReportConnectionRetry { + // _, _ = user.bridge.Bot.SendNotice(user.GetManagementRoom(), + // fmt.Sprintf("Reconnection attempt failed: %v. Retrying in %d seconds...", err, delay)) + // } + // time.Sleep(delay * time.Second) + // } + //} + // + //if user.bridge.Config.Bridge.ReportConnectionRetry { + // msg = fmt.Sprintf("%d reconnection attempts failed. Use the `reconnect` command to try to reconnect manually.", tries) + //} else { + // msg = fmt.Sprintf("\u26a0 %sAdditionally, %d reconnection attempts failed. "+ + // "Use the `reconnect` command to try to reconnect.", msg, tries) + //} + // + //content := format.RenderMarkdown(msg, true, false) + //_, _ = user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, content) +} + +func (user *User) ShouldCallSynchronously() bool { + return true +} + +func (user *User) HandleJSONParseError(err error) { + user.log.Errorln("WhatsApp JSON parse error:", err) +} + +func (user *User) PortalKey(jid types.SkypeID) database.PortalKey { + fmt.Println("User PortalKey jid: ", jid) + fmt.Println("User PortalKey user.JID: ", user.JID) + return database.NewPortalKey(jid, user.JID) +} + +func (user *User) GetPortalByJID(jid types.SkypeID) *Portal { + return user.bridge.GetPortalByJID(user.PortalKey(jid)) +} + +func (user *User) handleMessageLoop() { + for msg := range user.messages { + user.syncLock.Lock() + fmt.Println("User handleMessageLoop msg.chat: ", msg.chat) + user.GetPortalByJID(msg.chat).messages <- msg + user.syncLock.Unlock() + } +} + +func (user *User) putMessage(message PortalMessage) { + select { + case user.messages <- message: + default: + user.log.Warnln("Buffer is full, dropping message in", message.chat) + } +} + +func (user *User) HandleTextMessage(message skype.Resource) { + user.log.Debugf("HandleTextMessage: ", message) + user.putMessage(PortalMessage{message.Jid, user, message, uint64(message.Timestamp)}) +} + +func (user *User) HandleImageMessage(message skype.Resource) { + user.log.Debugf("HandleImageMessage: ", message) + user.putMessage(PortalMessage{message.Jid, user, message, uint64(message.Timestamp)}) +} + +func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) { + user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +} + +func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { + user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +} + +func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) { + user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +} + +func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) { + user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +} + +func (user *User) HandleContactMessage(message skype.Resource) { + user.log.Debugf("HandleContactMessage: ", message) + user.putMessage(PortalMessage{message.Jid, user, message, uint64(message.Timestamp)}) +} + +func (user *User) HandleLocationMessage(message skype.Resource) { + user.log.Debugf("HandleLocationMessage: ", message) + user.putMessage(PortalMessage{message.Jid, user, message, uint64(message.Timestamp)}) +} + +func (user *User) HandleMessageRevoke(message skype.Resource) { + user.putMessage(PortalMessage{message.Jid, user, message, uint64(message.Timestamp)}) +} + +type FakeMessage struct { + Text string + ID string + Alert bool +} + +func (user *User) HandleCallInfo(info whatsappExt.CallInfo) { + if info.Data != nil { + return + } + data := FakeMessage{ + ID: info.ID, + } + switch info.Type { + case whatsappExt.CallOffer: + if !user.bridge.Config.Bridge.CallNotices.Start { + return + } + data.Text = "Incoming call" + data.Alert = true + case whatsappExt.CallOfferVideo: + if !user.bridge.Config.Bridge.CallNotices.Start { + return + } + data.Text = "Incoming video call" + data.Alert = true + case whatsappExt.CallTerminate: + if !user.bridge.Config.Bridge.CallNotices.End { + return + } + data.Text = "Call ended" + data.ID += "E" + default: + return + } + portal := user.GetPortalByJID(info.From) + if portal != nil { + portal.messages <- PortalMessage{info.From, user, data, 0} + } +} + +func (user *User) HandleTypingStatus(info skype.Resource) { + sendId := info.SendId + skypeExt.NewUserSuffix + puppet := user.bridge.GetPuppetByJID(sendId) + + switch info.MessageType { + case "Control/ClearTyping": + if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + portal := user.bridge.GetPortalByMXID(puppet.typingIn) + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) + puppet.typingIn = "" + puppet.typingAt = 0 + } + case "Control/Typing": + portal := user.GetPortalByJID(info.Jid) + if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + if puppet.typingIn == portal.MXID { + return + } + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) + } + puppet.typingIn = portal.MXID + puppet.typingAt = time.Now().Unix() + _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 10*1000) + time.Sleep(10 * time.Second) + _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, false, 0) + //_ = puppet.DefaultIntent().SetPresence("online") + } +} + +func (user *User) HandlePresence(info skype.Resource) { + sendId := info.SendId + skypeExt.NewUserSuffix + puppet := user.bridge.GetPuppetByJID(sendId) + + if _,ok := user.contactsPresence[sendId]; ok { + user.contactsPresence[sendId].Availability = info.Availability + user.contactsPresence[sendId].Status = info.Status + } else { + user.contactsPresence[sendId] = &skypeExt.Presence { + Id: sendId, + Availability: info.Availability, + Status: info.Status, + } + } + + switch skype.Presence(info.Availability) { + case skype.PresenceOffline: + _ = puppet.DefaultIntent().SetPresence("offline") + case skype.PresenceOnline: + if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + portal := user.bridge.GetPortalByMXID(puppet.typingIn) + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) + puppet.typingIn = "" + puppet.typingAt = 0 + } else { + _ = puppet.DefaultIntent().SetPresence("online") + } + //case whatsapp.PresenceComposing: + // portal := user.GetPortalByJID(info.Jid) + // if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + // if puppet.typingIn == portal.MXID { + // return + // } + // _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) + // } + // puppet.typingIn = portal.MXID + // puppet.typingAt = time.Now().Unix() + // _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000) + // _ = puppet.DefaultIntent().SetPresence("online") + } +} + +func (user *User) HandlePresenceWA(info whatsappExt.Presence) { + puppet := user.bridge.GetPuppetByJID(info.SenderJID) + switch info.Status { + case whatsapp.PresenceUnavailable: + _ = puppet.DefaultIntent().SetPresence("offline") + case whatsapp.PresenceAvailable: + if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + portal := user.bridge.GetPortalByMXID(puppet.typingIn) + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) + puppet.typingIn = "" + puppet.typingAt = 0 + } + _ = puppet.DefaultIntent().SetPresence("online") + case whatsapp.PresenceComposing: + portal := user.GetPortalByJID(info.JID) + if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { + if puppet.typingIn == portal.MXID { + return + } + _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) + } + puppet.typingIn = portal.MXID + puppet.typingAt = time.Now().Unix() + _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000) + _ = puppet.DefaultIntent().SetPresence("online") + } +} + +func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) { + if (info.Command == whatsappExt.MsgInfoCommandAck || info.Command == whatsappExt.MsgInfoCommandAcks) && info.Acknowledgement == whatsappExt.AckMessageRead { + portal := user.GetPortalByJID(info.ToJID) + if len(portal.MXID) == 0 { + return + } + + go func() { + intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal) + for _, id := range info.IDs { + msg := user.bridge.DB.Message.GetByJID(portal.Key, id) + if msg == nil { + continue + } + + err := intent.MarkRead(portal.MXID, msg.MXID) + if err != nil { + user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err) + } + } + }() + } +} + +func (user *User) HandleCommand(cmd skypeExt.Command) { + switch cmd.Type { + case skypeExt.CommandPicture: + if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) { + puppet := user.bridge.GetPuppetByJID(cmd.JID) + go puppet.UpdateAvatar(user, cmd.ProfilePicInfo) + } else { + portal := user.GetPortalByJID(cmd.JID) + go portal.UpdateAvatar(user, cmd.ProfilePicInfo) + } + case skypeExt.CommandDisconnect: + var msg string + if cmd.Kind == "replaced" { + msg = "\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" + + "Use the `reconnect` command to disconnect the other client and resume bridging." + } else { + user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw)) + msg = fmt.Sprintf("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+ + "Use the `reconnect` command to reconnect.", cmd.Kind) + } + user.cleanDisconnection = true + go user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, format.RenderMarkdown(msg, true, false)) + } +} + +func (user *User) HandleChatUpdate(cmd skype.Resource) { + fmt.Println() + fmt.Println("HandleChatUpdate :") + fmt.Println() + //if cmd.Command != skypeExt.ChatUpdateCommandAction { + // return + //} + + portal := user.GetPortalByJID(cmd.Jid) + //if len(portal.MXID) == 0 { + // if cmd.Data.Action == skypeExt.ChatActionCreate { + // go portal.membershipCreate(user, cmd) + // } + // return + //} + + fmt.Println("portl:", portal) + fmt.Println("cmd.MessageType:", cmd.MessageType) + messageType := skypeExt.ChatActionType(cmd.MessageType) + + switch messageType { + case skypeExt.ChatTopicUpdate: + topicContent := skype.ChatTopicContent{} + //把xml数据解析成bs对象 + xml.Unmarshal([]byte(cmd.Content), &topicContent) + cmd.SendId = topicContent.Initiator + skypeExt.NewUserSuffix + go portal.UpdateName(cmd.ThreadTopic, cmd.SendId) + case skypeExt.ChatPictureUpdate: + topicContent := skype.ChatPictureContent{} + //把xml数据解析成bs对象 + xml.Unmarshal([]byte(cmd.Content), &topicContent) + cmd.SendId = topicContent.Initiator + skypeExt.NewUserSuffix + url := strings.TrimPrefix(topicContent.Value, "URL@") + if strings.Index(url, "/views/") > 0 { + url = strings.Replace(url, "avatar_fullsize", "swx_avatar", 1) + } else { + url = url + "/views/swx_avatar" + } + fmt.Println() + fmt.Println("HandleChatUpdateL picture:", url ) + fmt.Println() + avatar := &skypeExt.ProfilePicInfo{ + URL: url, + Tag: url, + Status: 0, + } + go portal.UpdateAvatar(user, avatar) + case skypeExt.ChatMemberAdd: + fmt.Println("portal.MXID") + fmt.Println(portal.MXID) + fmt.Println(user) + if len(portal.MXID) == 0 { + err := portal.CreateMatrixRoom(user) + if err != nil { + fmt.Println("create room failed") + } + } + go portal.membershipAdd(user, cmd.Jid) + case skypeExt.ChatMemberDelete: + go portal.membershipRemove(cmd.Content) + //case skypeExt.ChatActionAddTopic: + // go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID) + //case skypeExt.ChatActionRemoveTopic: + // go portal.UpdateTopic("", cmd.Data.SenderJID) + //case skypeExt.ChatActionPromote: + // go portal.ChangeAdminStatus(cmd.Data.PermissionChange.JIDs, true) + //case skypeExt.ChatActionDemote: + // go portal.ChangeAdminStatus(cmd.Data.PermissionChange.JIDs, false) + //case skypeExt.ChatActionAnnounce: + // go portal.RestrictMessageSending(cmd.Data.Announce) + //case skypeExt.ChatActionRestrict: + // go portal.RestrictMetadataChanges(cmd.Data.Restrict) + //case skypeExt.ChatActionAdd: + // go portal.membershipAdd(user, cmd.Jid) + //case skypeExt.ChatActionRemove: + // go portal.membershipRemove(cmd.Data.MemberAction.JIDs, cmd.Data.Action) + //case skypeExt.ChatActionIntroduce: + // go portal.membershipAdd(user, cmd.JID) + } +} + + +//func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) { +// if cmd.Command != whatsappExt.ChatUpdateCommandAction { +// return +// } +// +// portal := user.GetPortalByJID(cmd.JID) +// if len(portal.MXID) == 0 { +// if cmd.Data.Action == whatsappExt.ChatActionCreate { +// go portal.membershipCreate(user, cmd) +// } +// return +// } +// +// switch cmd.Data.Action { +// case whatsappExt.ChatActionNameChange: +// go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID) +// case whatsappExt.ChatActionAddTopic: +// go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID) +// case whatsappExt.ChatActionRemoveTopic: +// go portal.UpdateTopic("", cmd.Data.SenderJID) +// case whatsappExt.ChatActionPromote: +// go portal.ChangeAdminStatus(cmd.Data.PermissionChange.JIDs, true) +// case whatsappExt.ChatActionDemote: +// go portal.ChangeAdminStatus(cmd.Data.PermissionChange.JIDs, false) +// case whatsappExt.ChatActionAnnounce: +// go portal.RestrictMessageSending(cmd.Data.Announce) +// case whatsappExt.ChatActionRestrict: +// go portal.RestrictMetadataChanges(cmd.Data.Restrict) +// case whatsappExt.ChatActionAdd: +// go portal.membershipAdd(user, cmd.JID) +// case whatsappExt.ChatActionRemove: +// go portal.membershipRemove(cmd.Data.MemberAction.JIDs, cmd.Data.Action) +// case whatsappExt.ChatActionIntroduce: +// go portal.membershipAdd(user, cmd.JID) +// } +//} + +func (user *User) HandleJsonMessage(message string) { + var msg json.RawMessage + err := json.Unmarshal([]byte(message), &msg) + if err != nil { + return + } + user.log.Debugln("JSON message:", message) + user.updateLastConnectionIfNecessary() +} + +func (user *User) HandleRawMessage(message *waProto.WebMessageInfo) { + user.updateLastConnectionIfNecessary() +} + +func (user *User) NeedsRelaybot(portal *Portal) bool { + return false + //return !user.HasSession() || !user.IsInPortal(portal.Key) +} diff --git a/webp.go b/webp.go new file mode 100644 index 0000000..79b5be2 --- /dev/null +++ b/webp.go @@ -0,0 +1,14 @@ +// +build cgo + +package main + +import ( + "image" + "io" + + "github.com/chai2010/webp" +) + +func decodeWebp(r io.Reader) (image.Image, error) { + return webp.Decode(r) +} diff --git a/whatsapp-ext/call.go b/whatsapp-ext/call.go new file mode 100644 index 0000000..29575c3 --- /dev/null +++ b/whatsapp-ext/call.go @@ -0,0 +1,72 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type CallInfoType string + +const ( + CallOffer CallInfoType = "offer" + CallOfferVideo CallInfoType = "offer_video" + CallTransport CallInfoType = "transport" + CallRelayLatency CallInfoType = "relaylatency" + CallTerminate CallInfoType = "terminate" +) + +type CallInfo struct { + ID string `json:"id"` + Type CallInfoType `json:"type"` + From string `json:"from"` + + Platform string `json:"platform"` + Version []int `json:"version"` + + Data [][]interface{} `json:"data"` +} + +type CallInfoHandler interface { + whatsapp.Handler + HandleCallInfo(CallInfo) +} + +func (ext *ExtendedConn) handleMessageCall(message []byte) { + var event CallInfo + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.From = strings.Replace(event.From, OldUserSuffix, NewUserSuffix, 1) + for _, handler := range ext.handlers { + callInfoHandler, ok := handler.(CallInfoHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(callInfoHandler) { + callInfoHandler.HandleCallInfo(event) + } else { + go callInfoHandler.HandleCallInfo(event) + } + } +} diff --git a/whatsapp-ext/chat.go b/whatsapp-ext/chat.go new file mode 100644 index 0000000..8de0a99 --- /dev/null +++ b/whatsapp-ext/chat.go @@ -0,0 +1,174 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type ChatUpdateCommand string + +const ( + ChatUpdateCommandAction ChatUpdateCommand = "action" +) + +type ChatUpdate struct { + JID string `json:"id"` + Command ChatUpdateCommand `json:"cmd"` + Data ChatUpdateData `json:"data"` +} + +type ChatActionType string + +const ( + ChatActionNameChange ChatActionType = "subject" + ChatActionAddTopic ChatActionType = "desc_add" + ChatActionRemoveTopic ChatActionType = "desc_remove" + ChatActionRestrict ChatActionType = "restrict" + ChatActionAnnounce ChatActionType = "announce" + ChatActionPromote ChatActionType = "promote" + ChatActionDemote ChatActionType = "demote" + ChatActionRemove ChatActionType = "remove" + ChatActionAdd ChatActionType = "add" + ChatActionIntroduce ChatActionType = "introduce" + ChatActionCreate ChatActionType = "create" +) + +type ChatUpdateData struct { + Action ChatActionType + SenderJID string + + NameChange struct { + Name string `json:"subject"` + SetAt int64 `json:"s_t"` + SetBy string `json:"s_o"` + } + + AddTopic struct { + Topic string `json:"desc"` + ID string `json:"descId"` + SetAt int64 `json:"descTime"` + } + + RemoveTopic struct { + ID string `json:"descId"` + } + + Restrict bool + + Announce bool + + PermissionChange struct { + JIDs []string `json:"participants"` + } + + MemberAction struct { + JIDs []string `json:"participants"` + } + + Create struct { + Creation int64 `json:"creation"` + Name string `json:"subject"` + SetAt int64 `json:"s_t"` + SetBy string `json:"s_o"` + Admins []string `json:"admins"` + SuperAdmins []string `json:"superadmins"` + Regulars []string `json:"regulars"` + } +} + +func (cud *ChatUpdateData) UnmarshalJSON(data []byte) error { + var arr []json.RawMessage + err := json.Unmarshal(data, &arr) + if err != nil { + return err + } else if len(arr) < 3 { + return nil + } + + err = json.Unmarshal(arr[0], &cud.Action) + if err != nil { + return err + } + + err = json.Unmarshal(arr[1], &cud.SenderJID) + if err != nil { + return err + } + cud.SenderJID = strings.Replace(cud.SenderJID, OldUserSuffix, NewUserSuffix, 1) + + var unmarshalTo interface{} + switch cud.Action { + case ChatActionNameChange: + unmarshalTo = &cud.NameChange + case ChatActionAddTopic: + unmarshalTo = &cud.AddTopic + case ChatActionRemoveTopic: + unmarshalTo = &cud.RemoveTopic + case ChatActionRestrict: + unmarshalTo = &cud.Restrict + case ChatActionAnnounce: + unmarshalTo = &cud.Announce + case ChatActionPromote, ChatActionDemote: + unmarshalTo = &cud.PermissionChange + case ChatActionAdd, ChatActionRemove: + unmarshalTo = &cud.MemberAction + case ChatActionCreate: + unmarshalTo = &cud.Create + default: + return nil + } + err = json.Unmarshal(arr[2], unmarshalTo) + if err != nil { + return err + } + cud.NameChange.SetBy = strings.Replace(cud.NameChange.SetBy, OldUserSuffix, NewUserSuffix, 1) + for index, jid := range cud.PermissionChange.JIDs { + cud.PermissionChange.JIDs[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1) + } + return nil +} + +type ChatUpdateHandler interface { + whatsapp.Handler + HandleChatUpdate(ChatUpdate) +} + +func (ext *ExtendedConn) handleMessageChatUpdate(message []byte) { + var event ChatUpdate + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) + for _, handler := range ext.handlers { + chatUpdateHandler, ok := handler.(ChatUpdateHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(chatUpdateHandler) { + chatUpdateHandler.HandleChatUpdate(event) + } else { + go chatUpdateHandler.HandleChatUpdate(event) + } + } +} diff --git a/whatsapp-ext/cmd.go b/whatsapp-ext/cmd.go new file mode 100644 index 0000000..0954df3 --- /dev/null +++ b/whatsapp-ext/cmd.go @@ -0,0 +1,69 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type CommandType string + +const ( + CommandPicture CommandType = "picture" + CommandDisconnect CommandType = "disconnect" +) + +type Command struct { + Type CommandType `json:"type"` + JID string `json:"jid"` + + *ProfilePicInfo + Kind string `json:"kind"` + + Raw json.RawMessage `json:"-"` +} + +type CommandHandler interface { + whatsapp.Handler + HandleCommand(Command) +} + +func (ext *ExtendedConn) handleMessageCommand(message []byte) { + var event Command + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.Raw = message + event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) + for _, handler := range ext.handlers { + commandHandler, ok := handler.(CommandHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(commandHandler) { + commandHandler.HandleCommand(event) + } else { + go commandHandler.HandleCommand(event) + } + } +} diff --git a/whatsapp-ext/conn.go b/whatsapp-ext/conn.go new file mode 100644 index 0000000..8ce4345 --- /dev/null +++ b/whatsapp-ext/conn.go @@ -0,0 +1,65 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + + "github.com/Rhymen/go-whatsapp" +) + +type ConnInfo struct { + ProtocolVersion []int `json:"protoVersion"` + BinaryVersion int `json:"binVersion"` + Phone struct { + WhatsAppVersion string `json:"wa_version"` + MCC string `json:"mcc"` + MNC string `json:"mnc"` + OSVersion string `json:"os_version"` + DeviceManufacturer string `json:"device_manufacturer"` + DeviceModel string `json:"device_model"` + OSBuildNumber string `json:"os_build_number"` + } `json:"phone"` + Features map[string]interface{} `json:"features"` + PushName string `json:"pushname"` +} + +type ConnInfoHandler interface { + whatsapp.Handler + HandleConnInfo(ConnInfo) +} + +func (ext *ExtendedConn) handleMessageConn(message []byte) { + var event ConnInfo + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + for _, handler := range ext.handlers { + connInfoHandler, ok := handler.(ConnInfoHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(connInfoHandler) { + connInfoHandler.HandleConnInfo(event) + } else { + go connInfoHandler.HandleConnInfo(event) + } + } +} diff --git a/whatsapp-ext/jsonmessage.go b/whatsapp-ext/jsonmessage.go new file mode 100644 index 0000000..e0ac474 --- /dev/null +++ b/whatsapp-ext/jsonmessage.go @@ -0,0 +1,105 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + + "github.com/Rhymen/go-whatsapp" +) + +type JSONMessage []json.RawMessage + +type JSONMessageType string + +const ( + MessageMsgInfo JSONMessageType = "MsgInfo" + MessageMsg JSONMessageType = "Msg" + MessagePresence JSONMessageType = "Presence" + MessageStream JSONMessageType = "Stream" + MessageConn JSONMessageType = "Conn" + MessageProps JSONMessageType = "Props" + MessageCmd JSONMessageType = "Cmd" + MessageChat JSONMessageType = "Chat" + MessageCall JSONMessageType = "Call" +) + +func (ext *ExtendedConn) HandleError(error) {} + +type UnhandledJSONMessageHandler interface { + whatsapp.Handler + HandleUnhandledJSONMessage(string) +} + +type JSONParseErrorHandler interface { + whatsapp.Handler + HandleJSONParseError(error) +} + +func (ext *ExtendedConn) jsonParseError(err error) { + for _, handler := range ext.handlers { + errorHandler, ok := handler.(JSONParseErrorHandler) + if !ok { + continue + } + errorHandler.HandleJSONParseError(err) + } +} + +func (ext *ExtendedConn) HandleJsonMessage(message string) { + msg := JSONMessage{} + err := json.Unmarshal([]byte(message), &msg) + if err != nil || len(msg) < 2 { + ext.jsonParseError(err) + return + } + + var msgType JSONMessageType + json.Unmarshal(msg[0], &msgType) + + switch msgType { + case MessagePresence: + ext.handleMessagePresence(msg[1]) + case MessageStream: + ext.handleMessageStream(msg[1:]) + case MessageConn: + ext.handleMessageConn(msg[1]) + case MessageProps: + ext.handleMessageProps(msg[1]) + case MessageMsgInfo, MessageMsg: + ext.handleMessageMsgInfo(msgType, msg[1]) + case MessageCmd: + ext.handleMessageCommand(msg[1]) + case MessageChat: + ext.handleMessageChatUpdate(msg[1]) + case MessageCall: + ext.handleMessageCall(msg[1]) + default: + for _, handler := range ext.handlers { + ujmHandler, ok := handler.(UnhandledJSONMessageHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(ujmHandler) { + ujmHandler.HandleUnhandledJSONMessage(message) + } else { + go ujmHandler.HandleUnhandledJSONMessage(message) + } + } + } +} diff --git a/whatsapp-ext/msginfo.go b/whatsapp-ext/msginfo.go new file mode 100644 index 0000000..702c322 --- /dev/null +++ b/whatsapp-ext/msginfo.go @@ -0,0 +1,95 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type MsgInfoCommand string + +const ( + MsgInfoCommandAck MsgInfoCommand = "ack" + MsgInfoCommandAcks MsgInfoCommand = "acks" +) + +type Acknowledgement int + +const ( + AckMessageSent Acknowledgement = 1 + AckMessageDelivered Acknowledgement = 2 + AckMessageRead Acknowledgement = 3 +) + +type JSONStringOrArray []string + +func (jsoa *JSONStringOrArray) UnmarshalJSON(data []byte) error { + var str string + if json.Unmarshal(data, &str) == nil { + *jsoa = []string{str} + return nil + } + var strs []string + json.Unmarshal(data, &strs) + *jsoa = strs + return nil +} + +type MsgInfo struct { + Command MsgInfoCommand `json:"cmd"` + IDs JSONStringOrArray `json:"id"` + Acknowledgement Acknowledgement `json:"ack"` + MessageFromJID string `json:"from"` + SenderJID string `json:"participant"` + ToJID string `json:"to"` + Timestamp int64 `json:"t"` +} + +type MsgInfoHandler interface { + whatsapp.Handler + HandleMsgInfo(MsgInfo) +} + +func (ext *ExtendedConn) handleMessageMsgInfo(msgType JSONMessageType, message []byte) { + var event MsgInfo + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.MessageFromJID = strings.Replace(event.MessageFromJID, OldUserSuffix, NewUserSuffix, 1) + event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1) + event.ToJID = strings.Replace(event.ToJID, OldUserSuffix, NewUserSuffix, 1) + if msgType == MessageMsg { + event.SenderJID = event.ToJID + } + for _, handler := range ext.handlers { + msgInfoHandler, ok := handler.(MsgInfoHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(msgInfoHandler) { + msgInfoHandler.HandleMsgInfo(event) + } else { + go msgInfoHandler.HandleMsgInfo(event) + } + } +} diff --git a/whatsapp-ext/presence.go b/whatsapp-ext/presence.go new file mode 100644 index 0000000..01365d2 --- /dev/null +++ b/whatsapp-ext/presence.go @@ -0,0 +1,67 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +type Presence struct { + JID string `json:"id"` + SenderJID string `json:"participant"` + Status whatsapp.Presence `json:"type"` + Timestamp int64 `json:"t"` + Deny bool `json:"deny"` +} + +type PresenceHandler interface { + whatsapp.Handler + HandlePresence(Presence) +} + +func (ext *ExtendedConn) handleMessagePresence(message []byte) { + var event Presence + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) + if len(event.SenderJID) == 0 { + if strings.Index(event.JID,"@g.us") > -1 { + return + } + event.SenderJID = event.JID + } else { + event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1) + } + for _, handler := range ext.handlers { + presenceHandler, ok := handler.(PresenceHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(presenceHandler) { + presenceHandler.HandlePresence(event) + } else { + go presenceHandler.HandlePresence(event) + } + } +} diff --git a/whatsapp-ext/props.go b/whatsapp-ext/props.go new file mode 100644 index 0000000..4245683 --- /dev/null +++ b/whatsapp-ext/props.go @@ -0,0 +1,73 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + + "github.com/Rhymen/go-whatsapp" +) + +type ProtocolProps struct { + WebPresence bool `json:"webPresence"` + NotificationQuery bool `json:"notificationQuery"` + FacebookCrashLog bool `json:"fbCrashlog"` + Bucket string `json:"bucket"` + GIFSearch string `json:"gifSearch"` + Spam bool `json:"SPAM"` + SetBlock bool `json:"SET_BLOCK"` + MessageInfo bool `json:"MESSAGE_INFO"` + MaxFileSize int `json:"maxFileSize"` + Media int `json:"media"` + GroupNameLength int `json:"maxSubject"` + GroupDescriptionLength int `json:"groupDescLength"` + MaxParticipants int `json:"maxParticipants"` + VideoMaxEdge int `json:"videoMaxEdge"` + ImageMaxEdge int `json:"imageMaxEdge"` + ImageMaxKilobytes int `json:"imageMaxKBytes"` + Edit int `json:"edit"` + FwdUIStartTimestamp int `json:"fwdUiStartTs"` + GroupsV3 int `json:"groupsV3"` + RestrictGroups int `json:"restrictGroups"` + AnnounceGroups int `json:"announceGroups"` +} + +type ProtocolPropsHandler interface { + whatsapp.Handler + HandleProtocolProps(ProtocolProps) +} + +func (ext *ExtendedConn) handleMessageProps(message []byte) { + var event ProtocolProps + err := json.Unmarshal(message, &event) + if err != nil { + ext.jsonParseError(err) + return + } + for _, handler := range ext.handlers { + protocolPropsHandler, ok := handler.(ProtocolPropsHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(protocolPropsHandler) { + protocolPropsHandler.HandleProtocolProps(event) + } else { + go protocolPropsHandler.HandleProtocolProps(event) + } + } +} diff --git a/whatsapp-ext/protomessage.go b/whatsapp-ext/protomessage.go new file mode 100644 index 0000000..ca00f05 --- /dev/null +++ b/whatsapp-ext/protomessage.go @@ -0,0 +1,59 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "github.com/Rhymen/go-whatsapp" + "github.com/Rhymen/go-whatsapp/binary/proto" +) + +type MessageRevokeHandler interface { + whatsapp.Handler + HandleMessageRevoke(key MessageRevocation) +} + +type MessageRevocation struct { + Id string + RemoteJid string + FromMe bool + Participant string +} + +func (ext *ExtendedConn) HandleRawMessage(message *proto.WebMessageInfo) { + protoMsg := message.GetMessage().GetProtocolMessage() + if protoMsg != nil && protoMsg.GetType() == proto.ProtocolMessage_REVOKE { + key := protoMsg.GetKey() + deletedMessage := MessageRevocation{ + Id: key.GetId(), + RemoteJid: key.GetRemoteJid(), + FromMe: key.GetFromMe(), + Participant: key.GetParticipant(), + } + for _, handler := range ext.handlers { + mrHandler, ok := handler.(MessageRevokeHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(mrHandler) { + mrHandler.HandleMessageRevoke(deletedMessage) + } else { + go mrHandler.HandleMessageRevoke(deletedMessage) + } + } + } +} diff --git a/whatsapp-ext/stream.go b/whatsapp-ext/stream.go new file mode 100644 index 0000000..bd82672 --- /dev/null +++ b/whatsapp-ext/stream.go @@ -0,0 +1,68 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + + "github.com/Rhymen/go-whatsapp" +) + +type StreamType string + +const ( + StreamUpdate = "update" + StreamSleep = "asleep" +) + +type StreamEvent struct { + Type StreamType + Boolean bool + Version string +} + +type StreamEventHandler interface { + whatsapp.Handler + HandleStreamEvent(StreamEvent) +} + +func (ext *ExtendedConn) handleMessageStream(message []json.RawMessage) { + var event StreamEvent + err := json.Unmarshal(message[0], &event.Type) + if err != nil { + ext.jsonParseError(err) + return + } + + if event.Type == StreamUpdate && len(message) > 4 { + json.Unmarshal(message[1], event.Boolean) + json.Unmarshal(message[2], event.Version) + } + + for _, handler := range ext.handlers { + streamHandler, ok := handler.(StreamEventHandler) + if !ok { + continue + } + + if ext.shouldCallSynchronously(streamHandler) { + streamHandler.HandleStreamEvent(event) + } else { + go streamHandler.HandleStreamEvent(event) + } + } +} diff --git a/whatsapp-ext/whatsapp.go b/whatsapp-ext/whatsapp.go new file mode 100644 index 0000000..5c356c3 --- /dev/null +++ b/whatsapp-ext/whatsapp.go @@ -0,0 +1,206 @@ +// matrix-skype - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package whatsappExt + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "strings" + + "github.com/Rhymen/go-whatsapp" +) + +const ( + OldUserSuffix = "@c.us" + //NewUserSuffix = "@s.whatsapp.net" + NewUserSuffix = "@s.skype.net" +) + +type ExtendedConn struct { + *whatsapp.Conn + + handlers []whatsapp.Handler +} + +func ExtendConn(conn *whatsapp.Conn) *ExtendedConn { + ext := &ExtendedConn{ + Conn: conn, + } + ext.Conn.AddHandler(ext) + return ext +} + +func (ext *ExtendedConn) AddHandler(handler whatsapp.Handler) { + ext.Conn.AddHandler(handler) + ext.handlers = append(ext.handlers, handler) +} + + +func (ext *ExtendedConn) RemoveHandler(handler whatsapp.Handler) bool { + ext.Conn.RemoveHandler(handler) + for i, v := range ext.handlers { + if v == handler { + ext.handlers = append(ext.handlers[:i], ext.handlers[i+1:]...) + return true + } + } + return false +} + +func (ext *ExtendedConn) RemoveHandlers() { + ext.Conn.RemoveHandlers() + ext.handlers = make([]whatsapp.Handler, 0) +} + +func (ext *ExtendedConn) shouldCallSynchronously(handler whatsapp.Handler) bool { + sh, ok := handler.(whatsapp.SyncHandler) + return ok && sh.ShouldCallSynchronously() +} + +func (ext *ExtendedConn) ShouldCallSynchronously() bool { + return true +} + +type GroupInfo struct { + JID string `json:"jid"` + OwnerJID string `json:"owner"` + + Name string `json:"subject"` + NameSetTime int64 `json:"subjectTime"` + NameSetBy string `json:"subjectOwner"` + + Topic string `json:"desc"` + TopicID string `json:"descId"` + TopicSetAt int64 `json:"descTime"` + TopicSetBy string `json:"descOwner"` + + GroupCreated int64 `json:"creation"` + + Status int16 `json:"status"` + + Participants []struct { + JID string `json:"id"` + IsAdmin bool `json:"isAdmin"` + IsSuperAdmin bool `json:"isSuperAdmin"` + } `json:"participants"` +} + +func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) { + data, err := ext.Conn.GetGroupMetaData(jid) + if err != nil { + return nil, fmt.Errorf("failed to get group metadata: %v", err) + } + content := <-data + + info := &GroupInfo{} + err = json.Unmarshal([]byte(content), info) + if err != nil { + return info, fmt.Errorf("failed to unmarshal group metadata: %v", err) + } + + for index, participant := range info.Participants { + info.Participants[index].JID = strings.Replace(participant.JID, OldUserSuffix, NewUserSuffix, 1) + } + info.NameSetBy = strings.Replace(info.NameSetBy, OldUserSuffix, NewUserSuffix, 1) + info.TopicSetBy = strings.Replace(info.TopicSetBy, OldUserSuffix, NewUserSuffix, 1) + + return info, nil +} + +type ProfilePicInfo struct { + URL string `json:"eurl"` + Tag string `json:"tag"` + + Status int16 `json:"status"` +} + +func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) { + resp, err := http.Get(ppi.URL) + if err != nil { + return nil, err + } + return resp.Body, nil +} + +func (ppi *ProfilePicInfo) DownloadBytes() ([]byte, error) { + body, err := ppi.Download() + if err != nil { + return nil, err + } + defer body.Close() + data, err := ioutil.ReadAll(body) + return data, err +} + +func (ext *ExtendedConn) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) { + data, err := ext.Conn.GetProfilePicThumb(jid) + if err != nil { + return nil, fmt.Errorf("failed to get avatar: %v", err) + } + content := <-data + info := &ProfilePicInfo{} + err = json.Unmarshal([]byte(content), info) + if err != nil { + return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) + } + return info, nil +} + +func (ext *ExtendedConn) HandleGroupInvite(groupJid string, numbers[]string) (err error) { + var parts []string + parts = append(parts, numbers...) + _, err = ext.Conn.AddMember(groupJid, parts) + if err != nil { + fmt.Printf("%s Handle Invite err", err) + } + return +} + +func (ext *ExtendedConn) HandleGroupJoin(code string) (jid string, err error) { + return ext.Conn.GroupAcceptInviteCode(code) +} + +func (ext *ExtendedConn) HandleGroupKick(groupJid string, numbers[]string) (err error) { + var parts []string + parts = append(parts, numbers...) + _, err = ext.Conn.RemoveMember(groupJid, parts) + if err != nil { + fmt.Printf("%s Handle kick err", err) + } + return +} + +func (ext *ExtendedConn) HandleGroupCreate(subject string, numbers[]string) (err error) { + var parts []string + parts = append(parts, numbers...) + _, err = ext.Conn.CreateGroup(subject, parts) + if err != nil { + fmt.Printf("%s HandleGroupCreate err", err) + } + return +} + +func (ext *ExtendedConn) HandleGroupLeave(groupJid string) (err error) { + _, err = ext.Conn.LeaveGroup(groupJid) + if err != nil { + fmt.Printf("%s HandleGroupLeave err", err) + } + return +} From 02e54ff1e6a29b8a05a58fe3af5a11aa284088aa Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 8 Sep 2020 13:02:37 +0800 Subject: [PATCH 004/109] Update README.md --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7708e80..e746aee 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# matrix-skype \ No newline at end of file +# matrix-skype + +This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) + +## functions are available +`The following functions are available in both directions without special instructions)` + +* create private conversation +* create group conversation +* private conversation +* group conversation +* kick/invite(group) +* generate invitation link(group) +* quote message(Circular references may have some bugs) +* mention someone(message) +* media message +* picture message +* group avatar/name change +* user name/avatar change +* Typing status + + +## Below is the readme of mautrix-whatsapp +> # mautrix-whatsapp +> A Matrix-WhatsApp puppeting bridge based on the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp) +> implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project. + +> ### [Wiki](https://github.com/tulir/mautrix-whatsapp/wiki) + +> ### [Features & Roadmap](https://github.com/tulir/mautrix-whatsapp/blob/master/ROADMAP.md) From 316078bd8de1cb467da9d6cbc71d21404f0a1a15 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 8 Sep 2020 13:38:53 +0800 Subject: [PATCH 005/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e746aee..10ac222 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir * create group conversation * private conversation * group conversation -* kick/invite(group) +* kick/invite/leave/join(group) * generate invitation link(group) * quote message(Circular references may have some bugs) * mention someone(message) From 6a7937f41ab69e5530f4dc317b5fedb13c42c6a3 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 9 Sep 2020 17:05:56 +0800 Subject: [PATCH 006/109] Adjust the format of contact messages --- portal.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/portal.go b/portal.go index 8b9b05f..6c3337f 100644 --- a/portal.go +++ b/portal.go @@ -540,7 +540,6 @@ func (portal *Portal) UpdateMetadata(user *User) bool { if key == user.JID { continue } - fmt.Println("CreateMatrixRoom3.1: ", key) if contact, ok := user.Conn.Store.Contacts[key]; ok { portalName += contact.DisplayName } @@ -1468,10 +1467,9 @@ func (portal *Portal) HandleContactMessageSkype(source *User, message skype.Reso content := &event.MessageEventContent{ Body: fmt.Sprintf("%s\n%s", contactMessage.C.F, contactMessage.C.S), - MsgType: event.MsgText, + MsgType: "m.contact",//event.MsgText, } - //portal.SetReply(content, message.ContextInfo) portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) From a3cd1cb96ec899d47e4ce0e4f1b107d11ac28ae3 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Fri, 11 Sep 2020 10:20:06 +0800 Subject: [PATCH 007/109] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 10ac222..fd554c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # matrix-skype -This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) +matrix-skype is a library for bridging matrix and skype, about matrix, please refer to [matrix.org](http://matrix.org/). ## functions are available `The following functions are available in both directions without special instructions)` @@ -19,8 +19,10 @@ This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir * user name/avatar change * Typing status +The skype api lib of matrix-skype is [go-skypeapi]https://github.com/kelaresg/go-skypeapi. + +This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) -## Below is the readme of mautrix-whatsapp > # mautrix-whatsapp > A Matrix-WhatsApp puppeting bridge based on the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp) > implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project. From 12d9033e5843b55df1f32ba5b0826290c11b30a7 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Fri, 11 Sep 2020 10:20:39 +0800 Subject: [PATCH 008/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd554c7..f89237e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * user name/avatar change * Typing status -The skype api lib of matrix-skype is [go-skypeapi]https://github.com/kelaresg/go-skypeapi. +The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 1251ac2204e9e0469cbbc13745e04d22c7846896 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Fri, 11 Sep 2020 11:40:52 +0800 Subject: [PATCH 009/109] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index f89237e..b255169 100644 --- a/README.md +++ b/README.md @@ -28,5 +28,3 @@ This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir > implementation of the [sigalor/whatsapp-web-reveng](https://github.com/sigalor/whatsapp-web-reveng) project. > ### [Wiki](https://github.com/tulir/mautrix-whatsapp/wiki) - -> ### [Features & Roadmap](https://github.com/tulir/mautrix-whatsapp/blob/master/ROADMAP.md) From ab5d8526292109420dd8f29f1a0d999c525476ad Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 21 Sep 2020 22:48:06 +0800 Subject: [PATCH 010/109] kick/invite (in portal room) --- commands.go | 2 +- matrix.go | 135 +++++++++++++++++++++++++++++++++++++++++++-- portal.go | 119 ++++++++++++++++++++++++++++++++++----- puppet.go | 12 +++- skype-ext/skype.go | 7 ++- 5 files changed, 249 insertions(+), 26 deletions(-) diff --git a/commands.go b/commands.go index 92fcc0a..3fb85a5 100644 --- a/commands.go +++ b/commands.go @@ -900,7 +900,7 @@ func (handler *CommandHandler) CommandInvite(ce *CommandEvent) { userNumbers := strings.Split(ce.Args[1], ",") - if strings.HasSuffix(conversationId, whatsappExt.NewUserSuffix) { + if strings.HasSuffix(conversationId, skypeExt.NewUserSuffix) { ce.Reply("**Usage:** `invite ,...`") return } diff --git a/matrix.go b/matrix.go index 6dbbe77..bc5795e 100644 --- a/matrix.go +++ b/matrix.go @@ -3,6 +3,8 @@ package main import ( "fmt" skype "github.com/kelaresg/go-skypeapi" + "github.com/kelaresg/matrix-skype/database" + "maunium.net/go/mautrix" "strconv" "strings" @@ -52,6 +54,28 @@ func (mx *MatrixHandler) HandleEncryption(evt *event.Event) { } } +func (mx *MatrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { + resp, err := intent.JoinRoomByID(evt.RoomID) + if err != nil { + mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err) + return nil + } + + members, err := intent.JoinedMembers(resp.RoomID) + if err != nil { + mx.log.Debugfln("Failed to get members in room %s after accepting invite from %s as %s: %v", resp.RoomID, evt.Sender, intent.UserID, err) + _, _ = intent.LeaveRoom(resp.RoomID) + return nil + } + + if len(members.Joined) < 2 { + mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender, "as", intent.UserID) + _, _ = intent.LeaveRoom(resp.RoomID) + return nil + } + return members +} + func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { intent := mx.as.BotIntent() @@ -114,6 +138,102 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { } } +func (mx *MatrixHandler) handlePrivatePortal(roomID id.RoomID, inviter *User, puppet *Puppet, key database.PortalKey) { + portal := mx.bridge.GetPortalByJID(key) + + if len(portal.MXID) == 0 { + mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) + return + } + + err := portal.MainIntent().EnsureInvited(portal.MXID, inviter.MXID) + if err != nil { + mx.log.Warnfln("Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.JID, err) + mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) + return + } + intent := puppet.DefaultIntent() + _, _ = intent.SendNotice(roomID, "You already have a private chat portal with me at %s") + mx.log.Debugln("Leaving private chat room", roomID, "as", puppet.MXID, "after accepting invite from", inviter.MXID, "as we already have chat with the user") + _, _ = intent.LeaveRoom(roomID) +} + + +func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { + portal.MXID = roomID + portal.Topic = "WhatsApp private chat" + _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) + if portal.bridge.Config.Bridge.PrivateChatPortalMeta { + portal.Name = puppet.Displayname + portal.AvatarURL = puppet.AvatarURL + portal.Avatar = puppet.Avatar + _, _ = portal.MainIntent().SetRoomName(portal.MXID, portal.Name) + _, _ = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) + } else { + portal.Name = "" + } + portal.log.Infoln("Created private chat portal in %s after invite from", roomID, inviter.MXID) + intent := puppet.DefaultIntent() + + if mx.bridge.Config.Bridge.Encryption.Default { + _, err := intent.InviteUser(roomID, &mautrix.ReqInviteUser{UserID: mx.bridge.Bot.UserID}) + if err != nil { + portal.log.Warnln("Failed to invite bridge bot to enable e2be:", err) + } + err = mx.bridge.Bot.EnsureJoined(roomID) + if err != nil { + portal.log.Warnln("Failed to join as bridge bot to enable e2be:", err) + } + _, err = intent.SendStateEvent(roomID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}) + if err != nil { + portal.log.Warnln("Failed to enable e2be:", err) + } + mx.as.StateStore.SetMembership(roomID, inviter.MXID, event.MembershipJoin) + mx.as.StateStore.SetMembership(roomID, puppet.MXID, event.MembershipJoin) + mx.as.StateStore.SetMembership(roomID, mx.bridge.Bot.UserID, event.MembershipJoin) + portal.Encrypted = true + } + portal.Update() + portal.UpdateBridgeInfo() + _, _ = intent.SendNotice(roomID, "Private chat portal created") + + err := portal.FillInitialHistory(inviter) + if err != nil { + portal.log.Errorln("Failed to fill history:", err) + } + + inviter.addPortalToCommunity(portal) + inviter.addPuppetToCommunity(puppet) +} + +func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, puppet *Puppet) { + intent := puppet.DefaultIntent() + members := mx.joinAndCheckMembers(evt, intent) + if members == nil { + return + } + var hasBridgeBot, hasOtherUsers bool + for mxid, _ := range members.Joined { + if mxid == intent.UserID || mxid == inviter.MXID { + continue + } else if mxid == mx.bridge.Bot.UserID { + hasBridgeBot = true + } else { + hasOtherUsers = true + } + } + if !hasBridgeBot && !hasOtherUsers { + key := database.NewPortalKey(puppet.JID, inviter.JID) + mx.handlePrivatePortal(evt.RoomID, inviter, puppet, key) + } else if !hasBridgeBot { + mx.log.Debugln("Leaving multi-user room", evt.RoomID, "as", puppet.MXID, "after accepting invite from", evt.Sender) + _, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.") + _, _ = intent.LeaveRoom(evt.RoomID) + } else { + _, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a WhatsApp group.") + } +} + func (mx *MatrixHandler) HandleMembership(evt *event.Event) { if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { return @@ -126,10 +246,6 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { content := evt.Content.AsMember() if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() { mx.HandleBotInvite(evt) - } - - portal := mx.bridge.GetPortalByMXID(evt.RoomID) - if portal == nil { return } @@ -138,6 +254,15 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { return } + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + if portal == nil { + puppet := mx.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + if content.Membership == event.MembershipInvite && puppet != nil { + mx.HandlePuppetInvite(evt, user, puppet) + } + return + } + isSelf := id.UserID(evt.GetStateKey()) == evt.Sender if content.Membership == event.MembershipLeave { if id.UserID(evt.GetStateKey()) == evt.Sender { if evt.Unsigned.PrevContent != nil { @@ -152,6 +277,8 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { } else { portal.HandleMatrixKick(user, evt) } + } else if content.Membership == event.MembershipInvite && !isSelf { + portal.HandleMatrixInvite(user, evt) } } diff --git a/portal.go b/portal.go index 6c3337f..b79503f 100644 --- a/portal.go +++ b/portal.go @@ -1015,6 +1015,43 @@ var ( StateHalfShotBridgeInfo = event.Type{Type: "uk.half-shot.bridge", Class: event.StateEventType} ) +func (portal *Portal) getBridgeInfo() (string, BridgeInfoContent) { + bridgeInfo := BridgeInfoContent{ + BridgeBot: portal.bridge.Bot.UserID, + Creator: portal.MainIntent().UserID, + Protocol: BridgeInfoSection{ + ID: "skype", + DisplayName: "Skype", + AvatarURL: id.ContentURIString(portal.bridge.Config.AppService.Bot.Avatar), + ExternalURL: "https://www.skype.com/", + }, + Channel: BridgeInfoSection{ + ID: portal.Key.JID, + DisplayName: portal.Name, + AvatarURL: portal.AvatarURL.CUString(), + }, + } + bridgeInfoStateKey := fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) + return bridgeInfoStateKey, bridgeInfo +} + +func (portal *Portal) UpdateBridgeInfo() { + if len(portal.MXID) == 0 { + portal.log.Debugln("Not updating bridge info: no Matrix room created") + return + } + portal.log.Debugln("Updating bridge info...") + stateKey, content := portal.getBridgeInfo() + _, err := portal.MainIntent().SendStateEvent(portal.MXID, StateBridgeInfo, stateKey, content) + if err != nil { + portal.log.Warnln("Failed to update m.bridge:", err) + } + _, err = portal.MainIntent().SendStateEvent(portal.MXID, StateHalfShotBridgeInfo, stateKey, content) + if err != nil { + portal.log.Warnln("Failed to update uk.half-shot.bridge:", err) + } +} + func (portal *Portal) CreateMatrixRoom(user *User) error { portal.roomCreateLock.Lock() defer portal.roomCreateLock.Unlock() @@ -1024,9 +1061,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { intent := portal.MainIntent() if err := intent.EnsureRegistered(); err != nil { - fmt.Println() - fmt.Println("CreateMatrixRoom0: ", err) - fmt.Println() return err } @@ -1035,9 +1069,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { var metadata *skypeExt.GroupInfo if portal.IsPrivateChat() { - fmt.Println() - fmt.Println("CreateMatrixRoom1: ") - fmt.Println() puppet := portal.bridge.GetPuppetByJID(portal.Key.JID+skypeExt.NewUserSuffix) if portal.bridge.Config.Bridge.PrivateChatPortalMeta { portal.Name = puppet.Displayname @@ -1048,15 +1079,9 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { } portal.Topic = "skype private chat" } else if portal.IsStatusBroadcastRoom() { - fmt.Println() - fmt.Println("CreateMatrixRoom2: ") - fmt.Println() portal.Name = "skype Status Broadcast" portal.Topic = "skype status updates from your contacts" } else { - fmt.Println() - fmt.Println("CreateMatrixRoom3: ") - fmt.Println() var err error metadata, err = user.Conn.GetGroupMetaData(portal.Key.JID) if err == nil { @@ -1068,7 +1093,6 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { if key == user.JID { continue } - fmt.Println("CreateMatrixRoom3.1: ", key) if contact, ok := user.Conn.Store.Contacts[key]; ok { if len(portalName) > 0 { portalName = portalName + ", " + contact.DisplayName @@ -2355,6 +2379,35 @@ func (portal *Portal) Delete() { portal.bridge.portalsLock.Unlock() } +func (portal *Portal) GetMatrixUsers() ([]id.UserID, error) { + members, err := portal.MainIntent().JoinedMembers(portal.MXID) + if err != nil { + return nil, errors.Wrap(err, "failed to get member list") + } + var users []id.UserID + for userID := range members.Joined { + _, isPuppet := portal.bridge.ParsePuppetMXID(userID) + if !isPuppet && userID != portal.bridge.Bot.UserID { + users = append(users, userID) + } + } + return users, nil +} + +func (portal *Portal) CleanupIfEmpty() { + users, err := portal.GetMatrixUsers() + if err != nil { + portal.log.Errorfln("Failed to get Matrix user list to determine if portal needs to be cleaned up: %v", err) + return + } + + if len(users) == 0 { + portal.log.Infoln("Room seems to be empty, cleaning up...") + portal.Delete() + portal.Cleanup(false) + } +} + func (portal *Portal) Cleanup(puppetsOnly bool) { if len(portal.MXID) == 0 { return @@ -2401,9 +2454,45 @@ func (portal *Portal) HandleMatrixLeave(sender *User) { portal.Delete() portal.Cleanup(false) return + } else { + // TODO should we somehow deduplicate this call if this leave was sent by the bridge? + err := sender.Conn.HandleGroupLeave(portal.Key.JID) + if err != nil { + portal.log.Errorfln("Failed to leave group as %s: %v", sender.MXID, err) + return + } + //portal.log.Infoln("Leave response:", <-resp) + portal.CleanupIfEmpty() } } -func (portal *Portal) HandleMatrixKick(sender *User, event *event.Event) { - // TODO +func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) { + number, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) + puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + fmt.Println("HandleMatrixKick", puppet) + if puppet != nil { + number = strings.Replace(number, skypeExt.NewUserSuffix, "", 1) + err := sender.Conn.HandleGroupKick(portal.Key.JID, []string{number}) + if err != nil { + portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err) + return + } + //portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp) + } +} + +func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) { + number, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) + puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) + fmt.Println("HandleMatrixInvite", puppet) + if puppet != nil { + number = strings.Replace(number, "8:", "", 1) + number = strings.Replace(number, skypeExt.NewUserSuffix, "", 1) + err := sender.Conn.HandleGroupInvite(portal.Key.JID, []string{number}) + if err != nil { + portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err) + return + } + //portal.log.Infoln("Add %s response: %s", puppet.JID, <-resp) + } } diff --git a/puppet.go b/puppet.go index 02623ba..bcf5cd7 100644 --- a/puppet.go +++ b/puppet.go @@ -19,7 +19,6 @@ import ( ) func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.SkypeID, bool) { - fmt.Println("ParsePuppetMXID: ", bridge.Config.Bridge.FormatUsername("(.*)")) userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", bridge.Config.Bridge.FormatUsername("(.*)"), bridge.Config.Homeserver.Domain)) @@ -31,8 +30,15 @@ func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.SkypeID, bool) { if match == nil || len(match) != 2 { return "", false } - - jid := types.SkypeID(match[1] + whatsappExt.NewUserSuffix) + realId := match[1] + cond1 := "8-live-" + cond2 := "8-" + if strings.HasPrefix(realId, cond1) { + realId = strings.Replace(realId, cond1, "8:live:", 1) + } else if strings.HasPrefix(realId, cond2){ + realId = strings.Replace(realId, cond2, "8:", 1) + } + jid := types.SkypeID(realId + skypeExt.NewUserSuffix) return jid, true } diff --git a/skype-ext/skype.go b/skype-ext/skype.go index 7b4be96..2b915d3 100644 --- a/skype-ext/skype.go +++ b/skype-ext/skype.go @@ -275,9 +275,10 @@ func (ext *ExtendedConn) HandleGroupShare(groupJid string) (err error, link stri func (ext *ExtendedConn) HandleGroupKick(groupJid string, numbers[]string) (err error) { for _, number := range numbers{ - err = ext.Conn.RemoveMember(groupJid, number) - if err != nil { - fmt.Printf("%s Handle kick err", err) + if err == nil { + err = ext.Conn.RemoveMember(groupJid, number) + } else { + _ = ext.Conn.RemoveMember(groupJid, number) } } return From 891ff890db4cb0ec00478f135bb3de00a587f3ea Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 22 Sep 2020 19:43:34 +0800 Subject: [PATCH 011/109] Fix the problem that the synchronization session is not consistent with the skype side --- commands.go | 8 ++++---- example-config.yaml | 2 ++ portal.go | 6 +++++- user.go | 16 +++++++++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/commands.go b/commands.go index 3fb85a5..6452376 100644 --- a/commands.go +++ b/commands.go @@ -484,7 +484,7 @@ func (handler *CommandHandler) CommandSync(ce *CommandEvent) { ce.Reply("Failed to contacts chat list (see logs for details)") } ce.Reply("Syncing conversations...") - err = ce.User.Conn.GetConversations("") + err = ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) if err != nil { user.log.Errorln("Error get conversations:", err) ce.Reply("Failed to conversations list (see logs for details)") @@ -625,7 +625,7 @@ func (handler *CommandHandler) CommandList(ce *CommandEvent) { return } } else { - err = ce.User.Conn.GetConversations("") + err = ce.User.Conn.GetConversations("", handler.bridge.Config.Bridge.InitialChatSync) if err != nil { ce.Reply("Get conversations error") return @@ -669,7 +669,7 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) { ce.Reply("That looks like a user ID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsappExt.NewUserSuffix)]) return } - ce.User.Conn.GetConversations("") + ce.User.Conn.GetConversations("", handler.bridge.Config.Bridge.InitialChatSync) fmt.Println("user.Conn.Store.Chats: ", user.Conn.Store.Chats) chat, ok := user.Conn.Store.Chats[jid] if !ok { @@ -923,7 +923,7 @@ func (handler *CommandHandler) CommandInvite(ce *CommandEvent) { fmt.Println("user.Conn.Store.Chats", user.Conn.Store.Contacts) if !ok { //user.Conn - err := ce.User.Conn.GetConversations("") + err := ce.User.Conn.GetConversations("", handler.bridge.Config.Bridge.InitialChatSync) //time.Sleep(5 * time.Second) if err != nil { fmt.Println(err) diff --git a/example-config.yaml b/example-config.yaml index e1281ba..246a70e 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -105,6 +105,8 @@ bridge: end: true # Number of chats to sync for new users. + # Since some of the obtained conversations are not the conversations that the user needs to see, + # the actual number of conversations displayed on the matrix client will be slightly less than the set value initial_chat_sync_count: 10 # Number of old messages to fill when creating new portal rooms. initial_history_fill_count: 20 diff --git a/portal.go b/portal.go index b79503f..68bf24e 100644 --- a/portal.go +++ b/portal.go @@ -620,6 +620,10 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { } else { fmt.Println("SyncSkype ensureUserInvited", portal.MXID) portal.ensureUserInvited(user) + rep, err := portal.MainIntent().SetPowerLevel(portal.MXID, user.MXID, 100) + if err != nil { + portal.log.Warnfln("SyncSkype: SetPowerLevel err: ", err, rep) + } } if portal.IsPrivateChat() { @@ -680,7 +684,7 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent { anyone := 0 nope := 99 - invite := 99 + invite := 50 if portal.bridge.Config.Bridge.AllowUserInvite { invite = 0 } diff --git a/user.go b/user.go index 5f9774f..23d1e27 100644 --- a/user.go +++ b/user.go @@ -399,7 +399,7 @@ func (user *User) Login(ce *CommandEvent, name string, password string) { user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix user.addToJIDMap() //user.PostLogin() - ce.User.Conn.GetConversations("") + ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) } func loopPresence(user *User) { Loop: @@ -571,8 +571,18 @@ func (user *User) syncPortals(chatMap map[string]skype.Conversation, createAll b if chat.Properties.ConversationStatus != "Accepted" && len(chat.ThreadProperties.Lastjoinat) < 1 { continue } - // user.log.Warnfln("Non-integer last message time in %s: %s", chat.Id, t) - //continue + } + // Filter calllogs conversation + if chat.Id == "48:calllogs" { + continue + } + // Filter conversations that have not sent messages + if chat.LastMessage.Id == "" { + continue + } + // 'Lastleaveat' value means that you have left the current conversation + if len(chat.ThreadProperties.Lastleaveat) > 0 { + continue } ts := uint64(t.UnixNano()) cid, _ := chat.Id.(string) From ff1c6b766e94f4d3912529f33a7d4383802f3193 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 22 Sep 2020 19:58:38 +0800 Subject: [PATCH 012/109] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b255169..d1d0a7a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * user name/avatar change * Typing status -The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). +The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 1ff4fd6bafea09ad4df750321fa1b243fc5bbff0 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 22 Sep 2020 20:00:24 +0800 Subject: [PATCH 013/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1d0a7a..c76709e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@2bd2763a9a9835774738009547301ebc37220c24` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 8ff600bd23c8c97e6eb62729b5bb63cfdb013b8a Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 23 Sep 2020 20:09:33 +0800 Subject: [PATCH 014/109] update command 'create' group --- commands.go | 47 ++++------------------------------------------ skype-ext/skype.go | 2 +- 2 files changed, 5 insertions(+), 44 deletions(-) diff --git a/commands.go b/commands.go index 6452376..56cffad 100644 --- a/commands.go +++ b/commands.go @@ -901,55 +901,26 @@ func (handler *CommandHandler) CommandInvite(ce *CommandEvent) { userNumbers := strings.Split(ce.Args[1], ",") if strings.HasSuffix(conversationId, skypeExt.NewUserSuffix) { - ce.Reply("**Usage:** `invite ,...`") + ce.Reply("**Usage:** `invite ,...`") return } - for i, number := range userNumbers { - // - number = strings.Replace(number, "8:", "", 1) - userNumbers[i] = number // + whatsappExt.NewUserSuffix - } - fmt.Println("sign in invite function") - fmt.Printf("%+v \n", user) - fmt.Printf("%+v \n", userNumbers) - fmt.Printf("%+v \n", conversationId) - //jidStr := strings.Split(jid, "@s.skype.net") - //fmt.Println(jidStr) - //jid = jidStr[0] - //contact, ok := user.Conn.Store.Contacts[jid] - group, ok := user.Conn.Store.Chats[conversationId] - fmt.Println("group first : ", group) - fmt.Println("user.Conn.Store.Chats", user.Conn.Store.Contacts) + _, ok := user.Conn.Store.Chats[conversationId] if !ok { //user.Conn err := ce.User.Conn.GetConversations("", handler.bridge.Config.Bridge.InitialChatSync) //time.Sleep(5 * time.Second) if err != nil { - fmt.Println(err) ce.Reply("get conversations failed. Try syncing contacts with `sync` first.") } else { - group, ok = user.Conn.Store.Chats[conversationId] + _, ok = user.Conn.Store.Chats[conversationId] if !ok { ce.Reply("Group JID not found in chats. Try syncing groups with `sync` first.") return } } } - fmt.Println("group", group) - fmt.Println("GetConversations", user.Conn.Store.Contacts) handler.log.Debugln("GetConversations", conversationId, "for", user) - - //portal := user.bridge.GetPortalByJID(database.GroupPortalKey(conversationId)) - //fmt.Printf("portal %+v : ", portal) - //if len(portal.MXID) > 0 { - // //portaFl.Sync(user, contact) - // ce.Reply("Portal room synced.") - //} else { - // //portal.Sync(user, contact) - // //ce.Reply("Portal room created.") - //} - // handler.log.Debugln("Inviting", userNumbers, "to", conversationId) err := user.Conn.HandleGroupInvite(conversationId, userNumbers) if err != nil { @@ -957,16 +928,6 @@ func (handler *CommandHandler) CommandInvite(ce *CommandEvent) { } else { ce.Reply("Group invitation sent.\nIf the member fails to join the group, please check your permissions or command parameters") } - //time.Sleep(time.Duration(3)*time.Second) - //ce.Reply("Syncing room puppet...") - //chatMap := make(map[string]whatsapp.Chat) - //for _, chat := range user.Conn.Store.Chats { - // if chat.Jid == jid { - // chatMap[chat.Jid]= chat - // } - //} - //user.syncPortals(chatMap, false) - //ce.Reply("Syncing room puppet completed") } const cmdKickHelp = `kick <_group ID_> <_contact Id>,... - Remove members from the group.` @@ -1196,7 +1157,7 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { members = skype.Members{} for _, memberId := range inputArr { members.Members = append(members.Members, skype.Member{ - Id: "8:" + memberId, + Id: memberId, Role: "Admin", }) } diff --git a/skype-ext/skype.go b/skype-ext/skype.go index 2b915d3..c80c06e 100644 --- a/skype-ext/skype.go +++ b/skype-ext/skype.go @@ -230,7 +230,7 @@ func (ext *ExtendedConn) HandleGroupInvite(groupJid string, numbers[]string) (er members = skype.Members{} for _, memberId := range numbers { members.Members = append(members.Members, skype.Member{ - Id: "8:"+memberId, + Id: memberId, Role: "Admin", }) } From 99c47fe6aa0efd512fa2aad313779b8e4a308e27 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 24 Sep 2020 16:58:49 +0800 Subject: [PATCH 015/109] Fix the problem that members cannot be added when creating a group --- commands.go | 7 +++--- portal.go | 63 +++++++++++++++++----------------------------- skype-ext/skype.go | 8 +----- user.go | 6 ++--- 4 files changed, 30 insertions(+), 54 deletions(-) diff --git a/commands.go b/commands.go index 56cffad..8d4add5 100644 --- a/commands.go +++ b/commands.go @@ -1151,9 +1151,7 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { handler.log.Debugln("Create Group", topic, "with", members) err := user.Conn.HandleGroupCreate(members) - inputArr := strings.Split(ce.Args[1], ",") - inputArr = inputArr[1:] members = skype.Members{} for _, memberId := range inputArr { members.Members = append(members.Members, skype.Member{ @@ -1161,7 +1159,10 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { Role: "Admin", }) } - err = user.Conn.AddMember(members, "") + conversationId, ok := <-user.Conn.CreateChan + if ok { + err = user.Conn.AddMember(members, conversationId) + } if err != nil { ce.Reply("Please confirm that parameters is correct.") } else { diff --git a/portal.go b/portal.go index 68bf24e..f8cbaaa 100644 --- a/portal.go +++ b/portal.go @@ -752,50 +752,33 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { //} func (portal *Portal) membershipRemove(content string) { - xmlFormat := skype.XmlContent{} + xmlFormat := skype.XmlDeleteMember{} err := xml.Unmarshal([]byte(content), &xmlFormat) + for _, target := range xmlFormat.Targets { + member := portal.bridge.GetPuppetByJID(target) - member := portal.bridge.GetPuppetByJID(xmlFormat.Target) - - memberMaxid := strings.Replace(string(member.MXID), "@skype&8:", "@skype&8-", 1) - _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ - UserID: id.UserID(memberMaxid), - }) - if err != nil { - portal.log.Errorln("Error %v member from whatsapp:", err) - } - //for _, chat := range user.Conn.Store.Chats { - // group := portal.bridge.GetPuppetByJID(chat.Id.(string)) - // fmt.Println("member") - // fmt.Println(group) - // fmt.Println("用户信息:") - // fmt.Println(chat.Id.(string)) - // - // if group == nil { - // portal.log.Errorln("%s is not exist", jid) - // continue - // } - // if group.JID == jid { - // _, err := portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ - // UserID: group.MXID, - // }) - // if err != nil { - // portal.log.Errorln("Error %v member from whatsapp:", err) - // } - // } - //} -} - -func (portal *Portal) membershipAdd(user *User, jid string) { - chatMap := make(map[string]skype.Conversation) - for _, chat := range user.Conn.Store.Chats { - if chat.Id == jid { - cid, _ := chat.Id.(string) - chatMap[cid] = chat + memberMaxid := strings.Replace(string(member.MXID), "@skype&8:", "@skype&8-", 1) + _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ + UserID: id.UserID(memberMaxid), + }) + if err != nil { + portal.log.Errorln("Error %v member from whatsapp:", err) + } + } +} + +func (portal *Portal) membershipAdd(content string) { + xmlFormat := skype.XmlAddMember{} + err := xml.Unmarshal([]byte(content), &xmlFormat) + + for _, target := range xmlFormat.Targets { + puppet := portal.bridge.GetPuppetByJID(target) + fmt.Println("membershipAdd puppet jid", target) + err = puppet.IntentFor(portal).EnsureJoined(portal.MXID) + if err != nil { + portal.log.Errorln("Error %v joined member from skype:", err) } } - fmt.Println("membershipAddzsl:", chatMap) - user.syncPortals(chatMap, false) } func (portal *Portal) membershipCreate(user *User, cmd skypeExt.ChatUpdate) { diff --git a/skype-ext/skype.go b/skype-ext/skype.go index c80c06e..ab352da 100644 --- a/skype-ext/skype.go +++ b/skype-ext/skype.go @@ -285,13 +285,7 @@ func (ext *ExtendedConn) HandleGroupKick(groupJid string, numbers[]string) (err } func (ext *ExtendedConn) HandleGroupCreate(numbers skype.Members) (err error) { - //var parts []string - //parts = append(parts, numbers...) - err = ext.Conn.CreateConversationGroup(numbers) - //if err != nil { - // fmt.Printf("%s HandleGroupCreate err", err) - //} - return + return ext.Conn.CreateConversationGroup(numbers) } func (ext *ExtendedConn) HandleGroupLeave(groupJid string) (err error) { diff --git a/user.go b/user.go index 23d1e27..552bb80 100644 --- a/user.go +++ b/user.go @@ -1077,16 +1077,14 @@ func (user *User) HandleChatUpdate(cmd skype.Resource) { } go portal.UpdateAvatar(user, avatar) case skypeExt.ChatMemberAdd: - fmt.Println("portal.MXID") - fmt.Println(portal.MXID) - fmt.Println(user) + user.log.Debugfln("chat member add") if len(portal.MXID) == 0 { err := portal.CreateMatrixRoom(user) if err != nil { fmt.Println("create room failed") } } - go portal.membershipAdd(user, cmd.Jid) + go portal.membershipAdd(cmd.Content) case skypeExt.ChatMemberDelete: go portal.membershipRemove(cmd.Content) //case skypeExt.ChatActionAddTopic: From bdab0f669910019d7f16621747d49cd06ed2f981 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Thu, 24 Sep 2020 17:05:15 +0800 Subject: [PATCH 016/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c76709e..66ede68 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@2bd2763a9a9835774738009547301ebc37220c24` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@0dbf261f44da42a61cbce3dfe1eff1538520a6bf` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 261ea31c7502d8bd1dc8f8ae0b4cb235cf9dc3e8 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 24 Sep 2020 20:22:06 +0800 Subject: [PATCH 017/109] sync up leave group from skype --- portal.go | 2 +- skype-ext/chat.go | 1 + user.go | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/portal.go b/portal.go index f8cbaaa..efc9bf2 100644 --- a/portal.go +++ b/portal.go @@ -1127,7 +1127,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { // content.Users[user.MXID] = 100 //} // When creating a room, make user self the highest level of authority - content.Users[user.MXID] = 100 + content.Users[user.MXID] = 99 initialState := []*event.Event{{ Type: event.StatePowerLevels, Content: event.Content{ diff --git a/skype-ext/chat.go b/skype-ext/chat.go index 5a41d6b..70453dd 100644 --- a/skype-ext/chat.go +++ b/skype-ext/chat.go @@ -49,6 +49,7 @@ const ( ChatActionAdd ChatActionType = "add" ChatActionIntroduce ChatActionType = "introduce" ChatActionCreate ChatActionType = "create" + ChatActionThread ChatActionType = "Thread" ) const ( diff --git a/user.go b/user.go index 552bb80..1ea6870 100644 --- a/user.go +++ b/user.go @@ -1087,6 +1087,14 @@ func (user *User) HandleChatUpdate(cmd skype.Resource) { go portal.membershipAdd(cmd.Content) case skypeExt.ChatMemberDelete: go portal.membershipRemove(cmd.Content) + case "": + if skypeExt.ChatActionType(cmd.Type) == skypeExt.ChatActionThread { + if len(cmd.ETag) > 0 && len(cmd.Properties.Capabilities) < 1{ + portal.Delete() + portal.Cleanup(false) + } + } + //case skypeExt.ChatActionAddTopic: // go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID) //case skypeExt.ChatActionRemoveTopic: From d88251f1451c74056822aab16277789f77dadbdb Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 24 Sep 2020 20:22:26 +0800 Subject: [PATCH 018/109] open a direct message room in matrix client --- database/portal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/database/portal.go b/database/portal.go index 756d197..ade1cba 100644 --- a/database/portal.go +++ b/database/portal.go @@ -28,6 +28,7 @@ func NewPortalKey(jid, receiver types.SkypeID) PortalKey { if strings.HasSuffix(jid, skypeExt.GroupSuffix) { receiver = jid } + jid = strings.Replace(jid, skypeExt.NewUserSuffix, "", 1) return PortalKey{ JID: jid, Receiver: receiver, From 414db1af6f0fc1158b6884e9cc27137b5aca8d43 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Thu, 24 Sep 2020 20:25:31 +0800 Subject: [PATCH 019/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 66ede68..e498ddb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@0dbf261f44da42a61cbce3dfe1eff1538520a6bf` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@ef3c1ea900b605c75888b93c016eb9aad24d0fe4` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From ffa87ebcfc25f5fe1ba25789589e0bc58459fd38 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 12 Oct 2020 20:13:19 +0800 Subject: [PATCH 020/109] Solve the problem of switching the matrix client account to synchronize the same Skype account --- commands.go | 6 ++++-- matrix.go | 2 +- portal.go | 40 ++++++++++++++++++++++++---------------- user.go | 21 +++++++-------------- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/commands.go b/commands.go index 8d4add5..612409d 100644 --- a/commands.go +++ b/commands.go @@ -239,8 +239,10 @@ func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.") return } - ce.User.Login(ce, ce.Args[0], ce.Args[1]) - syncAll(ce.User, true) + err := ce.User.Login(ce, ce.Args[0], ce.Args[1]) + if err == nil { + syncAll(ce.User, true) + } } const cmdLogoutHelp = `logout - Logout from WhatsApp` diff --git a/matrix.go b/matrix.go index bc5795e..347598a 100644 --- a/matrix.go +++ b/matrix.go @@ -161,7 +161,7 @@ func (mx *MatrixHandler) handlePrivatePortal(roomID id.RoomID, inviter *User, pu func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { portal.MXID = roomID - portal.Topic = "WhatsApp private chat" + portal.Topic = "Skype private chat" _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) if portal.bridge.Config.Bridge.PrivateChatPortalMeta { portal.Name = puppet.Displayname diff --git a/portal.go b/portal.go index efc9bf2..75234a7 100644 --- a/portal.go +++ b/portal.go @@ -620,10 +620,20 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { } else { fmt.Println("SyncSkype ensureUserInvited", portal.MXID) portal.ensureUserInvited(user) - rep, err := portal.MainIntent().SetPowerLevel(portal.MXID, user.MXID, 100) + rep, err := portal.MainIntent().SetPowerLevel(portal.MXID, user.MXID, 95) if err != nil { portal.log.Warnfln("SyncSkype: SetPowerLevel err: ", err, rep) } + + preUserIds,_ := portal.GetMatrixUsers() + for _,userId := range preUserIds { + if user.MXID != userId { + err := portal.tryKickUser(userId, portal.MainIntent()) + if err != nil { + portal.log.Errorln("Failed to try kick user:", err) + } + } + } } if portal.IsPrivateChat() { @@ -683,7 +693,7 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent { anyone := 0 - nope := 99 + nope := 95 invite := 50 if portal.bridge.Config.Bridge.AllowUserInvite { invite = 0 @@ -1127,7 +1137,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { // content.Users[user.MXID] = 100 //} // When creating a room, make user self the highest level of authority - content.Users[user.MXID] = 99 + content.Users[user.MXID] = 95 initialState := []*event.Event{{ Type: event.StatePowerLevels, Content: event.Content{ @@ -1506,19 +1516,6 @@ func (portal *Portal) sendMediaBridgeFailureSkype(source *User, intent *appservi } } -func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, info whatsapp.MessageInfo, downloadErr error) { - portal.log.Errorfln("Failed to download media for %s: %v", info.Id, downloadErr) - resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: "Failed to bridge media", - }, int64(info.Timestamp*1000)) - if err != nil { - portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) - } else { - portal.finishHandling(source, info.Source, resp.EventID) - } -} - func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) { if !portal.Encrypted { return data, mimeType, nil @@ -1532,6 +1529,17 @@ func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, } +func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error { + _, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID}) + if err != nil { + httpErr, ok := err.(mautrix.HTTPError) + if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" { + _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID}) + } + } + return err +} + func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn *skype.Conn, mediaType string) (data []byte, mediaMessage *skype.MediaMessageContent, err error), mediaType string, thumbnail []byte, info skype.Resource, sendAsSticker bool) { intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, info) if endHandlePrivateChatFromMe != nil { diff --git a/user.go b/user.go index 1ea6870..337d7da 100644 --- a/user.go +++ b/user.go @@ -361,17 +361,16 @@ func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventID } } -func (user *User) Login(ce *CommandEvent, name string, password string) { +func (user *User) Login(ce *CommandEvent, name string, password string) (err error) { if user.contactsPresence == nil { user.contactsPresence = make(map[string]*skypeExt.Presence) } - session, err := user.Conn.Login(name, password) + err = user.Conn.Login(name, password) if err != nil { user.log.Errorln("Failed to login:", err) - ce.Reply("Please confirm that your account password is entered correctly") - return + ce.Reply(err.Error()) + return err } - fmt.Println("user.MXID", user.MXID) ce.Reply("Successfully logged in") // subscribe basic @@ -381,7 +380,6 @@ func (user *User) Login(ce *CommandEvent, name string, password string) { if err == nil{ var userIds []string for _, contact := range user.Conn.Store.Contacts { - //fmt.Println(contact.PersonId) if strings.Index(contact.PersonId, "28:") > -1 { continue } @@ -389,24 +387,19 @@ func (user *User) Login(ce *CommandEvent, name string, password string) { userIds = append(userIds, userId) } ce.User.Conn.SubscribeUsers(userIds) - //fmt.Println("Login user0: ", user.JID) go loopPresence(user) } go user.Conn.Poll() user.ConnectionErrors = 0 - user.SetSession(&session) + user.SetSession(user.Conn.LoginInfo) user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix user.addToJIDMap() - //user.PostLogin() ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) + return } func loopPresence(user *User) { -Loop: - for i := 0; i <= 1000; i++ { - if i > 1000 { - goto Loop - } + for { for cid, contact := range user.contactsPresence { puppet := user.bridge.GetPuppetByJID(cid) _ = puppet.DefaultIntent().SetPresence(event.Presence(strings.ToLower(contact.Availability))) From 5b122411aed1c65e4cd3da6412f8a7f7a928fd05 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 13 Oct 2020 13:15:19 +0800 Subject: [PATCH 021/109] add command "logout" --- commands.go | 23 ++++++++++++++++----- user.go | 57 +++++++---------------------------------------------- 2 files changed, 25 insertions(+), 55 deletions(-) diff --git a/commands.go b/commands.go index 612409d..275bfae 100644 --- a/commands.go +++ b/commands.go @@ -113,8 +113,8 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { // handler.CommandDevTest(ce) //case "set-pl": // handler.CommandSetPowerLevel(ce) - //case "logout": - // handler.CommandLogout(ce) + case "logout": + handler.CommandLogout(ce) case "login-matrix", "sync", "list", "open", "pm", "invite", "kick", "leave", "join", "create", "share": if !ce.User.HasSession() { ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") @@ -245,8 +245,21 @@ func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { } } -const cmdLogoutHelp = `logout - Logout from WhatsApp` - +const cmdLogoutHelp = `logout - Logout from Skype` +func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { + //ce.User.Conn.Conn.LogoutChan <- 1 + ce.User.Conn.LoggedIn = false + ce.User.Conn.LoginInfo = &skype.Session{ + SkypeToken: "", + SkypeExpires: "", + RegistrationToken: "", + RegistrationTokenStr: "", + RegistrationExpires: "", + LocationHost: "", + EndpointId: "", + } + ce.Reply("Logged out successfully.") +} // CommandLogout handles !logout command //func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { // if ce.User.Session == nil { @@ -446,7 +459,7 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { ce.Reply("* " + strings.Join([]string{ cmdPrefix + cmdHelpHelp, cmdPrefix + cmdLoginHelp, - //cmdPrefix + cmdLogoutHelp, + cmdPrefix + cmdLogoutHelp, //cmdPrefix + cmdDeleteSessionHelp, //cmdPrefix + cmdReconnectHelp, //cmdPrefix + cmdDisconnectHelp, diff --git a/user.go b/user.go index 337d7da..6ecaa6d 100644 --- a/user.go +++ b/user.go @@ -373,9 +373,7 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err } ce.Reply("Successfully logged in") - // subscribe basic - user.Conn.Subscribes() - // subscribe conta + user.Conn.Subscribes() // subscribe basic event err = user.Conn.Conn.ContactList(user.Conn.UserProfile.Username) if err == nil{ var userIds []string @@ -395,11 +393,15 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err user.SetSession(user.Conn.LoginInfo) user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix user.addToJIDMap() - ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) + _ = ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) return } -func loopPresence(user *User) { + +func loopPresence(user *User) { for { + if user.Conn.LoggedIn == false { + break + } for cid, contact := range user.contactsPresence { puppet := user.bridge.GetPuppetByJID(cid) _ = puppet.DefaultIntent().SetPresence(event.Presence(strings.ToLower(contact.Availability))) @@ -407,51 +409,6 @@ func loopPresence(user *User) { time.Sleep(39 * time.Second) } } -func (user *User) Login1(ce *CommandEvent) { - //qrChan := make(chan string, 3) - //eventIDChan := make(chan id.EventID, 1) - //go user.loginQrChannel(ce, qrChan, eventIDChan) - //session, err := user.Conn.LoginWithRetry(qrChan, user.bridge.Config.Bridge.LoginQRRegenCount) - //qrChan <- "stop" - //if err != nil { - // var eventID id.EventID - // select { - // case eventID = <-eventIDChan: - // default: - // } - // reply := event.MessageEventContent{ - // MsgType: event.MsgText, - // } - // if err == whatsapp.ErrAlreadyLoggedIn { - // reply.Body = "You're already logged in" - // } else if err == whatsapp.ErrLoginInProgress { - // reply.Body = "You have a login in progress already." - // } else if err == whatsapp.ErrLoginTimedOut { - // reply.Body = "QR code scan timed out. Please try again." - // } else { - // user.log.Warnln("Failed to log in:", err) - // reply.Body = fmt.Sprintf("Unknown error while logging in: %v", err) - // } - // msg := reply - // if eventID != "" { - // msg.NewContent = &reply - // msg.RelatesTo = &event.RelatesTo{ - // Type: event.RelReplace, - // EventID: eventID, - // } - // } - // _, _ = ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &msg) - // return - //} - //// TODO there's a bit of duplication between this and the provisioning API login method - //// Also between the two logout methods (commands.go and provisioning.go) - //user.ConnectionErrors = 0 - //user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) - //user.addToJIDMap() - //user.SetSession(&session) - //ce.Reply("Successfully logged in, synchronizing chats...") - //user.PostLogin() -} type Chat struct { Portal *Portal From 647b485cd0411ce319cb0ddac9208d16e9ec993d Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 13 Oct 2020 13:22:05 +0800 Subject: [PATCH 022/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e498ddb..0e298aa 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@ef3c1ea900b605c75888b93c016eb9aad24d0fe4` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@715ec96e1cdd9ed801f2ff9eed81544c0c65729b` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 55c32bfba793f2c6988fe31611b93b2cd30d2f3d Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 16 Oct 2020 20:42:32 +0800 Subject: [PATCH 023/109] add command 'ping' --- commands.go | 48 ++++++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/commands.go b/commands.go index 275bfae..1d27b18 100644 --- a/commands.go +++ b/commands.go @@ -99,8 +99,8 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { // handler.CommandReconnect(ce) //case "disconnect": // handler.CommandDisconnect(ce) - //case "ping": - // handler.CommandPing(ce) + case "ping": + handler.CommandPing(ce) //case "delete-connection": // handler.CommandDeleteConnection(ce) //case "delete-session": @@ -421,31 +421,23 @@ const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without loggin const cmdPingHelp = `ping - Check your connection to WhatsApp.` -//func (handler *CommandHandler) CommandPing(ce *CommandEvent) { -// if ce.User.Session == nil { -// if ce.User.IsLoginInProgress() { -// ce.Reply("You're not logged into WhatsApp, but there's a login in progress.") -// } else { -// ce.Reply("You're not logged into WhatsApp.") -// } -// } else if ce.User.Conn == nil { -// ce.Reply("You don't have a WhatsApp connection.") -// } else if ok, err := ce.User.Conn.AdminTest(); err != nil { -// if ce.User.IsLoginInProgress() { -// ce.Reply("Connection not OK: %v, but login in progress", err) -// } else { -// ce.Reply("Connection not OK: %v", err) -// } -// } else if !ok { -// if ce.User.IsLoginInProgress() { -// ce.Reply("Connection not OK, but no error received and login in progress") -// } else { -// ce.Reply("Connection not OK, but no error received") -// } -// } else { -// ce.Reply("Connection to WhatsApp OK") -// } -//} +func (handler *CommandHandler) CommandPing(ce *CommandEvent) { + if ce.User.Session == nil || ce.User.Session.SkypeToken == "" { + if ce.User.IsLoginInProgress() { + ce.Reply("You're not logged into Skype, but there's a login in progress.") + } else { + ce.Reply("You're not logged into Skype.") + } + } else if ce.User.Conn.LoggedIn == false { + ce.Reply("You're not logged into Skype.") + } else { + username := ce.User.Conn.UserProfile.FirstName + if len(ce.User.Conn.UserProfile.LastName) > 0 { + username = username + ce.User.Conn.UserProfile.LastName + } + ce.Reply("You are logged in as @" + username) + } +} const cmdHelpHelp = `help - Prints this help` @@ -464,7 +456,7 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { //cmdPrefix + cmdReconnectHelp, //cmdPrefix + cmdDisconnectHelp, //cmdPrefix + cmdDeleteConnectionHelp, - //cmdPrefix + cmdPingHelp, + cmdPrefix + cmdPingHelp, //cmdPrefix + cmdLoginMatrixHelp, //cmdPrefix + cmdLogoutMatrixHelp, cmdPrefix + cmdSyncHelp, From bada7a88d655e779bc2f07f7555d9111b2eb413b Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 20 Oct 2020 13:50:08 +0800 Subject: [PATCH 024/109] return username after login --- user.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/user.go b/user.go index 6ecaa6d..0f74d01 100644 --- a/user.go +++ b/user.go @@ -305,8 +305,7 @@ func (user *User) IsConnected() bool { } func (user *User) IsLoginInProgress() bool { - //return user.Conn != nil && user.Conn.IsLoginInProgress() - return false + return user.Conn != nil && user.Conn.IsLoginInProgress() } func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- id.EventID) { @@ -371,7 +370,11 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err ce.Reply(err.Error()) return err } - ce.Reply("Successfully logged in") + username := user.Conn.UserProfile.FirstName + if len(user.Conn.UserProfile.LastName) > 0 { + username = username + user.Conn.UserProfile.LastName + } + ce.Reply("Successfully logged in as @" + username) user.Conn.Subscribes() // subscribe basic event err = user.Conn.Conn.ContactList(user.Conn.UserProfile.Username) From 32434ee3af53304cbc890a7b2ae667a3c157931a Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 20 Oct 2020 18:47:43 +0800 Subject: [PATCH 025/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e298aa..4dea87a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@715ec96e1cdd9ed801f2ff9eed81544c0c65729b` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@5afdc90632571f14477a3375f90135abf56a7bfe` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From c12ba3d64e2ac4d5a206ac0f0992701572b4ef2d Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 28 Oct 2020 19:09:07 +0800 Subject: [PATCH 026/109] reply in management room when session expired and remove useless print log --- commands.go | 4 ++-- main.go | 4 ++-- matrix.go | 21 --------------------- user.go | 9 +++++---- 4 files changed, 9 insertions(+), 29 deletions(-) diff --git a/commands.go b/commands.go index 1d27b18..f61ede7 100644 --- a/commands.go +++ b/commands.go @@ -117,7 +117,7 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { handler.CommandLogout(ce) case "login-matrix", "sync", "list", "open", "pm", "invite", "kick", "leave", "join", "create", "share": if !ce.User.HasSession() { - ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") + ce.Reply("You're not logged in. Use the `login` command to log into Skype.") return } @@ -435,7 +435,7 @@ func (handler *CommandHandler) CommandPing(ce *CommandEvent) { if len(ce.User.Conn.UserProfile.LastName) > 0 { username = username + ce.User.Conn.UserProfile.LastName } - ce.Reply("You are logged in as @" + username) + ce.Reply("You're logged in as @" + username) } } diff --git a/main.go b/main.go index 6cd3878..260bce3 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ var ( URL = "unknown" // This is changed when making a release Version = "0.1.1" - WAVersion = "" + SkypeVersion = "" // These are filled at build time with the -X linker flag Tag = "unknown" Commit = "unknown" @@ -43,7 +43,7 @@ func init() { if Tag != Version && !strings.HasSuffix(Version, "+dev") { Version += "+dev" } - WAVersion = strings.FieldsFunc(Version, func(r rune) bool { return r == '-' || r == '+' })[0] + SkypeVersion = strings.FieldsFunc(Version, func(r rune) bool { return r == '-' || r == '+' })[0] } var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String() diff --git a/matrix.go b/matrix.go index 347598a..87b5ebf 100644 --- a/matrix.go +++ b/matrix.go @@ -379,40 +379,19 @@ func (mx *MatrixHandler) HandleEncrypted(evt *event.Event) { } func (mx *MatrixHandler) HandleMessage(evt *event.Event) { - fmt.Println() - fmt.Printf("HandleMessage : %+v", evt) - fmt.Println() if mx.shouldIgnoreEvent(evt) { return } user := mx.bridge.GetUserByMXID(evt.Sender) - fmt.Println() - fmt.Printf("HandleMessage user: %+v", user) - fmt.Println() content := evt.Content.AsMessage() if user.Whitelisted && content.MsgType == event.MsgText { commandPrefix := mx.bridge.Config.Bridge.CommandPrefix - fmt.Println() - fmt.Printf("HandleMessage commandPrefix: %+v", commandPrefix) - fmt.Println() hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix) if hasCommandPrefix { content.Body = strings.TrimLeft(content.Body[len(commandPrefix):], " ") } - fmt.Println() - fmt.Printf("HandleMessage hasCommandPrefix: %+v", hasCommandPrefix) - fmt.Println() - fmt.Println() - fmt.Printf("HandleMessage evt.RoomID0: %+v", evt.RoomID) - fmt.Println() - fmt.Println() - fmt.Printf("HandleMessage user.ManagementRoom: %+v", user.ManagementRoom) - fmt.Println() if hasCommandPrefix || evt.RoomID == user.ManagementRoom { - fmt.Println() - fmt.Printf("HandleMessage commandPrefix: %+v", commandPrefix) - fmt.Println() mx.cmd.Handle(evt.RoomID, user, content.Body) return } diff --git a/user.go b/user.go index 0f74d01..826613a 100644 --- a/user.go +++ b/user.go @@ -237,7 +237,7 @@ func (user *User) SetSession(session *skype.Session) { // return false // } // user.Conn = whatsappExt.ExtendConn(conn) -// _ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", WAVersion) +// _ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", SkypeVersion) // user.log.Debugln("WhatsApp connection successful") // user.Conn.AddHandler(user) // return user.RestoreSession() @@ -263,7 +263,7 @@ func (user *User) Connect(evenIfNoSession bool) bool { return false } user.Conn = skypeExt.ExtendConn(conn) - //_ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", WAVersion) + //_ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", SkypeVersion) user.log.Debugln("skype connection successful") user.Conn.AddHandler(user) return user.RestoreSession() @@ -388,7 +388,7 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err userIds = append(userIds, userId) } ce.User.Conn.SubscribeUsers(userIds) - go loopPresence(user) + go loopPresence(ce, user) } go user.Conn.Poll() @@ -400,9 +400,10 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err return } -func loopPresence(user *User) { +func loopPresence(ce *CommandEvent, user *User) { for { if user.Conn.LoggedIn == false { + ce.Reply("Session expired") break } for cid, contact := range user.contactsPresence { From 488fa5aa1be5858e0162967477faa97ddb941e6c Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 28 Oct 2020 19:11:03 +0800 Subject: [PATCH 027/109] Generate AS registrations with bot sync workaround Refer to tulir commit [47a1d7f] --- config/registration.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/registration.go b/config/registration.go index aa3b054..00922b7 100644 --- a/config/registration.go +++ b/config/registration.go @@ -33,6 +33,12 @@ func (config *Config) NewRegistration() (*appservice.Registration, error) { config.AppService.ASToken = registration.AppToken config.AppService.HSToken = registration.ServerToken + + // Workaround for https://github.com/matrix-org/synapse/pull/5758 + registration.SenderLocalpart = appservice.RandomString(32) + botRegex := regexp.MustCompile(fmt.Sprintf("^@%s:%s$", config.AppService.Bot.Username, config.Homeserver.Domain)) + registration.Namespaces.RegisterUserIDs(botRegex, true) + return registration, nil } From 04b043426efa8c3be68a134adf3995fc61e0fe33 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 4 Nov 2020 14:55:06 +0800 Subject: [PATCH 028/109] Adjust private chat synchronization settings --- commands.go | 3 +++ example-config.yaml | 2 +- portal.go | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/commands.go b/commands.go index f61ede7..7a90cc8 100644 --- a/commands.go +++ b/commands.go @@ -247,6 +247,9 @@ func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { const cmdLogoutHelp = `logout - Logout from Skype` func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { + if ce.User.Conn == nil { + return + } //ce.User.Conn.Conn.LogoutChan <- 1 ce.User.Conn.LoggedIn = false ce.User.Conn.LoginInfo = &skype.Session{ diff --git a/example-config.yaml b/example-config.yaml index 246a70e..6826da5 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -142,7 +142,7 @@ bridge: # Whether or not to explicitly set the avatar and room name for private # chat portal rooms. This can be useful if the previous field works fine, # but causes room avatar/name bugs. - private_chat_portal_meta: false + private_chat_portal_meta: true # Whether or not thumbnails from Skype should be sent. # They're disabled by default due to very low resolution. diff --git a/portal.go b/portal.go index 75234a7..d11633f 100644 --- a/portal.go +++ b/portal.go @@ -360,7 +360,7 @@ func (portal *Portal) handlePrivateChatFromMe(fromMe bool) func() { return func() { if privateChatPuppet != nil && privateChatPuppetInvited { - _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) + //_, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) } } } @@ -908,7 +908,7 @@ func (portal *Portal) beginBackfill() func() { portal.privateChatBackfillInvitePuppet = nil portal.backfillLock.Unlock() if privateChatPuppet != nil && privateChatPuppetInvited { - _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) + //_, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) } } } From 5dcee4ba0345a013a7fea91f7e668a7219041cda Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 4 Nov 2020 17:14:11 +0800 Subject: [PATCH 029/109] fix private chat room name/avatar update bug --- puppet.go | 4 ++-- user.go | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/puppet.go b/puppet.go index bcf5cd7..31543dd 100644 --- a/puppet.go +++ b/puppet.go @@ -272,7 +272,8 @@ func (puppet *Puppet) UpdateName(source *User, contact skype.Contact) bool { func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) { if puppet.bridge.Config.Bridge.PrivateChatPortalMeta { - for _, portal := range puppet.bridge.GetAllPortalsByJID(puppet.JID) { + jid := strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1) + for _, portal := range puppet.bridge.GetAllPortalsByJID(jid) { meta(portal) } } @@ -306,7 +307,6 @@ func (puppet *Puppet) updatePortalName() { } func (puppet *Puppet) Sync(source *User, contact skype.Contact) { - fmt.Println("sync") err := puppet.DefaultIntent().EnsureRegistered() if err != nil { puppet.log.Errorln("Failed to ensure registered:", err) diff --git a/user.go b/user.go index 826613a..cd8bc10 100644 --- a/user.go +++ b/user.go @@ -21,7 +21,6 @@ import ( waProto "github.com/Rhymen/go-whatsapp/binary/proto" "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" "github.com/kelaresg/matrix-skype/database" @@ -969,17 +968,18 @@ func (user *User) HandleCommand(cmd skypeExt.Command) { go portal.UpdateAvatar(user, cmd.ProfilePicInfo) } case skypeExt.CommandDisconnect: - var msg string - if cmd.Kind == "replaced" { - msg = "\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" + - "Use the `reconnect` command to disconnect the other client and resume bridging." - } else { - user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw)) - msg = fmt.Sprintf("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+ - "Use the `reconnect` command to reconnect.", cmd.Kind) - } - user.cleanDisconnection = true - go user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, format.RenderMarkdown(msg, true, false)) + //var msg string + //if cmd.Kind == "replaced" { + // msg = "\u26a0 Your Skype connection was closed by the server because you opened another Skype Web client.\n\n" + + // "Use the `reconnect` command to disconnect the other client and resume bridging." + //} else { + // user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw)) + // msg = fmt.Sprintf("\u26a0 Your Skype connection was closed by the server (reason code: %s).\n\n"+ + // "Use the `reconnect` command to reconnect.", cmd.Kind) + //} + //msg = "can not disconnect" + //user.cleanDisconnection = true + //go user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, format.RenderMarkdown(msg, true, false)) } } From cd537636e8f92ae38ff0f01ffe4564f7c0744d4e Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 4 Nov 2020 19:46:46 +0800 Subject: [PATCH 030/109] better --- matrix.go | 5 ++++- portal.go | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/matrix.go b/matrix.go index 87b5ebf..1374e1c 100644 --- a/matrix.go +++ b/matrix.go @@ -153,7 +153,7 @@ func (mx *MatrixHandler) handlePrivatePortal(roomID id.RoomID, inviter *User, pu return } intent := puppet.DefaultIntent() - _, _ = intent.SendNotice(roomID, "You already have a private chat portal with me at %s") + _, _ = intent.SendNotice(roomID, fmt.Sprintf("You already have a private chat portal with me at %s", roomID)) mx.log.Debugln("Leaving private chat room", roomID, "as", puppet.MXID, "after accepting invite from", inviter.MXID, "as we already have chat with the user") _, _ = intent.LeaveRoom(roomID) } @@ -263,6 +263,9 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { return } isSelf := id.UserID(evt.GetStateKey()) == evt.Sender + fmt.Println("HandleMembership isSelf:", isSelf) + fmt.Println("HandleMembership id.UserID(evt.GetStateKey()):", id.UserID(evt.GetStateKey())) + fmt.Println("HandleMembership evt.Sender:", evt.Sender) if content.Membership == event.MembershipLeave { if id.UserID(evt.GetStateKey()) == evt.Sender { if evt.Unsigned.PrevContent != nil { diff --git a/portal.go b/portal.go index d11633f..f406046 100644 --- a/portal.go +++ b/portal.go @@ -625,12 +625,14 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { portal.log.Warnfln("SyncSkype: SetPowerLevel err: ", err, rep) } - preUserIds,_ := portal.GetMatrixUsers() - for _,userId := range preUserIds { - if user.MXID != userId { - err := portal.tryKickUser(userId, portal.MainIntent()) - if err != nil { - portal.log.Errorln("Failed to try kick user:", err) + if portal.IsPrivateChat() { + preUserIds,_ := portal.GetMatrixUsers() + for _,userId := range preUserIds { + if user.MXID != userId { + err := portal.tryKickUser(userId, portal.MainIntent()) + if err != nil { + portal.log.Errorln("Failed to try kick user:", err) + } } } } From b21edffadda78596f39961dc4547caf9a4592f84 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 5 Nov 2020 19:22:19 +0800 Subject: [PATCH 031/109] Leave all bridged rooms when logging out --- commands.go | 23 ++++++++++++++++++++++- portal.go | 22 +++++++++++----------- user.go | 1 - 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/commands.go b/commands.go index 7a90cc8..88f0589 100644 --- a/commands.go +++ b/commands.go @@ -246,12 +246,18 @@ func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { } const cmdLogoutHelp = `logout - Logout from Skype` + func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { if ce.User.Conn == nil { return } - //ce.User.Conn.Conn.LogoutChan <- 1 ce.User.Conn.LoggedIn = false + username := "" + password := "" + if ce.User.Conn.LoginInfo != nil { + username = ce.User.Conn.LoginInfo.Username + password = ce.User.Conn.LoginInfo.Password + } ce.User.Conn.LoginInfo = &skype.Session{ SkypeToken: "", SkypeExpires: "", @@ -260,9 +266,24 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { RegistrationExpires: "", LocationHost: "", EndpointId: "", + Username: username, + Password: password, + } + portals := ce.User.GetPortals() + leave := func(portal *Portal) { + if len(portal.MXID) > 0 { + _, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ + Reason: "Logout", + UserID: ce.User.MXID, + }) + } + } + for _, portal := range portals { + leave(portal) } ce.Reply("Logged out successfully.") } + // CommandLogout handles !logout command //func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { // if ce.User.Session == nil { diff --git a/portal.go b/portal.go index f406046..d3ad95b 100644 --- a/portal.go +++ b/portal.go @@ -625,17 +625,17 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { portal.log.Warnfln("SyncSkype: SetPowerLevel err: ", err, rep) } - if portal.IsPrivateChat() { - preUserIds,_ := portal.GetMatrixUsers() - for _,userId := range preUserIds { - if user.MXID != userId { - err := portal.tryKickUser(userId, portal.MainIntent()) - if err != nil { - portal.log.Errorln("Failed to try kick user:", err) - } - } - } - } + //if portal.IsPrivateChat() { + // preUserIds,_ := portal.GetMatrixUsers() + // for _,userId := range preUserIds { + // if user.MXID != userId { + // err := portal.tryKickUser(userId, portal.MainIntent()) + // if err != nil { + // portal.log.Errorln("Failed to try kick user:", err) + // } + // } + // } + //} } if portal.IsPrivateChat() { diff --git a/user.go b/user.go index cd8bc10..54aa7a5 100644 --- a/user.go +++ b/user.go @@ -148,7 +148,6 @@ func (user *User) GetPortals() []*Portal { for i, key := range keys { portal, ok := user.bridge.portalsByJID[key] if !ok { - fmt.Println("loadDBPortal4") portal = user.bridge.loadDBPortal(user.bridge.DB.Portal.GetByJID(key), &key) } portals[i] = portal From f0c76006f2a854773be87f628a726a4733f38a55 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Thu, 5 Nov 2020 19:23:30 +0800 Subject: [PATCH 032/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dea87a..d62bb33 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@5afdc90632571f14477a3375f90135abf56a7bfe` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@807a3ad2ec5bdd8f768e701ae5914938ececc2bc` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 0b18b231bb8f4a4ee7d1b6c4503a357ad3e9b1ed Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 6 Nov 2020 19:42:34 +0800 Subject: [PATCH 033/109] Optimize the synchronization of skype group names without topics --- portal.go | 98 +++++++++++++++++++++++++++++++++++++++++-------------- user.go | 30 ++++++++++++++++- 2 files changed, 103 insertions(+), 25 deletions(-) diff --git a/portal.go b/portal.go index d3ad95b..5328fc7 100644 --- a/portal.go +++ b/portal.go @@ -533,19 +533,37 @@ func (portal *Portal) UpdateMetadata(user *User) bool { } portalName := "" + noRoomTopic := false names := strings.Split(metadata.Name, ", ") for _, name := range names { - if strings.Index(name, ":") > 0 { - key := "8:" + name + skypeExt.NewUserSuffix - if key == user.JID { - continue - } - if contact, ok := user.Conn.Store.Contacts[key]; ok { - portalName += contact.DisplayName - } + key := "8:" + name + skypeExt.NewUserSuffix + if key == user.JID { + noRoomTopic = true } } - if len(portalName) < 1 { + if noRoomTopic { + for index, participant := range metadata.Participants { + fmt.Println() + fmt.Printf("metadata.Participants1: %+v", participant) + fmt.Println() + + if participant.JID == user.JID { + continue + } + if contact, ok := user.Conn.Store.Contacts[participant.JID]; ok { + if len(portalName) == 0 { + portalName = contact.DisplayName + } else { + if index > 5 { + portalName = portalName + ", ..." + break + } else { + portalName = portalName + ", " + contact.DisplayName + } + } + } + } + } else { portalName = metadata.Name } // portal.Topic = "" @@ -1085,25 +1103,36 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { metadata, err = user.Conn.GetGroupMetaData(portal.Key.JID) if err == nil { portalName := "" + noRoomTopic := false names := strings.Split(metadata.Name, ", ") for _, name := range names { - if strings.Index(name, ":") > 0 { - key := "8:" + name + skypeExt.NewUserSuffix - if key == user.JID { - continue - } - if contact, ok := user.Conn.Store.Contacts[key]; ok { - if len(portalName) > 0 { - portalName = portalName + ", " + contact.DisplayName - } else { - portalName = contact.DisplayName - } - } - } else if name == "..." { - portalName = portalName + ", ..." + key := "8:" + name + skypeExt.NewUserSuffix + if key == user.JID { + noRoomTopic = true } } - if len(portalName) > 0 { + if noRoomTopic { + for index, participant := range metadata.Participants { + fmt.Println() + fmt.Printf("metadata.Participants2: %+v", participant) + fmt.Println() + + if participant.JID == user.JID { + continue + } + if contact, ok := user.Conn.Store.Contacts[participant.JID]; ok { + if len(portalName) == 0 { + portalName = contact.DisplayName + } else { + if index > 5 { + portalName = portalName + ", ..." + break + } else { + portalName = portalName + ", " + contact.DisplayName + } + } + } + } portal.Name = portalName } else { portal.Name = metadata.Name @@ -2391,6 +2420,27 @@ func (portal *Portal) GetMatrixUsers() ([]id.UserID, error) { return users, nil } +func (portal *Portal) GetPuppets() ([]struct { + DisplayName *string `json:"display_name"` + AvatarURL *string `json:"avatar_url"` +}, error) { + members, err := portal.MainIntent().JoinedMembers(portal.MXID) + if err != nil { + return nil, errors.Wrap(err, "failed to get member list") + } + var puppets []struct { + DisplayName *string `json:"display_name"` + AvatarURL *string `json:"avatar_url"` + } + for userID := range members.Joined { + _, isPuppet := portal.bridge.ParsePuppetMXID(userID) + if isPuppet && userID != portal.bridge.Bot.UserID { + puppets = append(puppets, members.Joined[userID]) + } + } + return puppets, nil +} + func (portal *Portal) CleanupIfEmpty() { users, err := portal.GetMatrixUsers() if err != nil { diff --git a/user.go b/user.go index 54aa7a5..f567ffe 100644 --- a/user.go +++ b/user.go @@ -1007,8 +1007,36 @@ func (user *User) HandleChatUpdate(cmd skype.Resource) { topicContent := skype.ChatTopicContent{} //把xml数据解析成bs对象 xml.Unmarshal([]byte(cmd.Content), &topicContent) + portalName := "" + noRoomTopic := false + names := strings.Split(cmd.ThreadTopic, ", ") + for _, name := range names { + key := "8:" + name + skypeExt.NewUserSuffix + if key == user.JID { + noRoomTopic = true + } + } + if noRoomTopic { + participants, _ := portal.GetPuppets() + for index, participant := range participants { + if *participant.DisplayName != user.Conn.LoginInfo.Username { + if len(portalName) == 0 { + portalName = *participant.DisplayName + } else { + if index > 5 { + portalName = portalName + ", ..." + break + } else { + portalName = *participant.DisplayName + ", " + portalName + } + } + } + } + } else { + portalName = cmd.ThreadTopic + } cmd.SendId = topicContent.Initiator + skypeExt.NewUserSuffix - go portal.UpdateName(cmd.ThreadTopic, cmd.SendId) + go portal.UpdateName(portalName, cmd.SendId) case skypeExt.ChatPictureUpdate: topicContent := skype.ChatPictureContent{} //把xml数据解析成bs对象 From 27b870d1ca0cc6d2f784b1e47929bee457e07bb1 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sat, 7 Nov 2020 00:42:40 +0800 Subject: [PATCH 034/109] update cypto --- config/bridge.go | 6 + crypto.go | 82 +++- database/cryptostore.go | 420 ++---------------- .../2020-07-10-update-crypto-store.go | 13 + .../2020-08-03-update-crypto-store.go | 13 + .../upgrades/2020-08-25-message-id-column.go | 2 +- database/upgrades/upgrades.go | 13 +- formatting.go | 15 +- go.mod | 11 +- go.sum | 91 ++++ main.go | 2 + 11 files changed, 260 insertions(+), 408 deletions(-) create mode 100644 database/upgrades/2020-07-10-update-crypto-store.go create mode 100644 database/upgrades/2020-08-03-update-crypto-store.go diff --git a/config/bridge.go b/config/bridge.go index b93eb50..0c4e019 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -55,6 +55,12 @@ type BridgeConfig struct { Encryption struct { Allow bool `yaml:"allow"` Default bool `yaml:"default"` + + KeySharing struct { + Allow bool `yaml:"allow"` + RequireCrossSigning bool `yaml:"require_cross_signing"` + RequireVerification bool `yaml:"require_verification"` + } `yaml:"key_sharing"` } `yaml:"encryption"` Permissions PermissionConfig `yaml:"permissions"` diff --git a/crypto.go b/crypto.go index 65f728b..194872a 100644 --- a/crypto.go +++ b/crypto.go @@ -1,4 +1,17 @@ -// +build cgo +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build cgo,!nocrypto package main @@ -13,10 +26,11 @@ import ( "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" - "github.com/kelaresg/matrix-skype/database" "maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" + + "github.com/kelaresg/matrix-skype/database" ) var levelTrace = maulogger.Level{ @@ -52,6 +66,10 @@ func NewCryptoHelper(bridge *Bridge) Crypto { func (helper *CryptoHelper) Init() error { helper.log.Debugln("Initializing end-to-bridge encryption...") + + helper.store = database.NewSQLCryptoStore(helper.bridge.DB, helper.bridge.AS.BotMXID(), + fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.AS.HomeserverDomain)) + var err error helper.client, err = helper.loginBot() if err != nil { @@ -61,10 +79,8 @@ func (helper *CryptoHelper) Init() error { helper.log.Debugln("Logged in as bridge bot with device ID", helper.client.DeviceID) logger := &cryptoLogger{helper.baseLog} stateStore := &cryptoStateStore{helper.bridge} - helper.store = database.NewSQLCryptoStore(helper.bridge.DB, helper.client.DeviceID) - helper.store.UserID = helper.client.UserID - helper.store.GhostIDFormat = fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.AS.HomeserverDomain) helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore) + helper.mach.AllowKeyShare = helper.allowKeyShare helper.client.Logger = logger.int.Sub("Bot") helper.client.Syncer = &cryptoSyncer{helper.mach} @@ -73,28 +89,55 @@ func (helper *CryptoHelper) Init() error { return helper.mach.Load() } +func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info event.RequestedKeyInfo) *crypto.KeyShareRejection { + cfg := helper.bridge.Config.Bridge.Encryption.KeySharing + if !cfg.Allow { + return &crypto.KeyShareRejectNoResponse + } else if device.Trust == crypto.TrustStateBlacklisted { + return &crypto.KeyShareRejectBlacklisted + } else if device.Trust == crypto.TrustStateVerified || !cfg.RequireVerification { + portal := helper.bridge.GetPortalByMXID(info.RoomID) + if portal == nil { + helper.log.Debugfln("Rejecting key request for %s from %s/%s: room is not a portal", info.SessionID, device.UserID, device.DeviceID) + return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"} + } + user := helper.bridge.GetUserByMXID(device.UserID) + if !user.IsInPortal(portal.Key) { + helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID) + return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"} + } + helper.log.Debugfln("Accepting key request for %s from %s/%s", info.SessionID, device.UserID, device.DeviceID) + return nil + } else { + return &crypto.KeyShareRejectUnverified + } +} + func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { - deviceID := helper.bridge.DB.FindDeviceID() + deviceID := helper.store.FindDeviceID() if len(deviceID) > 0 { helper.log.Debugln("Found existing device ID for bot in database:", deviceID) } mac := hmac.New(sha512.New, []byte(helper.bridge.Config.Bridge.LoginSharedSecret)) mac.Write([]byte(helper.bridge.AS.BotMXID())) - resp, err := helper.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ - Type: "m.login.password", - Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(helper.bridge.AS.BotMXID())}, + client, err := mautrix.NewClient(helper.bridge.AS.HomeserverURL, "", "") + if err != nil { + return nil, err + } + resp, err := client.Login(&mautrix.ReqLogin{ + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, Password: hex.EncodeToString(mac.Sum(nil)), DeviceID: deviceID, - InitialDeviceDisplayName: "Skype Bridge", + InitialDeviceDisplayName: "WhatsApp Bridge", + StoreCredentials: true, }) if err != nil { return nil, err } - client, err := mautrix.NewClient(helper.bridge.AS.HomeserverURL, helper.bridge.AS.BotMXID(), resp.AccessToken) - if err != nil { - return nil, err + if len(deviceID) == 0 { + helper.store.DeviceID = resp.DeviceID } - client.DeviceID = resp.DeviceID return client, nil } @@ -115,7 +158,7 @@ func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) { } func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) { - encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, content) + encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, &content) if err != nil { if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession { return nil, err @@ -129,7 +172,7 @@ func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, conten if err != nil { return nil, errors.Wrap(err, "failed to share group session") } - encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, content) + encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, &content) if err != nil { return nil, errors.Wrap(err, "failed to encrypt event after re-sharing group session") } @@ -213,6 +256,8 @@ type cryptoStateStore struct { bridge *Bridge } +var _ crypto.StateStore = (*cryptoStateStore)(nil) + func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool { portal := c.bridge.GetPortalByMXID(id) if portal != nil { @@ -224,3 +269,8 @@ func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool { func (c *cryptoStateStore) FindSharedRooms(id id.UserID) []id.RoomID { return c.bridge.StateStore.FindSharedRooms(id) } + +func (c *cryptoStateStore) GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent { + // TODO implement + return nil +} diff --git a/database/cryptostore.go b/database/cryptostore.go index e0fe724..7dd49e9 100644 --- a/database/cryptostore.go +++ b/database/cryptostore.go @@ -1,56 +1,58 @@ -// +build cgo +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build cgo,!nocrypto package database import ( "database/sql" - "fmt" - "strings" - "github.com/lib/pq" - "github.com/pkg/errors" log "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix/crypto" - "maunium.net/go/mautrix/crypto/olm" "maunium.net/go/mautrix/id" ) type SQLCryptoStore struct { - db *Database - log log.Logger - - UserID id.UserID - DeviceID id.DeviceID - SyncToken string - PickleKey []byte - Account *crypto.OlmAccount - + *crypto.SQLCryptoStore + UserID id.UserID GhostIDFormat string } var _ crypto.Store = (*SQLCryptoStore)(nil) -func NewSQLCryptoStore(db *Database, deviceID id.DeviceID) *SQLCryptoStore { +func NewSQLCryptoStore(db *Database, userID id.UserID, ghostIDFormat string) *SQLCryptoStore { return &SQLCryptoStore{ - db: db, - log: db.log.Sub("CryptoStore"), - PickleKey: []byte("github.com/kelaresg/matrix-skype"), - DeviceID: deviceID, + SQLCryptoStore: crypto.NewSQLCryptoStore(db.DB, db.dialect, "", "", + []byte("maunium.net/go/mautrix-whatsapp"), + &cryptoLogger{db.log.Sub("CryptoStore")}), + UserID: userID, + GhostIDFormat: ghostIDFormat, } } -func (db *Database) FindDeviceID() (deviceID id.DeviceID) { - err := db.QueryRow("SELECT device_id FROM crypto_account LIMIT 1").Scan(&deviceID) +func (store *SQLCryptoStore) FindDeviceID() (deviceID id.DeviceID) { + err := store.DB.QueryRow("SELECT device_id FROM crypto_account WHERE account_id=$1", store.AccountID).Scan(&deviceID) if err != nil && err != sql.ErrNoRows { - db.log.Warnln("Failed to scan device ID:", err) + store.Log.Warn("Failed to scan device ID: %v", err) } return } func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) (members []id.UserID, err error) { var rows *sql.Rows - rows, err = store.db.Query(` + rows, err = store.DB.Query(` SELECT user_id FROM mx_user_profile WHERE room_id=$1 AND (membership='join' OR membership='invite') @@ -64,7 +66,7 @@ func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) (members []id.User var userID id.UserID err := rows.Scan(&userID) if err != nil { - store.log.Warnfln("Failed to scan member in %s: %v", roomID, err) + store.Log.Warn("Failed to scan member in %s: %v", roomID, err) } else { members = append(members, userID) } @@ -72,367 +74,29 @@ func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) (members []id.User return } -func (store *SQLCryptoStore) Flush() error { - return nil +// TODO merge this with the one in the parent package +type cryptoLogger struct { + int log.Logger } -func (store *SQLCryptoStore) PutNextBatch(nextBatch string) { - store.SyncToken = nextBatch - _, err := store.db.Exec(`UPDATE crypto_account SET sync_token=$1 WHERE device_id=$2`, store.SyncToken, store.DeviceID) - if err != nil { - store.log.Warnln("Failed to store sync token:", err) - } +var levelTrace = log.Level{ + Name: "Trace", + Severity: -10, + Color: -1, } -func (store *SQLCryptoStore) GetNextBatch() string { - if store.SyncToken == "" { - err := store.db. - QueryRow("SELECT sync_token FROM crypto_account WHERE device_id=$1", store.DeviceID). - Scan(&store.SyncToken) - if err != nil && err != sql.ErrNoRows { - store.log.Warnln("Failed to scan sync token:", err) - } - } - return store.SyncToken +func (c *cryptoLogger) Error(message string, args ...interface{}) { + c.int.Errorfln(message, args...) } -func (store *SQLCryptoStore) PutAccount(account *crypto.OlmAccount) error { - store.Account = account - bytes := account.Internal.Pickle(store.PickleKey) - var err error - if store.db.dialect == "postgres" { - _, err = store.db.Exec(` - INSERT INTO crypto_account (device_id, shared, sync_token, account) VALUES ($1, $2, $3, $4) - ON CONFLICT (device_id) DO UPDATE SET shared=$2, sync_token=$3, account=$4`, - store.DeviceID, account.Shared, store.SyncToken, bytes) - } else if store.db.dialect == "sqlite3" { - _, err = store.db.Exec("INSERT OR REPLACE INTO crypto_account (device_id, shared, sync_token, account) VALUES ($1, $2, $3, $4)", - store.DeviceID, account.Shared, store.SyncToken, bytes) - } else { - err = fmt.Errorf("unsupported dialect %s", store.db.dialect) - } - if err != nil { - store.log.Warnln("Failed to store account:", err) - } - return nil +func (c *cryptoLogger) Warn(message string, args ...interface{}) { + c.int.Warnfln(message, args...) } -func (store *SQLCryptoStore) GetAccount() (*crypto.OlmAccount, error) { - if store.Account == nil { - row := store.db.QueryRow("SELECT shared, sync_token, account FROM crypto_account WHERE device_id=$1", store.DeviceID) - acc := &crypto.OlmAccount{Internal: *olm.NewBlankAccount()} - var accountBytes []byte - err := row.Scan(&acc.Shared, &store.SyncToken, &accountBytes) - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - err = acc.Internal.Unpickle(accountBytes, store.PickleKey) - if err != nil { - return nil, err - } - store.Account = acc - } - return store.Account, nil +func (c *cryptoLogger) Debug(message string, args ...interface{}) { + c.int.Debugfln(message, args...) } -func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool { - // TODO this may need to be changed if olm sessions start expiring - var sessionID id.SessionID - err := store.db.QueryRow("SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 LIMIT 1", key).Scan(&sessionID) - if err == sql.ErrNoRows { - return false - } - return len(sessionID) > 0 -} - -func (store *SQLCryptoStore) GetSessions(key id.SenderKey) (crypto.OlmSessionList, error) { - rows, err := store.db.Query("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 ORDER BY session_id", key) - if err != nil { - return nil, err - } - list := crypto.OlmSessionList{} - for rows.Next() { - sess := crypto.OlmSession{Internal: *olm.NewBlankSession()} - var sessionBytes []byte - err := rows.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime) - if err != nil { - return nil, err - } - err = sess.Internal.Unpickle(sessionBytes, store.PickleKey) - if err != nil { - return nil, err - } - list = append(list, &sess) - } - return list, nil -} - -func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*crypto.OlmSession, error) { - row := store.db.QueryRow("SELECT session, created_at, last_used FROM crypto_olm_session WHERE sender_key=$1 ORDER BY session_id DESC LIMIT 1", key) - sess := crypto.OlmSession{Internal: *olm.NewBlankSession()} - var sessionBytes []byte - err := row.Scan(&sessionBytes, &sess.CreationTime, &sess.UseTime) - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - return &sess, sess.Internal.Unpickle(sessionBytes, store.PickleKey) -} - -func (store *SQLCryptoStore) AddSession(key id.SenderKey, session *crypto.OlmSession) error { - sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.db.Exec("INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_used) VALUES ($1, $2, $3, $4, $5)", - session.ID(), key, sessionBytes, session.CreationTime, session.UseTime) - return err -} - -func (store *SQLCryptoStore) UpdateSession(key id.SenderKey, session *crypto.OlmSession) error { - sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.db.Exec("UPDATE crypto_olm_session SET session=$1, last_used=$2 WHERE session_id=$3", - sessionBytes, session.UseTime, session.ID()) - return err -} - -func (store *SQLCryptoStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, session *crypto.InboundGroupSession) error { - sessionBytes := session.Internal.Pickle(store.PickleKey) - forwardingChains := strings.Join(session.ForwardingChains, ",") - _, err := store.db.Exec("INSERT INTO crypto_megolm_inbound_session (session_id, sender_key, signing_key, room_id, session, forwarding_chains) VALUES ($1, $2, $3, $4, $5, $6)", - sessionID, senderKey, session.SigningKey, roomID, sessionBytes, forwardingChains) - return err -} - -func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*crypto.InboundGroupSession, error) { - var signingKey id.Ed25519 - var sessionBytes []byte - var forwardingChains string - err := store.db.QueryRow(` - SELECT signing_key, session, forwarding_chains - FROM crypto_megolm_inbound_session - WHERE room_id=$1 AND sender_key=$2 AND session_id=$3`, - roomID, senderKey, sessionID, - ).Scan(&signingKey, &sessionBytes, &forwardingChains) - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - igs := olm.NewBlankInboundGroupSession() - err = igs.Unpickle(sessionBytes, store.PickleKey) - if err != nil { - return nil, err - } - return &crypto.InboundGroupSession{ - Internal: *igs, - SigningKey: signingKey, - SenderKey: senderKey, - RoomID: roomID, - ForwardingChains: strings.Split(forwardingChains, ","), - }, nil -} - -func (store *SQLCryptoStore) AddOutboundGroupSession(session *crypto.OutboundGroupSession) (err error) { - sessionBytes := session.Internal.Pickle(store.PickleKey) - if store.db.dialect == "postgres" { - _, err = store.db.Exec(` - INSERT INTO crypto_megolm_outbound_session ( - room_id, session_id, session, shared, max_messages, message_count, max_age, created_at, last_used - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - ON CONFLICT (room_id) DO UPDATE SET session_id=$2, session=$3, shared=$4, max_messages=$5, message_count=$6, max_age=$7, created_at=$8, last_used=$9`, - session.RoomID, session.ID(), sessionBytes, session.Shared, session.MaxMessages, session.MessageCount, session.MaxAge, session.CreationTime, session.UseTime) - } else if store.db.dialect == "sqlite3" { - _, err = store.db.Exec(` - INSERT OR REPLACE INTO crypto_megolm_outbound_session ( - room_id, session_id, session, shared, max_messages, message_count, max_age, created_at, last_used - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - session.RoomID, session.ID(), sessionBytes, session.Shared, session.MaxMessages, session.MessageCount, session.MaxAge, session.CreationTime, session.UseTime) - } else { - err = fmt.Errorf("unsupported dialect %s", store.db.dialect) - } - return -} - -func (store *SQLCryptoStore) UpdateOutboundGroupSession(session *crypto.OutboundGroupSession) error { - sessionBytes := session.Internal.Pickle(store.PickleKey) - _, err := store.db.Exec("UPDATE crypto_megolm_outbound_session SET session=$1, message_count=$2, last_used=$3 WHERE room_id=$4 AND session_id=$5", - sessionBytes, session.MessageCount, session.UseTime, session.RoomID, session.ID()) - return err -} - -func (store *SQLCryptoStore) GetOutboundGroupSession(roomID id.RoomID) (*crypto.OutboundGroupSession, error) { - var ogs crypto.OutboundGroupSession - var sessionBytes []byte - err := store.db.QueryRow(` - SELECT session, shared, max_messages, message_count, max_age, created_at, last_used - FROM crypto_megolm_outbound_session WHERE room_id=$1`, - roomID, - ).Scan(&sessionBytes, &ogs.Shared, &ogs.MaxMessages, &ogs.MessageCount, &ogs.MaxAge, &ogs.CreationTime, &ogs.UseTime) - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - intOGS := olm.NewBlankOutboundGroupSession() - err = intOGS.Unpickle(sessionBytes, store.PickleKey) - if err != nil { - return nil, err - } - ogs.Internal = *intOGS - ogs.RoomID = roomID - return &ogs, nil -} - -func (store *SQLCryptoStore) RemoveOutboundGroupSession(roomID id.RoomID) error { - _, err := store.db.Exec("DELETE FROM crypto_megolm_outbound_session WHERE room_id=$1", roomID) - return err -} - -func (store *SQLCryptoStore) ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) bool { - var resultEventID id.EventID - var resultTimestamp int64 - err := store.db.QueryRow( - `SELECT event_id, timestamp FROM crypto_message_index WHERE sender_key=$1 AND session_id=$2 AND "index"=$3`, - senderKey, sessionID, index, - ).Scan(&resultEventID, &resultTimestamp) - if err == sql.ErrNoRows { - _, err := store.db.Exec(`INSERT INTO crypto_message_index (sender_key, session_id, "index", event_id, timestamp) VALUES ($1, $2, $3, $4, $5)`, - senderKey, sessionID, index, eventID, timestamp) - if err != nil { - store.log.Warnln("Failed to store message index:", err) - } - return true - } else if err != nil { - store.log.Warnln("Failed to scan message index:", err) - return true - } - if resultEventID != eventID || resultTimestamp != timestamp { - return false - } - return true -} - -func (store *SQLCryptoStore) GetDevices(userID id.UserID) (map[id.DeviceID]*crypto.DeviceIdentity, error) { - var ignore id.UserID - err := store.db.QueryRow("SELECT user_id FROM crypto_tracked_user WHERE user_id=$1", userID).Scan(&ignore) - if err == sql.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, err - } - - rows, err := store.db.Query("SELECT device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1", userID) - if err != nil { - return nil, err - } - data := make(map[id.DeviceID]*crypto.DeviceIdentity) - for rows.Next() { - var identity crypto.DeviceIdentity - err := rows.Scan(&identity.DeviceID, &identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) - if err != nil { - return nil, err - } - identity.UserID = userID - data[identity.DeviceID] = &identity - } - return data, nil -} - -func (store *SQLCryptoStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*crypto.DeviceIdentity, error) { - var identity crypto.DeviceIdentity - err := store.db.QueryRow(` - SELECT identity_key, signing_key, trust, deleted, name - FROM crypto_device WHERE user_id=$1 AND device_id=$2`, - userID, deviceID, - ).Scan(&identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name) - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } - return nil, err - } - return &identity, nil -} - -func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*crypto.DeviceIdentity) error { - tx, err := store.db.Begin() - if err != nil { - return err - } - - if store.db.dialect == "postgres" { - _, err = tx.Exec("INSERT INTO crypto_tracked_user (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) - } else if store.db.dialect == "sqlite3" { - _, err = tx.Exec("INSERT OR IGNORE INTO crypto_tracked_user (user_id) VALUES ($1)", userID) - } else { - err = fmt.Errorf("unsupported dialect %s", store.db.dialect) - } - if err != nil { - return errors.Wrap(err, "failed to add user to tracked users list") - } - - _, err = tx.Exec("DELETE FROM crypto_device WHERE user_id=$1", userID) - if err != nil { - _ = tx.Rollback() - return errors.Wrap(err, "failed to delete old devices") - } - if len(devices) == 0 { - err = tx.Commit() - if err != nil { - return errors.Wrap(err, "failed to commit changes (no devices added)") - } - return nil - } - // TODO do this in batches to avoid too large db queries - values := make([]interface{}, 1, len(devices)*6+1) - values[0] = userID - valueStrings := make([]string, 0, len(devices)) - i := 2 - for deviceID, identity := range devices { - values = append(values, deviceID, identity.IdentityKey, identity.SigningKey, identity.Trust, identity.Deleted, identity.Name) - valueStrings = append(valueStrings, fmt.Sprintf("($1, $%d, $%d, $%d, $%d, $%d, $%d)", i, i+1, i+2, i+3, i+4, i+5)) - i += 6 - } - valueString := strings.Join(valueStrings, ",") - _, err = tx.Exec("INSERT INTO crypto_device (user_id, device_id, identity_key, signing_key, trust, deleted, name) VALUES "+valueString, values...) - if err != nil { - _ = tx.Rollback() - return errors.Wrap(err, "failed to insert new devices") - } - err = tx.Commit() - if err != nil { - return errors.Wrap(err, "failed to commit changes") - } - return nil -} - -func (store *SQLCryptoStore) FilterTrackedUsers(users []id.UserID) []id.UserID { - var rows *sql.Rows - var err error - if store.db.dialect == "postgres" { - rows, err = store.db.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id = ANY($1)", pq.Array(users)) - } else { - queryString := make([]string, len(users)) - params := make([]interface{}, len(users)) - for i, user := range users { - queryString[i] = fmt.Sprintf("$%d", i+1) - params[i] = user - } - rows, err = store.db.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id IN ("+strings.Join(queryString, ",")+")", params...) - } - if err != nil { - store.log.Warnln("Failed to filter tracked users:", err) - return users - } - var ptr int - for rows.Next() { - err = rows.Scan(&users[ptr]) - if err != nil { - store.log.Warnln("Failed to tracked user ID:", err) - } else { - ptr++ - } - } - return users[:ptr] +func (c *cryptoLogger) Trace(message string, args ...interface{}) { + c.int.Logfln(levelTrace, message, args...) } diff --git a/database/upgrades/2020-07-10-update-crypto-store.go b/database/upgrades/2020-07-10-update-crypto-store.go new file mode 100644 index 0000000..6b6d44d --- /dev/null +++ b/database/upgrades/2020-07-10-update-crypto-store.go @@ -0,0 +1,13 @@ +package upgrades + +import ( + "database/sql" + + "maunium.net/go/mautrix/crypto/sql_store_upgrade" +) + +func init() { + upgrades[15] = upgrade{"Add account_id to crypto store", func(tx *sql.Tx, c context) error { + return sql_store_upgrade.Upgrades[1](tx, c.dialect.String()) + }} +} diff --git a/database/upgrades/2020-08-03-update-crypto-store.go b/database/upgrades/2020-08-03-update-crypto-store.go new file mode 100644 index 0000000..4322e59 --- /dev/null +++ b/database/upgrades/2020-08-03-update-crypto-store.go @@ -0,0 +1,13 @@ +package upgrades + +import ( + "database/sql" + + "maunium.net/go/mautrix/crypto/sql_store_upgrade" +) + +func init() { + upgrades[16] = upgrade{"Add megolm withheld data to crypto store", func(tx *sql.Tx, c context) error { + return sql_store_upgrade.Upgrades[2](tx, c.dialect.String()) + }} +} diff --git a/database/upgrades/2020-08-25-message-id-column.go b/database/upgrades/2020-08-25-message-id-column.go index 79139f2..a74f824 100644 --- a/database/upgrades/2020-08-25-message-id-column.go +++ b/database/upgrades/2020-08-25-message-id-column.go @@ -5,7 +5,7 @@ import ( ) func init() { - upgrades[15] = upgrade{"Add id column to messages", func(tx *sql.Tx, ctx context) error { + upgrades[17] = upgrade{"Add id column to messages", func(tx *sql.Tx, ctx context) error { _, err := tx.Exec(`ALTER TABLE message ADD COLUMN id CHAR(13) DEFAULT ""`) if err != nil { return err diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go index 9b1d572..f020d94 100644 --- a/database/upgrades/upgrades.go +++ b/database/upgrades/upgrades.go @@ -15,6 +15,17 @@ const ( SQLite ) +func (dialect Dialect) String() string { + switch dialect { + case Postgres: + return "postgres" + case SQLite: + return "sqlite3" + default: + return "" + } +} + type upgradeFunc func(*sql.Tx, context) error type context struct { @@ -28,7 +39,7 @@ type upgrade struct { fn upgradeFunc } -const NumberOfUpgrades = 16 +const NumberOfUpgrades = 18 var upgrades [NumberOfUpgrades]upgrade diff --git a/formatting.go b/formatting.go index a3ca57d..22e0ff6 100644 --- a/formatting.go +++ b/formatting.go @@ -39,7 +39,7 @@ func NewFormatter(bridge *Bridge) *Formatter { TabsToSpaces: 4, Newline: "\n", - PillConverter: func(mxid, eventID string) string { + PillConverter: func(mxid, eventID string, ctx format.Context) string { if mxid[0] == '@' { puppet := bridge.GetPuppetByMXID(id.UserID(mxid)) if puppet != nil { @@ -48,19 +48,19 @@ func NewFormatter(bridge *Bridge) *Formatter { } return mxid }, - BoldConverter: func(text string) string { + BoldConverter: func(text string, _ format.Context) string { return fmt.Sprintf("*%s*", text) }, - ItalicConverter: func(text string) string { + ItalicConverter: func(text string, _ format.Context) string { return fmt.Sprintf("_%s_", text) }, - StrikethroughConverter: func(text string) string { + StrikethroughConverter: func(text string, _ format.Context) string { return fmt.Sprintf("~%s~", text) }, - MonospaceConverter: func(text string) string { + MonospaceConverter: func(text string, _ format.Context) string { return fmt.Sprintf("```%s```", text) }, - MonospaceBlockConverter: func(text, language string) string { + MonospaceBlockConverter: func(text, language string, _ format.Context) string { return fmt.Sprintf("```%s```", text) }, }, @@ -166,5 +166,6 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { } func (formatter *Formatter) ParseMatrix(html string) string { - return formatter.matrixHTMLParser.Parse(html) + ctx := make(format.Context) + return formatter.matrixHTMLParser.Parse(html, ctx) } diff --git a/go.mod b/go.mod index f15c092..6d043b9 100644 --- a/go.mod +++ b/go.mod @@ -7,19 +7,20 @@ require ( github.com/chai2010/webp v1.1.0 github.com/gorilla/websocket v1.4.2 github.com/kelaresg/go-skypeapi v0.1.2-0.20200828122051-fb22fb75dede - github.com/lib/pq v1.5.2 + github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 - golang.org/x/image v0.0.0-20200430140353-33d19683fad8 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + golang.org/x/image v0.0.0-20200618115811-c13761719519 gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.1.1 - maunium.net/go/mautrix v0.5.0-rc.3 + maunium.net/go/mautrix v0.7.2 ) replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8 -replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4 +replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d +replace github.com/kelaresg/go-skypeapi => /Users/yangguang/matrix/go-skypeapi-kelare diff --git a/go.sum b/go.sum index e5fce9a..0c69ab8 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,11 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -26,9 +29,21 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ= github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOYvd3Ow= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= @@ -39,6 +54,7 @@ github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -51,6 +67,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -70,12 +87,22 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -117,14 +144,19 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -134,12 +166,15 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= +github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -152,17 +187,27 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4 h1:FkwHv3b/K6jsNo8rFk7VCsdJ7HL61AcjqAS0DqDYLyk= github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= +github.com/pidongqianqian/mautrix-go v0.7.2/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= +github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106123139-e1c6c37e09d6 h1:RKxi11Gkg48exSKQSFBYGduK5xfJ72MdcaeMMfsMF4w= +github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106123139-e1c6c37e09d6/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= +github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d h1:cw6XWBIvj8DaRO/rWzndd4q5Yzbei6Sgj/UUPHuCWv4= +github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -172,12 +217,19 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -188,8 +240,11 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -210,6 +265,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= @@ -233,6 +289,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= @@ -240,6 +297,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -249,6 +310,7 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -275,10 +337,14 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs= +golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -287,26 +353,36 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -327,6 +403,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -348,18 +425,30 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= @@ -372,4 +461,6 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +maunium.net/go/mautrix-whatsapp v0.1.4 h1:qlkb3eXcKm1QE6AjrAl9aKxokHlwj7BNr+aUQFXFmWE= +maunium.net/go/mautrix-whatsapp v0.1.4/go.mod h1:yC5pjdUQckJzuMX5rrg2237kz/7zP+7qO1uckOODe5M= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/main.go b/main.go index 260bce3..9bc4dda 100644 --- a/main.go +++ b/main.go @@ -244,6 +244,8 @@ func (bridge *Bridge) Start() { bridge.Log.Fatalln("Failed to initialize database:", err) os.Exit(15) } + bridge.Log.Debugln("Checking connection to homeserver") + bridge.ensureConnection() if bridge.Crypto != nil { err := bridge.Crypto.Init() if err != nil { From fdd06e814552a7e8ab21075c73e7b0c5a152375c Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 17 Nov 2020 20:40:20 +0800 Subject: [PATCH 035/109] Fix the bug that cannot bridge address message and contact message --- portal.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/portal.go b/portal.go index 5328fc7..d1dfedc 100644 --- a/portal.go +++ b/portal.go @@ -1483,7 +1483,7 @@ func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Res longitude, _:= strconv.Atoi(locationMessage.Longitude) geo := fmt.Sprintf("geo:%.6f,%.6f", float32(latitude)/1000000, float32(longitude)/1000000) content := &event.MessageEventContent{ - MsgType: event.MsgText, //event.MsgLocation, + MsgType: event.MsgText, Body: fmt.Sprintf("Location: %s%s
", locationMessage.A.Href, locationMessage.Address, geo), Format: event.FormatHTML, FormattedBody: fmt.Sprintf("Location: %s%s
", locationMessage.A.Href, locationMessage.Address, geo), @@ -1494,8 +1494,7 @@ func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Res portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) - t, _ := time.Parse(time.RFC3339,message.ComposeTime) - resp, err := portal.sendMessage(intent, event.EventMessage, content, t.Unix()) + resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) return @@ -1519,14 +1518,13 @@ func (portal *Portal) HandleContactMessageSkype(source *User, message skype.Reso content := &event.MessageEventContent{ Body: fmt.Sprintf("%s\n%s", contactMessage.C.F, contactMessage.C.S), - MsgType: "m.contact",//event.MsgText, + MsgType: event.MsgText, } portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) - t, _ := time.Parse(time.RFC3339,message.ComposeTime) - resp, err := portal.sendMessage(intent, event.EventMessage, content, t.Unix()) + resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) return From adf4890fbb659e13eb7ac0f3483e55af8f80a3e7 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 18 Nov 2020 18:14:00 +0800 Subject: [PATCH 036/109] Optimize JoinRoomByID in case of network delay --- matrix.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/matrix.go b/matrix.go index 1374e1c..b6b3834 100644 --- a/matrix.go +++ b/matrix.go @@ -7,6 +7,7 @@ import ( "maunium.net/go/mautrix" "strconv" "strings" + "time" "maunium.net/go/maulogger/v2" @@ -57,8 +58,12 @@ func (mx *MatrixHandler) HandleEncryption(evt *event.Event) { func (mx *MatrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { resp, err := intent.JoinRoomByID(evt.RoomID) if err != nil { - mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err) - return nil + time.Sleep(3 * time.Second) + resp, err = intent.JoinRoomByID(evt.RoomID) + if err != nil { + mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err) + return nil + } } members, err := intent.JoinedMembers(resp.RoomID) From 5cf98abe9c06b7f956a37c03e8d367a140de99b4 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 23 Nov 2020 16:14:03 +0800 Subject: [PATCH 037/109] upload skype bot avatar --- example-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example-config.yaml b/example-config.yaml index 6826da5..78c8cfc 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -43,7 +43,7 @@ appservice: # Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty # to leave display name/avatar as-is. displayname: Skype bridge bot - avatar: https://secure.skypeassets.com/wcss/8-61-0-80/images/favicons/favicon.ico + avatar: mxc://matrix.org/kGQUDQyPiwbRXPFkjoBrPyhC # Authentication tokens for AS <-> HS communication. Autogenerated; do not modify. as_token: "This value is generated when generating the registration" From 501dc541786cc7fe95150653ed473faf78530bb8 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 23 Nov 2020 18:13:14 +0800 Subject: [PATCH 038/109] fix invite bot timeout in faderation --- matrix.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/matrix.go b/matrix.go index b6b3834..e205643 100644 --- a/matrix.go +++ b/matrix.go @@ -58,11 +58,17 @@ func (mx *MatrixHandler) HandleEncryption(evt *event.Event) { func (mx *MatrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { resp, err := intent.JoinRoomByID(evt.RoomID) if err != nil { - time.Sleep(3 * time.Second) + mx.log.Debugfln("JoinRoomByID err, retry in 5 seconds", err) + time.Sleep(5 * time.Second) resp, err = intent.JoinRoomByID(evt.RoomID) if err != nil { - mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err) - return nil + mx.log.Debugfln("JoinRoomByID err, retry again in 5 seconds", err) + time.Sleep(5 * time.Second) + resp, err = intent.JoinRoomByID(evt.RoomID) + if err != nil { + mx.log.Debugfln("Failed to join room %s as %s with invite from %s: %v", evt.RoomID, intent.UserID, evt.Sender, err) + return nil + } } } From 4b0e58bfad38408700c06df0fe6982a3466fde9e Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 24 Nov 2020 14:22:52 +0800 Subject: [PATCH 039/109] supplement to the last commit --- matrix.go | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/matrix.go b/matrix.go index e205643..2c13705 100644 --- a/matrix.go +++ b/matrix.go @@ -95,34 +95,20 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { return } - resp, err := intent.JoinRoomByID(evt.RoomID) - if err != nil { - mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender) - return - } - - members, err := intent.JoinedMembers(resp.RoomID) - if err != nil { - mx.log.Debugln("Failed to get members in room", resp.RoomID, "after accepting invite from", evt.Sender) - intent.LeaveRoom(resp.RoomID) - return - } - - if len(members.Joined) < 2 { - mx.log.Debugln("Leaving empty room", resp.RoomID, "after accepting invite from", evt.Sender) - intent.LeaveRoom(resp.RoomID) + members := mx.joinAndCheckMembers(evt, intent) + if members == nil { return } if !user.Whitelisted { - intent.SendNotice(resp.RoomID, "You are not whitelisted to use this bridge.\n"+ + _, _ = intent.SendNotice(evt.RoomID, "You are not whitelisted to use this bridge.\n"+ "If you're the owner of this bridge, see the bridge.permissions section in your config file.") - intent.LeaveRoom(resp.RoomID) + _, _ = intent.LeaveRoom(evt.RoomID) return } if evt.RoomID == mx.bridge.Config.Bridge.Relaybot.ManagementRoom { - intent.SendNotice(evt.RoomID, "This is the relaybot management room. Send `!wa help` to get a list of commands.") + _, _ = intent.SendNotice(evt.RoomID, "This is the relaybot management room. Send `!wa help` to get a list of commands.") mx.log.Debugln("Joined relaybot management room", evt.RoomID, "after invite from", evt.Sender) return } @@ -135,17 +121,16 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { hasPuppets = true continue } - mx.log.Debugln("Leaving multi-user room", resp.RoomID, "after accepting invite from", evt.Sender) - intent.SendNotice(resp.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.") - intent.LeaveRoom(resp.RoomID) + mx.log.Debugln("Leaving multi-user room", evt.RoomID, "after accepting invite from", evt.Sender) + _, _ = intent.SendNotice(evt.RoomID, "This bridge is user-specific, please don't invite me into rooms with other users.") + _, _ = intent.LeaveRoom(evt.RoomID) return } - if !hasPuppets { - user := mx.bridge.GetUserByMXID(evt.Sender) - user.SetManagementRoom(resp.RoomID) - intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.") - mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) + if !hasPuppets && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) { + user.SetManagementRoom(evt.RoomID) + _, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.") + mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender) } } From 17b42a369a9b720c1d420c1f244a0f690203d28a Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 26 Nov 2020 18:35:33 +0800 Subject: [PATCH 040/109] update crypto --- commands.go | 2 +- config/registration.go | 3 +- crypto.go | 39 ++++++++++++++++++- .../2020-10-28-crypto-store-cross-signing.go | 13 +++++++ database/upgrades/upgrades.go | 2 +- go.mod | 4 +- go.sum | 6 +++ 7 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 database/upgrades/2020-10-28-crypto-store-cross-signing.go diff --git a/commands.go b/commands.go index 88f0589..72e7b93 100644 --- a/commands.go +++ b/commands.go @@ -443,7 +443,7 @@ const cmdDisconnectHelp = `disconnect - Disconnect from WhatsApp (without loggin // ce.Reply("Successfully disconnected. Use the `reconnect` command to reconnect.") //} -const cmdPingHelp = `ping - Check your connection to WhatsApp.` +const cmdPingHelp = `ping - Check your connection to Skype.` func (handler *CommandHandler) CommandPing(ce *CommandEvent) { if ce.User.Session == nil || ce.User.Session.SkypeToken == "" { diff --git a/config/registration.go b/config/registration.go index 00922b7..fafd59e 100644 --- a/config/registration.go +++ b/config/registration.go @@ -58,7 +58,8 @@ func (config *Config) GetRegistration() (*appservice.Registration, error) { func (config *Config) copyToRegistration(registration *appservice.Registration) error { registration.ID = config.AppService.ID registration.URL = config.AppService.Address - registration.RateLimited = false + falseVal := false + registration.RateLimited = &falseVal registration.SenderLocalpart = config.AppService.Bot.Username userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", diff --git a/crypto.go b/crypto.go index 194872a..709325e 100644 --- a/crypto.go +++ b/crypto.go @@ -82,7 +82,6 @@ func (helper *CryptoHelper) Init() error { helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore) helper.mach.AllowKeyShare = helper.allowKeyShare - helper.client.Logger = logger.int.Sub("Bot") helper.client.Syncer = &cryptoSyncer{helper.mach} helper.client.Store = &cryptoClientStore{helper.store} @@ -114,6 +113,44 @@ func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info ev } func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { + deviceID := helper.store.FindDeviceID() + if len(deviceID) > 0 { + helper.log.Debugln("Found existing device ID for bot in database:", deviceID) + } + client, err := mautrix.NewClient(helper.bridge.AS.HomeserverURL, "", "") + if err != nil { + return nil, fmt.Errorf("failed to initialize client: %w", err) + } + client.Logger = helper.baseLog.Sub("Bot") + flows, err := client.GetLoginFlows() + if err != nil { + return nil, fmt.Errorf("failed to get supported login flows: %w", err) + } + if !flows.HasFlow(mautrix.AuthTypeAppservice) { + // TODO after synapse 1.22, turn this into an error + helper.log.Warnln("Encryption enabled in config, but homeserver does not advertise appservice login") + //return nil, fmt.Errorf("homeserver does not support appservice login") + } + // We set the API token to the AS token here to authenticate the appservice login + // It'll get overridden after the login + client.AccessToken = helper.bridge.AS.Registration.AppToken + resp, err := client.Login(&mautrix.ReqLogin{ + Type: mautrix.AuthTypeAppservice, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, + DeviceID: deviceID, + InitialDeviceDisplayName: "WhatsApp Bridge", + StoreCredentials: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to log in as bridge bot: %w", err) + } + if len(deviceID) == 0 { + helper.store.DeviceID = resp.DeviceID + } + return client, nil +} + +func (helper *CryptoHelper) loginBotOld() (*mautrix.Client, error) { deviceID := helper.store.FindDeviceID() if len(deviceID) > 0 { helper.log.Debugln("Found existing device ID for bot in database:", deviceID) diff --git a/database/upgrades/2020-10-28-crypto-store-cross-signing.go b/database/upgrades/2020-10-28-crypto-store-cross-signing.go new file mode 100644 index 0000000..2fb89f0 --- /dev/null +++ b/database/upgrades/2020-10-28-crypto-store-cross-signing.go @@ -0,0 +1,13 @@ +package upgrades + +import ( + "database/sql" + + "maunium.net/go/mautrix/crypto/sql_store_upgrade" +) + +func init() { + upgrades[18] = upgrade{"Add cross-signing keys to crypto store", func(tx *sql.Tx, c context) error { + return sql_store_upgrade.Upgrades[3](tx, c.dialect.String()) + }} +} diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go index f020d94..a870745 100644 --- a/database/upgrades/upgrades.go +++ b/database/upgrades/upgrades.go @@ -39,7 +39,7 @@ type upgrade struct { fn upgradeFunc } -const NumberOfUpgrades = 18 +const NumberOfUpgrades = 19 var upgrades [NumberOfUpgrades]upgrade diff --git a/go.mod b/go.mod index 6d043b9..b5e8f40 100644 --- a/go.mod +++ b/go.mod @@ -16,11 +16,11 @@ require ( gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.1.1 - maunium.net/go/mautrix v0.7.2 + maunium.net/go/mautrix v0.8.0-rc.4 ) replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8 -replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d +replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc replace github.com/kelaresg/go-skypeapi => /Users/yangguang/matrix/go-skypeapi-kelare diff --git a/go.sum b/go.sum index 0c69ab8..c099393 100644 --- a/go.sum +++ b/go.sum @@ -208,6 +208,10 @@ github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106123139-e1c6c37e09d6 h1:RKx github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106123139-e1c6c37e09d6/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d h1:cw6XWBIvj8DaRO/rWzndd4q5Yzbei6Sgj/UUPHuCWv4= github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= +github.com/pidongqianqian/mautrix-go v0.7.3-0.20201126040927-dbde233d88f6 h1:A2W0H1dcKLw1f4z5hA5BET0mAn3gyhwyYGBhYN6Dz9g= +github.com/pidongqianqian/mautrix-go v0.7.3-0.20201126040927-dbde233d88f6/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= +github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc h1:wXc6kSKzzYrLgqJwaCgJvKZc4Qfq85/3TivSItUFuNk= +github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -461,6 +465,8 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +maunium.net/go/mautrix v0.7.13 h1:qfnvLxvQafvLgHbdZF/+9qs9gyArYf8fUnzfQbjgQaU= +maunium.net/go/mautrix v0.7.13/go.mod h1:Jn0ijwXwMFvJFIN9IljirIVKpZQbZP/Dk7pdX2qDmXk= maunium.net/go/mautrix-whatsapp v0.1.4 h1:qlkb3eXcKm1QE6AjrAl9aKxokHlwj7BNr+aUQFXFmWE= maunium.net/go/mautrix-whatsapp v0.1.4/go.mod h1:yC5pjdUQckJzuMX5rrg2237kz/7zP+7qO1uckOODe5M= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From 638ed8942d000cb341eafa9d6f907584eca1ec78 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Thu, 26 Nov 2020 18:41:35 +0800 Subject: [PATCH 041/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d62bb33..6185fbd 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@807a3ad2ec5bdd8f768e701ae5914938ececc2bc` +Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@5517c72e321147c133403698608032106b98f50f` This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 9e98174b3c7b82a8dbfad04c30c99d693e55923b Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 26 Nov 2020 18:52:08 +0800 Subject: [PATCH 042/109] update go.mod --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b5e8f40..d59b3e1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Rhymen/go-whatsapp v0.1.0 github.com/chai2010/webp v1.1.0 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20200828122051-fb22fb75dede + github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858 github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 @@ -22,5 +22,3 @@ require ( replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8 replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc - -replace github.com/kelaresg/go-skypeapi => /Users/yangguang/matrix/go-skypeapi-kelare diff --git a/go.sum b/go.sum index c099393..81981ba 100644 --- a/go.sum +++ b/go.sum @@ -154,6 +154,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858 h1:dHavsrpbaOA/fqwB39zndM1uHIy84EDKhldWajIcfJg= +github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= From 2f429259bd1e425290199ce4bec5e93f8f4fc8f2 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Thu, 26 Nov 2020 18:53:57 +0800 Subject: [PATCH 043/109] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6185fbd..c4e449f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@5517c72e321147c133403698608032106b98f50f` +~~Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@226d1ec92858504b03e32017cd007420b3d7f205`~~ This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 5bf083832995f8b8df0b1df849f207ec29527fea Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 27 Nov 2020 11:10:19 +0800 Subject: [PATCH 044/109] change device name --- community.go | 2 +- crypto.go | 4 ++-- custompuppet.go | 4 ++-- user.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/community.go b/community.go index 849fbca..4544a66 100644 --- a/community.go +++ b/community.go @@ -22,7 +22,7 @@ func (user *User) updateCommunityProfile() { Name string `json:"name"` AvatarURL string `json:"avatar_url"` ShortDescription string `json:"short_description"` - }{"WhatsApp", user.bridge.Config.AppService.Bot.Avatar, "Your WhatsApp bridged chats"} + }{"Skype", user.bridge.Config.AppService.Bot.Avatar, "Your Skype bridged chats"} _, err := user.bridge.Bot.MakeRequest(http.MethodPost, url, &profileReq, nil) if err != nil { user.log.Warnfln("Failed to update metadata of %s: %v", user.CommunityID, err) diff --git a/crypto.go b/crypto.go index 709325e..f67db9a 100644 --- a/crypto.go +++ b/crypto.go @@ -138,7 +138,7 @@ func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { Type: mautrix.AuthTypeAppservice, Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, DeviceID: deviceID, - InitialDeviceDisplayName: "WhatsApp Bridge", + InitialDeviceDisplayName: "Skype Bridge", StoreCredentials: true, }) if err != nil { @@ -166,7 +166,7 @@ func (helper *CryptoHelper) loginBotOld() (*mautrix.Client, error) { Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, Password: hex.EncodeToString(mac.Sum(nil)), DeviceID: deviceID, - InitialDeviceDisplayName: "WhatsApp Bridge", + InitialDeviceDisplayName: "Skype Bridge", StoreCredentials: true, }) if err != nil { diff --git a/custompuppet.go b/custompuppet.go index 91ea32c..58cab5c 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -51,8 +51,8 @@ func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { Type: "m.login.password", Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(mxid)}, Password: hex.EncodeToString(mac.Sum(nil)), - DeviceID: "WhatsApp Bridge", - InitialDeviceDisplayName: "WhatsApp Bridge", + DeviceID: "Skype Bridge", + InitialDeviceDisplayName: "Skype Bridge", }) if err != nil { return "", err diff --git a/user.go b/user.go index f567ffe..89bed9d 100644 --- a/user.go +++ b/user.go @@ -183,7 +183,7 @@ func (user *User) GetManagementRoom() id.RoomID { return user.ManagementRoom } resp, err := user.bridge.Bot.CreateRoom(&mautrix.ReqCreateRoom{ - Topic: "WhatsApp bridge notices", + Topic: "Skype bridge notices", IsDirect: true, }) if err != nil { From b2471e801f8e0ca9650eadfe010e39bbeeb4e951 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 27 Nov 2020 12:17:47 +0800 Subject: [PATCH 045/109] Remove irrelevant code --- commands.go | 14 +- go.mod | 3 - portal.go | 379 +++++++++-------------------------- puppet.go | 10 +- skype-ext/chat.go | 5 +- skype-ext/cmd.go | 5 +- user.go | 242 +++++++++++----------- whatsapp-ext/call.go | 72 ------- whatsapp-ext/chat.go | 174 ---------------- whatsapp-ext/cmd.go | 69 ------- whatsapp-ext/conn.go | 65 ------ whatsapp-ext/jsonmessage.go | 105 ---------- whatsapp-ext/msginfo.go | 95 --------- whatsapp-ext/presence.go | 67 ------- whatsapp-ext/props.go | 73 ------- whatsapp-ext/protomessage.go | 59 ------ whatsapp-ext/stream.go | 68 ------- whatsapp-ext/whatsapp.go | 206 ------------------- 18 files changed, 224 insertions(+), 1487 deletions(-) delete mode 100644 whatsapp-ext/call.go delete mode 100644 whatsapp-ext/chat.go delete mode 100644 whatsapp-ext/cmd.go delete mode 100644 whatsapp-ext/conn.go delete mode 100644 whatsapp-ext/jsonmessage.go delete mode 100644 whatsapp-ext/msginfo.go delete mode 100644 whatsapp-ext/presence.go delete mode 100644 whatsapp-ext/props.go delete mode 100644 whatsapp-ext/protomessage.go delete mode 100644 whatsapp-ext/stream.go delete mode 100644 whatsapp-ext/whatsapp.go diff --git a/commands.go b/commands.go index 72e7b93..2df83ca 100644 --- a/commands.go +++ b/commands.go @@ -8,11 +8,9 @@ import ( "math" "time" - //"math" "sort" "strconv" "strings" - //"time" "maunium.net/go/maulogger/v2" @@ -21,8 +19,6 @@ import ( "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" - - "github.com/kelaresg/matrix-skype/whatsapp-ext" ) type CommandHandler struct { @@ -697,7 +693,7 @@ func (handler *CommandHandler) CommandOpen(ce *CommandEvent) { jid := ce.Args[0] if strings.HasSuffix(jid, skypeExt.NewUserSuffix) { - ce.Reply("That looks like a user ID. Did you mean `pm %s`?", jid[:len(jid)-len(whatsappExt.NewUserSuffix)]) + ce.Reply("That looks like a user ID. Did you mean `pm %s`?", jid[:len(jid)-len(skypeExt.NewUserSuffix)]) return } ce.User.Conn.GetConversations("", handler.bridge.Config.Bridge.InitialChatSync) @@ -977,7 +973,7 @@ func (handler *CommandHandler) CommandKick(ce *CommandEvent) { // reason = ce.Args[0] //} - if strings.HasSuffix(converationId, whatsappExt.NewUserSuffix) { + if strings.HasSuffix(converationId, skypeExt.NewUserSuffix) { ce.Reply("**Usage:** `kick ,... reason`") return } @@ -995,8 +991,8 @@ func (handler *CommandHandler) CommandKick(ce *CommandEvent) { portal := user.bridge.GetPortalByJID(database.GroupPortalKey(converationId)) for i, number := range userNumbers { - userNumbers[i] = number // + whatsappExt.NewUserSuffix - member := portal.bridge.GetPuppetByJID(number + whatsappExt.NewUserSuffix) + userNumbers[i] = number // + skypeExt.NewUserSuffix + member := portal.bridge.GetPuppetByJID(number + skypeExt.NewUserSuffix) if member == nil { portal.log.Errorln("%s is not a puppet", number) @@ -1025,7 +1021,7 @@ func (handler *CommandHandler) CommandLeave(ce *CommandEvent) { user := ce.User groupId := ce.Args[0] - if strings.HasSuffix(groupId, whatsappExt.NewUserSuffix) { + if strings.HasSuffix(groupId, skypeExt.NewUserSuffix) { ce.Reply("**Usage:** `leave `") return } diff --git a/go.mod b/go.mod index d59b3e1..ac80bb8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/kelaresg/matrix-skype go 1.14 require ( - github.com/Rhymen/go-whatsapp v0.1.0 github.com/chai2010/webp v1.1.0 github.com/gorilla/websocket v1.4.2 github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858 @@ -19,6 +18,4 @@ require ( maunium.net/go/mautrix v0.8.0-rc.4 ) -replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.8 - replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc diff --git a/portal.go b/portal.go index d1dfedc..d818c28 100644 --- a/portal.go +++ b/portal.go @@ -2,13 +2,16 @@ package main import ( "bytes" - "encoding/gob" + //whatsappExt "github.com/kelaresg/matrix-skype/whatsapp-ext" + + //"encoding/gob" "encoding/hex" "encoding/json" "encoding/xml" "fmt" skype "github.com/kelaresg/go-skypeapi" skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + //whatsappExt "github.com/kelaresg/matrix-skype/whatsapp-ext" "html" "image" "image/gif" @@ -28,8 +31,8 @@ import ( "maunium.net/go/mautrix/crypto/attachment" - "github.com/Rhymen/go-whatsapp" - waProto "github.com/Rhymen/go-whatsapp/binary/proto" + //"github.com/Rhymen/go-whatsapp" + //waProto "github.com/Rhymen/go-whatsapp/binary/proto" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" @@ -40,7 +43,7 @@ import ( "github.com/kelaresg/matrix-skype/database" "github.com/kelaresg/matrix-skype/types" - "github.com/kelaresg/matrix-skype/whatsapp-ext" + //"github.com/kelaresg/matrix-skype/whatsapp-ext" ) func (bridge *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal { @@ -251,36 +254,36 @@ func (portal *Portal) isDuplicate(clientMessageId types.SkypeMessageID, id strin return false } -func init() { - gob.Register(&waProto.Message{}) -} +//func init() { +// gob.Register(&waProto.Message{}) +//} -func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { - msg := portal.bridge.DB.Message.New() - msg.Chat = portal.Key - msg.JID = message.GetKey().GetId() - msg.MXID = mxid - msg.Timestamp = message.GetMessageTimestamp() - if message.GetKey().GetFromMe() { - msg.Sender = source.JID - } else if portal.IsPrivateChat() { - msg.Sender = portal.Key.JID - } else { - msg.Sender = message.GetKey().GetParticipant() - if len(msg.Sender) == 0 { - msg.Sender = message.GetParticipant() - } - } - //msg.Content = message.Message - msg.Content = &skype.Resource{} - msg.Insert() - - portal.recentlyHandledLock.Lock() - index := portal.recentlyHandledIndex - portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength - portal.recentlyHandledLock.Unlock() - portal.recentlyHandled[index] = msg.JID -} +//func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { +// msg := portal.bridge.DB.Message.New() +// msg.Chat = portal.Key +// msg.JID = message.GetKey().GetId() +// msg.MXID = mxid +// msg.Timestamp = message.GetMessageTimestamp() +// if message.GetKey().GetFromMe() { +// msg.Sender = source.JID +// } else if portal.IsPrivateChat() { +// msg.Sender = portal.Key.JID +// } else { +// msg.Sender = message.GetKey().GetParticipant() +// if len(msg.Sender) == 0 { +// msg.Sender = message.GetParticipant() +// } +// } +// //msg.Content = message.Message +// msg.Content = &skype.Resource{} +// msg.Insert() +// +// portal.recentlyHandledLock.Lock() +// index := portal.recentlyHandledIndex +// portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength +// portal.recentlyHandledLock.Unlock() +// portal.recentlyHandled[index] = msg.JID +//} func (portal *Portal) markHandledSkype(source *User, message *skype.Resource, mxid id.EventID) { msg := portal.bridge.DB.Message.New() @@ -312,20 +315,20 @@ fmt.Println("markHandledSkype2", msg.JID) portal.recentlyHandled[index] = msg.JID } -func (portal *Portal) getMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { - if info.FromMe { - return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) - } else if portal.IsPrivateChat() { - return portal.MainIntent() - } else if len(info.SenderJid) == 0 { - if len(info.Source.GetParticipant()) != 0 { - info.SenderJid = info.Source.GetParticipant() - } else { - return nil - } - } - return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal) -} +//func (portal *Portal) getMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { +// if info.FromMe { +// return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) +// } else if portal.IsPrivateChat() { +// return portal.MainIntent() +// } else if len(info.SenderJid) == 0 { +// if len(info.Source.GetParticipant()) != 0 { +// info.SenderJid = info.Source.GetParticipant() +// } else { +// return nil +// } +// } +// return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal) +//} func (portal *Portal) getMessageIntentSkype(user *User, info skype.Resource) *appservice.IntentAPI { if info.GetFromMe(user.Conn.Conn) { @@ -385,11 +388,11 @@ func (portal *Portal) startHandlingSkype(source *User, info skype.Resource) (*ap return nil, nil } -func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { - portal.markHandled(source, message, mxid) - portal.sendDeliveryReceipt(mxid) - portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid) -} +//func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { +// portal.markHandled(source, message, mxid) +// portal.sendDeliveryReceipt(mxid) +// portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid) +//} func (portal *Portal) finishHandlingSkype(source *User, message *skype.Resource, mxid id.EventID) { portal.markHandledSkype(source, message, mxid) @@ -1291,21 +1294,21 @@ func (portal *Portal) MainIntent() *appservice.IntentAPI { return portal.bridge.Bot } -func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp.ContextInfo) { - if len(info.QuotedMessageID) == 0 { - return - } - message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID) - if message != nil { - evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) - if err != nil { - portal.log.Warnln("Failed to get reply target:", err) - return - } - content.SetReply(evt) - } - return -} +//func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp.ContextInfo) { +// if len(info.QuotedMessageID) == 0 { +// return +// } +// message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID) +// if message != nil { +// evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) +// if err != nil { +// portal.log.Warnln("Failed to get reply target:", err) +// return +// } +// content.SetReply(evt) +// } +// return +//} func (portal *Portal) SetReplySkype(content *event.MessageEventContent, info skype.Resource) { if len(info.Id) == 0 { @@ -1347,32 +1350,31 @@ func (portal *Portal) HandleMessageRevokeSkype(user *User, message skype.Resourc msg.Delete() } - -func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.MessageRevocation) { - msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id) - if msg == nil { - return - } - var intent *appservice.IntentAPI - if message.FromMe { - if portal.IsPrivateChat() { - intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent() - } else { - intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) - } - } else if len(message.Participant) > 0 { - intent = portal.bridge.GetPuppetByJID(message.Participant).IntentFor(portal) - } - if intent == nil { - intent = portal.MainIntent() - } - _, err := intent.RedactEvent(portal.MXID, msg.MXID) - if err != nil { - portal.log.Errorln("Failed to redact %s: %v", msg.JID, err) - return - } - msg.Delete() -} +//func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.MessageRevocation) { +// msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id) +// if msg == nil { +// return +// } +// var intent *appservice.IntentAPI +// if message.FromMe { +// if portal.IsPrivateChat() { +// intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent() +// } else { +// intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) +// } +// } else if len(message.Participant) > 0 { +// intent = portal.bridge.GetPuppetByJID(message.Participant).IntentFor(portal) +// } +// if intent == nil { +// intent = portal.MainIntent() +// } +// _, err := intent.RedactEvent(portal.MXID, msg.MXID) +// if err != nil { +// portal.log.Errorln("Failed to redact %s: %v", msg.JID, err) +// return +// } +// msg.Delete() +//} func (portal *Portal) HandleFakeMessage(source *User, message FakeMessage) { if portal.isRecentlyHandled(message.ID) { @@ -1579,7 +1581,7 @@ func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn * } data, mediaMessage, err := download(source.Conn.Conn, mediaType) - if err == whatsapp.ErrMediaDownloadFailedWith404 || err == whatsapp.ErrMediaDownloadFailedWith410 { + if err == skype.ErrMediaDownloadFailedWith404 || err == skype.ErrMediaDownloadFailedWith410 { portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", info.Id, err) //_, err = source.Conn.LoadMediaInfo(info.RemoteJid, info.Id, info.FromMe) //if err != nil { @@ -1588,7 +1590,7 @@ func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn * //} data, mediaMessage, err = download(source.Conn.Conn, mediaType) } - if err == whatsapp.ErrNoURLPresent { + if err == skype.ErrNoURLPresent { portal.log.Debugfln("No URL present error for media message %s, ignoring...", info.Id) return } else if err != nil { @@ -1819,54 +1821,6 @@ func (portal *Portal) preprocessMatrixMediaSkype(relaybotFormatted bool, content return caption, uint64(len(data)), data } -func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, eventID id.EventID, mediaType whatsapp.MediaType) *MediaUpload { - //var caption string - //if relaybotFormatted { - // caption = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) - //} - - var file *event.EncryptedFileInfo - rawMXC := content.URL - if content.File != nil { - file = content.File - rawMXC = file.URL - } - mxc, err := rawMXC.Parse() - if err != nil { - portal.log.Errorln("Malformed content URL in %s: %v", eventID, err) - return nil - } - data, err := portal.MainIntent().DownloadBytes(mxc) - if err != nil { - portal.log.Errorfln("Failed to download media in %s: %v", eventID, err) - return nil - } - if file != nil { - data, err = file.Decrypt(data) - if err != nil { - portal.log.Errorfln("Failed to decrypt media in %s: %v", eventID, err) - return nil - } - } - - //url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(data), mediaType) - //if err != nil { - // portal.log.Errorfln("Failed to upload media in %s: %v", eventID, err) - // return nil - //} - // - //return &MediaUpload{ - // Caption: caption, - // URL: url, - // MediaKey: mediaKey, - // FileEncSHA256: fileEncSHA256, - // FileSHA256: fileSHA256, - // FileLength: fileLength, - // Thumbnail: portal.downloadThumbnail(content, eventID), - //} - return nil -} - type MediaUpload struct { Caption string URL string @@ -2098,145 +2052,6 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) return info, sender, content } -func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.WebMessageInfo, *User, *event.MessageEventContent) { - content, ok := evt.Content.Parsed.(*event.MessageEventContent) - if !ok { - portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed) - return nil, sender, content - } - - ts := uint64(evt.Timestamp / 1000) - status := waProto.WebMessageInfo_ERROR - fromMe := true - info := &waProto.WebMessageInfo{ - Key: &waProto.MessageKey{ - FromMe: &fromMe, - Id: makeMessageID(), - RemoteJid: &portal.Key.JID, - }, - MessageTimestamp: &ts, - Message: &waProto.Message{}, - Status: &status, - } - //ctxInfo := &waProto.ContextInfo{} - replyToID := content.GetReplyTo() - if len(replyToID) > 0 { - content.RemoveReplyFallback() - msg := portal.bridge.DB.Message.GetByMXID(replyToID) - //if msg != nil && msg.Content != nil { - if msg != nil { - //ctxInfo.StanzaId = &msg.JID - //ctxInfo.Participant = &msg.Sender - //ctxInfo.QuotedMessage = msg.Content - } - } - relaybotFormatted := false - if sender.NeedsRelaybot(portal) { - if !portal.HasRelaybot() { - if sender.HasSession() { - portal.log.Debugln("Database says", sender.MXID, "not in chat and no relaybot, but trying to send anyway") - } else { - portal.log.Debugln("Ignoring message from", sender.MXID, "in chat with no relaybot") - return nil, sender, content - } - } else { - relaybotFormatted = portal.addRelaybotFormat(sender, content) - sender = portal.bridge.Relaybot - } - } - if evt.Type == event.EventSticker { - content.MsgType = event.MsgImage - } -fmt.Println("convertMatrixMessage content.MsgType: ", content.MsgType) - switch content.MsgType { - case event.MsgText, event.MsgEmote, event.MsgNotice: - text := content.Body - if content.Format == event.FormatHTML { - text = portal.bridge.Formatter.ParseMatrix(content.FormattedBody) - } - if content.MsgType == event.MsgEmote && !relaybotFormatted { - text = "/me " + text - } - //ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1) - //for index, mention := range ctxInfo.MentionedJid { - // ctxInfo.MentionedJid[index] = mention[1:] + whatsappExt.NewUserSuffix - //} - //if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil { - // info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{ - // Text: &text, - // ContextInfo: ctxInfo, - // } - //} else { - // info.Message.Conversation = &text - //} - case event.MsgImage: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaImage) - if media == nil { - return nil, sender, content - } - info.Message.ImageMessage = &waProto.ImageMessage{ - Caption: &media.Caption, - JpegThumbnail: media.Thumbnail, - Url: &media.URL, - MediaKey: media.MediaKey, - Mimetype: &content.GetInfo().MimeType, - FileEncSha256: media.FileEncSHA256, - FileSha256: media.FileSHA256, - FileLength: &media.FileLength, - } - case event.MsgVideo: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaVideo) - if media == nil { - return nil, sender, content - } - duration := uint32(content.GetInfo().Duration) - info.Message.VideoMessage = &waProto.VideoMessage{ - Caption: &media.Caption, - JpegThumbnail: media.Thumbnail, - Url: &media.URL, - MediaKey: media.MediaKey, - Mimetype: &content.GetInfo().MimeType, - Seconds: &duration, - FileEncSha256: media.FileEncSHA256, - FileSha256: media.FileSHA256, - FileLength: &media.FileLength, - } - case event.MsgAudio: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaAudio) - if media == nil { - return nil, sender, content - } - duration := uint32(content.GetInfo().Duration) - info.Message.AudioMessage = &waProto.AudioMessage{ - Url: &media.URL, - MediaKey: media.MediaKey, - Mimetype: &content.GetInfo().MimeType, - Seconds: &duration, - FileEncSha256: media.FileEncSHA256, - FileSha256: media.FileSHA256, - FileLength: &media.FileLength, - } - case event.MsgFile: - media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaDocument) - if media == nil { - return nil, sender, content - } - info.Message.DocumentMessage = &waProto.DocumentMessage{ - Url: &media.URL, - FileName: &content.Body, - MediaKey: media.MediaKey, - Mimetype: &content.GetInfo().MimeType, - FileEncSha256: media.FileEncSHA256, - FileSha256: media.FileSHA256, - FileLength: &media.FileLength, - } - default: - portal.log.Debugln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType) - return nil, sender, content - } - return info, sender, content -} - func (portal *Portal) wasMessageSent(sender *User, id string) bool { //_, err := sender.Conn.LoadMessagesAfter(portal.Key.JID, id, true, 0) //if err != nil { diff --git a/puppet.go b/puppet.go index 31543dd..15bdfdc 100644 --- a/puppet.go +++ b/puppet.go @@ -15,7 +15,7 @@ import ( "github.com/kelaresg/matrix-skype/database" "github.com/kelaresg/matrix-skype/types" - "github.com/kelaresg/matrix-skype/whatsapp-ext" + //"github.com/kelaresg/matrix-skype/whatsapp-ext" ) func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.SkypeID, bool) { @@ -134,10 +134,8 @@ func (bridge *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet { MXID: id.NewUserID( bridge.Config.Bridge.FormatUsername( - // dbPuppet.JID, - //), strings.Replace( - strings.Replace(dbPuppet.JID, whatsappExt.NewUserSuffix, "", 1), + strings.Replace(dbPuppet.JID, skypeExt.NewUserSuffix, "", 1), ":", "-", -1, @@ -164,7 +162,7 @@ type Puppet struct { } func (puppet *Puppet) PhoneNumber() string { - return strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1) + return strings.Replace(puppet.JID, skypeExt.NewUserSuffix, "", 1) } func (puppet *Puppet) IntentFor(portal *Portal) *appservice.IntentAPI { @@ -272,7 +270,7 @@ func (puppet *Puppet) UpdateName(source *User, contact skype.Contact) bool { func (puppet *Puppet) updatePortalMeta(meta func(portal *Portal)) { if puppet.bridge.Config.Bridge.PrivateChatPortalMeta { - jid := strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1) + jid := strings.Replace(puppet.JID, skypeExt.NewUserSuffix, "", 1) for _, portal := range puppet.bridge.GetAllPortalsByJID(jid) { meta(portal) } diff --git a/skype-ext/chat.go b/skype-ext/chat.go index 70453dd..28845e4 100644 --- a/skype-ext/chat.go +++ b/skype-ext/chat.go @@ -18,9 +18,8 @@ package skypeExt import ( "encoding/json" + skype "github.com/kelaresg/go-skypeapi" "strings" - - "github.com/Rhymen/go-whatsapp" ) type ChatUpdateCommand string @@ -155,7 +154,7 @@ func (cud *ChatUpdateData) UnmarshalJSON(data []byte) error { } type ChatUpdateHandler interface { - whatsapp.Handler + skype.Handler HandleChatUpdate(ChatUpdate) } diff --git a/skype-ext/cmd.go b/skype-ext/cmd.go index fcbda17..75ef250 100644 --- a/skype-ext/cmd.go +++ b/skype-ext/cmd.go @@ -18,9 +18,8 @@ package skypeExt import ( "encoding/json" + skype "github.com/kelaresg/go-skypeapi" "strings" - - "github.com/Rhymen/go-whatsapp" ) type CommandType string @@ -41,7 +40,7 @@ type Command struct { } type CommandHandler interface { - whatsapp.Handler + skype.Handler HandleCommand(Command) } diff --git a/user.go b/user.go index 89bed9d..216e94d 100644 --- a/user.go +++ b/user.go @@ -12,20 +12,20 @@ import ( "sync" "time" - "github.com/pkg/errors" + //"github.com/pkg/errors" "github.com/skip2/go-qrcode" log "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" - "github.com/Rhymen/go-whatsapp" - waProto "github.com/Rhymen/go-whatsapp/binary/proto" + //"github.com/Rhymen/go-whatsapp" + //waProto "github.com/Rhymen/go-whatsapp/binary/proto" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "github.com/kelaresg/matrix-skype/database" "github.com/kelaresg/matrix-skype/types" - "github.com/kelaresg/matrix-skype/whatsapp-ext" + //"github.com/kelaresg/matrix-skype/whatsapp-ext" ) type User struct { @@ -587,13 +587,13 @@ func (user *User) syncPortals(chatMap map[string]skype.Conversation, createAll b } } -func (user *User) HandleContactList(contacts []whatsapp.Contact) { - contactMap := make(map[string]whatsapp.Contact) - for _, contact := range contacts { - contactMap[contact.Jid] = contact - } - // go user.syncPuppets(contactMap) -} +//func (user *User) HandleContactList(contacts []whatsapp.Contact) { +// contactMap := make(map[string]whatsapp.Contact) +// for _, contact := range contacts { +// contactMap[contact.Jid] = contact +// } +// // go user.syncPuppets(contactMap) +//} func (user *User) syncPuppets(contacts map[string]skype.Contact) { if contacts == nil { @@ -636,20 +636,6 @@ func (user *User) updateLastConnectionIfNecessary() { } func (user *User) HandleError(err error) { - if errors.Cause(err) != whatsapp.ErrInvalidWsData { - user.log.Errorfln("WhatsApp error: %v", err) - } - if closed, ok := err.(*whatsapp.ErrConnectionClosed); ok { - if closed.Code == 1000 && user.cleanDisconnection { - user.cleanDisconnection = false - user.log.Infoln("Clean disconnection by server") - return - } - go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection was closed with websocket status code %d", closed.Code)) - } else if failed, ok := err.(*whatsapp.ErrConnectionFailed); ok { - user.ConnectionErrors++ - go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection failed: %v", failed.Err)) - } // Otherwise unknown error, probably mostly harmless } @@ -762,21 +748,21 @@ func (user *User) HandleImageMessage(message skype.Resource) { user.putMessage(PortalMessage{message.Jid, user, message, uint64(message.Timestamp)}) } -func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) { - user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) -} - -func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { - user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) -} - -func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) { - user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) -} - -func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) { - user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) -} +//func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) { +// user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +//} +// +//func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { +// user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +//} +// +//func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) { +// user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +//} +// +//func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) { +// user.putMessage(PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}) +//} func (user *User) HandleContactMessage(message skype.Resource) { user.log.Debugf("HandleContactMessage: ", message) @@ -798,40 +784,40 @@ type FakeMessage struct { Alert bool } -func (user *User) HandleCallInfo(info whatsappExt.CallInfo) { - if info.Data != nil { - return - } - data := FakeMessage{ - ID: info.ID, - } - switch info.Type { - case whatsappExt.CallOffer: - if !user.bridge.Config.Bridge.CallNotices.Start { - return - } - data.Text = "Incoming call" - data.Alert = true - case whatsappExt.CallOfferVideo: - if !user.bridge.Config.Bridge.CallNotices.Start { - return - } - data.Text = "Incoming video call" - data.Alert = true - case whatsappExt.CallTerminate: - if !user.bridge.Config.Bridge.CallNotices.End { - return - } - data.Text = "Call ended" - data.ID += "E" - default: - return - } - portal := user.GetPortalByJID(info.From) - if portal != nil { - portal.messages <- PortalMessage{info.From, user, data, 0} - } -} +//func (user *User) HandleCallInfo(info whatsappExt.CallInfo) { +// if info.Data != nil { +// return +// } +// data := FakeMessage{ +// ID: info.ID, +// } +// switch info.Type { +// case whatsappExt.CallOffer: +// if !user.bridge.Config.Bridge.CallNotices.Start { +// return +// } +// data.Text = "Incoming call" +// data.Alert = true +// case whatsappExt.CallOfferVideo: +// if !user.bridge.Config.Bridge.CallNotices.Start { +// return +// } +// data.Text = "Incoming video call" +// data.Alert = true +// case whatsappExt.CallTerminate: +// if !user.bridge.Config.Bridge.CallNotices.End { +// return +// } +// data.Text = "Call ended" +// data.ID += "E" +// default: +// return +// } +// portal := user.GetPortalByJID(info.From) +// if portal != nil { +// portal.messages <- PortalMessage{info.From, user, data, 0} +// } +//} func (user *User) HandleTypingStatus(info skype.Resource) { sendId := info.SendId + skypeExt.NewUserSuffix @@ -904,62 +890,62 @@ func (user *User) HandlePresence(info skype.Resource) { } } -func (user *User) HandlePresenceWA(info whatsappExt.Presence) { - puppet := user.bridge.GetPuppetByJID(info.SenderJID) - switch info.Status { - case whatsapp.PresenceUnavailable: - _ = puppet.DefaultIntent().SetPresence("offline") - case whatsapp.PresenceAvailable: - if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { - portal := user.bridge.GetPortalByMXID(puppet.typingIn) - _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) - puppet.typingIn = "" - puppet.typingAt = 0 - } - _ = puppet.DefaultIntent().SetPresence("online") - case whatsapp.PresenceComposing: - portal := user.GetPortalByJID(info.JID) - if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { - if puppet.typingIn == portal.MXID { - return - } - _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) - } - puppet.typingIn = portal.MXID - puppet.typingAt = time.Now().Unix() - _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000) - _ = puppet.DefaultIntent().SetPresence("online") - } -} +//func (user *User) HandlePresenceWA(info whatsappExt.Presence) { +// puppet := user.bridge.GetPuppetByJID(info.SenderJID) +// switch info.Status { +// case whatsapp.PresenceUnavailable: +// _ = puppet.DefaultIntent().SetPresence("offline") +// case whatsapp.PresenceAvailable: +// if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { +// portal := user.bridge.GetPortalByMXID(puppet.typingIn) +// _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) +// puppet.typingIn = "" +// puppet.typingAt = 0 +// } +// _ = puppet.DefaultIntent().SetPresence("online") +// case whatsapp.PresenceComposing: +// portal := user.GetPortalByJID(info.JID) +// if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() { +// if puppet.typingIn == portal.MXID { +// return +// } +// _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0) +// } +// puppet.typingIn = portal.MXID +// puppet.typingAt = time.Now().Unix() +// _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000) +// _ = puppet.DefaultIntent().SetPresence("online") +// } +//} -func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) { - if (info.Command == whatsappExt.MsgInfoCommandAck || info.Command == whatsappExt.MsgInfoCommandAcks) && info.Acknowledgement == whatsappExt.AckMessageRead { - portal := user.GetPortalByJID(info.ToJID) - if len(portal.MXID) == 0 { - return - } - - go func() { - intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal) - for _, id := range info.IDs { - msg := user.bridge.DB.Message.GetByJID(portal.Key, id) - if msg == nil { - continue - } - - err := intent.MarkRead(portal.MXID, msg.MXID) - if err != nil { - user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err) - } - } - }() - } -} +//func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) { +// if (info.Command == whatsappExt.MsgInfoCommandAck || info.Command == whatsappExt.MsgInfoCommandAcks) && info.Acknowledgement == whatsappExt.AckMessageRead { +// portal := user.GetPortalByJID(info.ToJID) +// if len(portal.MXID) == 0 { +// return +// } +// +// go func() { +// intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal) +// for _, id := range info.IDs { +// msg := user.bridge.DB.Message.GetByJID(portal.Key, id) +// if msg == nil { +// continue +// } +// +// err := intent.MarkRead(portal.MXID, msg.MXID) +// if err != nil { +// user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err) +// } +// } +// }() +// } +//} func (user *User) HandleCommand(cmd skypeExt.Command) { switch cmd.Type { case skypeExt.CommandPicture: - if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) { + if strings.HasSuffix(cmd.JID, skypeExt.NewUserSuffix) { puppet := user.bridge.GetPuppetByJID(cmd.JID) go puppet.UpdateAvatar(user, cmd.ProfilePicInfo) } else { @@ -1145,9 +1131,9 @@ func (user *User) HandleJsonMessage(message string) { user.updateLastConnectionIfNecessary() } -func (user *User) HandleRawMessage(message *waProto.WebMessageInfo) { - user.updateLastConnectionIfNecessary() -} +//func (user *User) HandleRawMessage(message *waProto.WebMessageInfo) { +// user.updateLastConnectionIfNecessary() +//} func (user *User) NeedsRelaybot(portal *Portal) bool { return false diff --git a/whatsapp-ext/call.go b/whatsapp-ext/call.go deleted file mode 100644 index 29575c3..0000000 --- a/whatsapp-ext/call.go +++ /dev/null @@ -1,72 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - "strings" - - "github.com/Rhymen/go-whatsapp" -) - -type CallInfoType string - -const ( - CallOffer CallInfoType = "offer" - CallOfferVideo CallInfoType = "offer_video" - CallTransport CallInfoType = "transport" - CallRelayLatency CallInfoType = "relaylatency" - CallTerminate CallInfoType = "terminate" -) - -type CallInfo struct { - ID string `json:"id"` - Type CallInfoType `json:"type"` - From string `json:"from"` - - Platform string `json:"platform"` - Version []int `json:"version"` - - Data [][]interface{} `json:"data"` -} - -type CallInfoHandler interface { - whatsapp.Handler - HandleCallInfo(CallInfo) -} - -func (ext *ExtendedConn) handleMessageCall(message []byte) { - var event CallInfo - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - event.From = strings.Replace(event.From, OldUserSuffix, NewUserSuffix, 1) - for _, handler := range ext.handlers { - callInfoHandler, ok := handler.(CallInfoHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(callInfoHandler) { - callInfoHandler.HandleCallInfo(event) - } else { - go callInfoHandler.HandleCallInfo(event) - } - } -} diff --git a/whatsapp-ext/chat.go b/whatsapp-ext/chat.go deleted file mode 100644 index 8de0a99..0000000 --- a/whatsapp-ext/chat.go +++ /dev/null @@ -1,174 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - "strings" - - "github.com/Rhymen/go-whatsapp" -) - -type ChatUpdateCommand string - -const ( - ChatUpdateCommandAction ChatUpdateCommand = "action" -) - -type ChatUpdate struct { - JID string `json:"id"` - Command ChatUpdateCommand `json:"cmd"` - Data ChatUpdateData `json:"data"` -} - -type ChatActionType string - -const ( - ChatActionNameChange ChatActionType = "subject" - ChatActionAddTopic ChatActionType = "desc_add" - ChatActionRemoveTopic ChatActionType = "desc_remove" - ChatActionRestrict ChatActionType = "restrict" - ChatActionAnnounce ChatActionType = "announce" - ChatActionPromote ChatActionType = "promote" - ChatActionDemote ChatActionType = "demote" - ChatActionRemove ChatActionType = "remove" - ChatActionAdd ChatActionType = "add" - ChatActionIntroduce ChatActionType = "introduce" - ChatActionCreate ChatActionType = "create" -) - -type ChatUpdateData struct { - Action ChatActionType - SenderJID string - - NameChange struct { - Name string `json:"subject"` - SetAt int64 `json:"s_t"` - SetBy string `json:"s_o"` - } - - AddTopic struct { - Topic string `json:"desc"` - ID string `json:"descId"` - SetAt int64 `json:"descTime"` - } - - RemoveTopic struct { - ID string `json:"descId"` - } - - Restrict bool - - Announce bool - - PermissionChange struct { - JIDs []string `json:"participants"` - } - - MemberAction struct { - JIDs []string `json:"participants"` - } - - Create struct { - Creation int64 `json:"creation"` - Name string `json:"subject"` - SetAt int64 `json:"s_t"` - SetBy string `json:"s_o"` - Admins []string `json:"admins"` - SuperAdmins []string `json:"superadmins"` - Regulars []string `json:"regulars"` - } -} - -func (cud *ChatUpdateData) UnmarshalJSON(data []byte) error { - var arr []json.RawMessage - err := json.Unmarshal(data, &arr) - if err != nil { - return err - } else if len(arr) < 3 { - return nil - } - - err = json.Unmarshal(arr[0], &cud.Action) - if err != nil { - return err - } - - err = json.Unmarshal(arr[1], &cud.SenderJID) - if err != nil { - return err - } - cud.SenderJID = strings.Replace(cud.SenderJID, OldUserSuffix, NewUserSuffix, 1) - - var unmarshalTo interface{} - switch cud.Action { - case ChatActionNameChange: - unmarshalTo = &cud.NameChange - case ChatActionAddTopic: - unmarshalTo = &cud.AddTopic - case ChatActionRemoveTopic: - unmarshalTo = &cud.RemoveTopic - case ChatActionRestrict: - unmarshalTo = &cud.Restrict - case ChatActionAnnounce: - unmarshalTo = &cud.Announce - case ChatActionPromote, ChatActionDemote: - unmarshalTo = &cud.PermissionChange - case ChatActionAdd, ChatActionRemove: - unmarshalTo = &cud.MemberAction - case ChatActionCreate: - unmarshalTo = &cud.Create - default: - return nil - } - err = json.Unmarshal(arr[2], unmarshalTo) - if err != nil { - return err - } - cud.NameChange.SetBy = strings.Replace(cud.NameChange.SetBy, OldUserSuffix, NewUserSuffix, 1) - for index, jid := range cud.PermissionChange.JIDs { - cud.PermissionChange.JIDs[index] = strings.Replace(jid, OldUserSuffix, NewUserSuffix, 1) - } - return nil -} - -type ChatUpdateHandler interface { - whatsapp.Handler - HandleChatUpdate(ChatUpdate) -} - -func (ext *ExtendedConn) handleMessageChatUpdate(message []byte) { - var event ChatUpdate - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) - for _, handler := range ext.handlers { - chatUpdateHandler, ok := handler.(ChatUpdateHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(chatUpdateHandler) { - chatUpdateHandler.HandleChatUpdate(event) - } else { - go chatUpdateHandler.HandleChatUpdate(event) - } - } -} diff --git a/whatsapp-ext/cmd.go b/whatsapp-ext/cmd.go deleted file mode 100644 index 0954df3..0000000 --- a/whatsapp-ext/cmd.go +++ /dev/null @@ -1,69 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - "strings" - - "github.com/Rhymen/go-whatsapp" -) - -type CommandType string - -const ( - CommandPicture CommandType = "picture" - CommandDisconnect CommandType = "disconnect" -) - -type Command struct { - Type CommandType `json:"type"` - JID string `json:"jid"` - - *ProfilePicInfo - Kind string `json:"kind"` - - Raw json.RawMessage `json:"-"` -} - -type CommandHandler interface { - whatsapp.Handler - HandleCommand(Command) -} - -func (ext *ExtendedConn) handleMessageCommand(message []byte) { - var event Command - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - event.Raw = message - event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) - for _, handler := range ext.handlers { - commandHandler, ok := handler.(CommandHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(commandHandler) { - commandHandler.HandleCommand(event) - } else { - go commandHandler.HandleCommand(event) - } - } -} diff --git a/whatsapp-ext/conn.go b/whatsapp-ext/conn.go deleted file mode 100644 index 8ce4345..0000000 --- a/whatsapp-ext/conn.go +++ /dev/null @@ -1,65 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - - "github.com/Rhymen/go-whatsapp" -) - -type ConnInfo struct { - ProtocolVersion []int `json:"protoVersion"` - BinaryVersion int `json:"binVersion"` - Phone struct { - WhatsAppVersion string `json:"wa_version"` - MCC string `json:"mcc"` - MNC string `json:"mnc"` - OSVersion string `json:"os_version"` - DeviceManufacturer string `json:"device_manufacturer"` - DeviceModel string `json:"device_model"` - OSBuildNumber string `json:"os_build_number"` - } `json:"phone"` - Features map[string]interface{} `json:"features"` - PushName string `json:"pushname"` -} - -type ConnInfoHandler interface { - whatsapp.Handler - HandleConnInfo(ConnInfo) -} - -func (ext *ExtendedConn) handleMessageConn(message []byte) { - var event ConnInfo - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - for _, handler := range ext.handlers { - connInfoHandler, ok := handler.(ConnInfoHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(connInfoHandler) { - connInfoHandler.HandleConnInfo(event) - } else { - go connInfoHandler.HandleConnInfo(event) - } - } -} diff --git a/whatsapp-ext/jsonmessage.go b/whatsapp-ext/jsonmessage.go deleted file mode 100644 index e0ac474..0000000 --- a/whatsapp-ext/jsonmessage.go +++ /dev/null @@ -1,105 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - - "github.com/Rhymen/go-whatsapp" -) - -type JSONMessage []json.RawMessage - -type JSONMessageType string - -const ( - MessageMsgInfo JSONMessageType = "MsgInfo" - MessageMsg JSONMessageType = "Msg" - MessagePresence JSONMessageType = "Presence" - MessageStream JSONMessageType = "Stream" - MessageConn JSONMessageType = "Conn" - MessageProps JSONMessageType = "Props" - MessageCmd JSONMessageType = "Cmd" - MessageChat JSONMessageType = "Chat" - MessageCall JSONMessageType = "Call" -) - -func (ext *ExtendedConn) HandleError(error) {} - -type UnhandledJSONMessageHandler interface { - whatsapp.Handler - HandleUnhandledJSONMessage(string) -} - -type JSONParseErrorHandler interface { - whatsapp.Handler - HandleJSONParseError(error) -} - -func (ext *ExtendedConn) jsonParseError(err error) { - for _, handler := range ext.handlers { - errorHandler, ok := handler.(JSONParseErrorHandler) - if !ok { - continue - } - errorHandler.HandleJSONParseError(err) - } -} - -func (ext *ExtendedConn) HandleJsonMessage(message string) { - msg := JSONMessage{} - err := json.Unmarshal([]byte(message), &msg) - if err != nil || len(msg) < 2 { - ext.jsonParseError(err) - return - } - - var msgType JSONMessageType - json.Unmarshal(msg[0], &msgType) - - switch msgType { - case MessagePresence: - ext.handleMessagePresence(msg[1]) - case MessageStream: - ext.handleMessageStream(msg[1:]) - case MessageConn: - ext.handleMessageConn(msg[1]) - case MessageProps: - ext.handleMessageProps(msg[1]) - case MessageMsgInfo, MessageMsg: - ext.handleMessageMsgInfo(msgType, msg[1]) - case MessageCmd: - ext.handleMessageCommand(msg[1]) - case MessageChat: - ext.handleMessageChatUpdate(msg[1]) - case MessageCall: - ext.handleMessageCall(msg[1]) - default: - for _, handler := range ext.handlers { - ujmHandler, ok := handler.(UnhandledJSONMessageHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(ujmHandler) { - ujmHandler.HandleUnhandledJSONMessage(message) - } else { - go ujmHandler.HandleUnhandledJSONMessage(message) - } - } - } -} diff --git a/whatsapp-ext/msginfo.go b/whatsapp-ext/msginfo.go deleted file mode 100644 index 702c322..0000000 --- a/whatsapp-ext/msginfo.go +++ /dev/null @@ -1,95 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - "strings" - - "github.com/Rhymen/go-whatsapp" -) - -type MsgInfoCommand string - -const ( - MsgInfoCommandAck MsgInfoCommand = "ack" - MsgInfoCommandAcks MsgInfoCommand = "acks" -) - -type Acknowledgement int - -const ( - AckMessageSent Acknowledgement = 1 - AckMessageDelivered Acknowledgement = 2 - AckMessageRead Acknowledgement = 3 -) - -type JSONStringOrArray []string - -func (jsoa *JSONStringOrArray) UnmarshalJSON(data []byte) error { - var str string - if json.Unmarshal(data, &str) == nil { - *jsoa = []string{str} - return nil - } - var strs []string - json.Unmarshal(data, &strs) - *jsoa = strs - return nil -} - -type MsgInfo struct { - Command MsgInfoCommand `json:"cmd"` - IDs JSONStringOrArray `json:"id"` - Acknowledgement Acknowledgement `json:"ack"` - MessageFromJID string `json:"from"` - SenderJID string `json:"participant"` - ToJID string `json:"to"` - Timestamp int64 `json:"t"` -} - -type MsgInfoHandler interface { - whatsapp.Handler - HandleMsgInfo(MsgInfo) -} - -func (ext *ExtendedConn) handleMessageMsgInfo(msgType JSONMessageType, message []byte) { - var event MsgInfo - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - event.MessageFromJID = strings.Replace(event.MessageFromJID, OldUserSuffix, NewUserSuffix, 1) - event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1) - event.ToJID = strings.Replace(event.ToJID, OldUserSuffix, NewUserSuffix, 1) - if msgType == MessageMsg { - event.SenderJID = event.ToJID - } - for _, handler := range ext.handlers { - msgInfoHandler, ok := handler.(MsgInfoHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(msgInfoHandler) { - msgInfoHandler.HandleMsgInfo(event) - } else { - go msgInfoHandler.HandleMsgInfo(event) - } - } -} diff --git a/whatsapp-ext/presence.go b/whatsapp-ext/presence.go deleted file mode 100644 index 01365d2..0000000 --- a/whatsapp-ext/presence.go +++ /dev/null @@ -1,67 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - "strings" - - "github.com/Rhymen/go-whatsapp" -) - -type Presence struct { - JID string `json:"id"` - SenderJID string `json:"participant"` - Status whatsapp.Presence `json:"type"` - Timestamp int64 `json:"t"` - Deny bool `json:"deny"` -} - -type PresenceHandler interface { - whatsapp.Handler - HandlePresence(Presence) -} - -func (ext *ExtendedConn) handleMessagePresence(message []byte) { - var event Presence - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - event.JID = strings.Replace(event.JID, OldUserSuffix, NewUserSuffix, 1) - if len(event.SenderJID) == 0 { - if strings.Index(event.JID,"@g.us") > -1 { - return - } - event.SenderJID = event.JID - } else { - event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1) - } - for _, handler := range ext.handlers { - presenceHandler, ok := handler.(PresenceHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(presenceHandler) { - presenceHandler.HandlePresence(event) - } else { - go presenceHandler.HandlePresence(event) - } - } -} diff --git a/whatsapp-ext/props.go b/whatsapp-ext/props.go deleted file mode 100644 index 4245683..0000000 --- a/whatsapp-ext/props.go +++ /dev/null @@ -1,73 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - - "github.com/Rhymen/go-whatsapp" -) - -type ProtocolProps struct { - WebPresence bool `json:"webPresence"` - NotificationQuery bool `json:"notificationQuery"` - FacebookCrashLog bool `json:"fbCrashlog"` - Bucket string `json:"bucket"` - GIFSearch string `json:"gifSearch"` - Spam bool `json:"SPAM"` - SetBlock bool `json:"SET_BLOCK"` - MessageInfo bool `json:"MESSAGE_INFO"` - MaxFileSize int `json:"maxFileSize"` - Media int `json:"media"` - GroupNameLength int `json:"maxSubject"` - GroupDescriptionLength int `json:"groupDescLength"` - MaxParticipants int `json:"maxParticipants"` - VideoMaxEdge int `json:"videoMaxEdge"` - ImageMaxEdge int `json:"imageMaxEdge"` - ImageMaxKilobytes int `json:"imageMaxKBytes"` - Edit int `json:"edit"` - FwdUIStartTimestamp int `json:"fwdUiStartTs"` - GroupsV3 int `json:"groupsV3"` - RestrictGroups int `json:"restrictGroups"` - AnnounceGroups int `json:"announceGroups"` -} - -type ProtocolPropsHandler interface { - whatsapp.Handler - HandleProtocolProps(ProtocolProps) -} - -func (ext *ExtendedConn) handleMessageProps(message []byte) { - var event ProtocolProps - err := json.Unmarshal(message, &event) - if err != nil { - ext.jsonParseError(err) - return - } - for _, handler := range ext.handlers { - protocolPropsHandler, ok := handler.(ProtocolPropsHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(protocolPropsHandler) { - protocolPropsHandler.HandleProtocolProps(event) - } else { - go protocolPropsHandler.HandleProtocolProps(event) - } - } -} diff --git a/whatsapp-ext/protomessage.go b/whatsapp-ext/protomessage.go deleted file mode 100644 index ca00f05..0000000 --- a/whatsapp-ext/protomessage.go +++ /dev/null @@ -1,59 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "github.com/Rhymen/go-whatsapp" - "github.com/Rhymen/go-whatsapp/binary/proto" -) - -type MessageRevokeHandler interface { - whatsapp.Handler - HandleMessageRevoke(key MessageRevocation) -} - -type MessageRevocation struct { - Id string - RemoteJid string - FromMe bool - Participant string -} - -func (ext *ExtendedConn) HandleRawMessage(message *proto.WebMessageInfo) { - protoMsg := message.GetMessage().GetProtocolMessage() - if protoMsg != nil && protoMsg.GetType() == proto.ProtocolMessage_REVOKE { - key := protoMsg.GetKey() - deletedMessage := MessageRevocation{ - Id: key.GetId(), - RemoteJid: key.GetRemoteJid(), - FromMe: key.GetFromMe(), - Participant: key.GetParticipant(), - } - for _, handler := range ext.handlers { - mrHandler, ok := handler.(MessageRevokeHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(mrHandler) { - mrHandler.HandleMessageRevoke(deletedMessage) - } else { - go mrHandler.HandleMessageRevoke(deletedMessage) - } - } - } -} diff --git a/whatsapp-ext/stream.go b/whatsapp-ext/stream.go deleted file mode 100644 index bd82672..0000000 --- a/whatsapp-ext/stream.go +++ /dev/null @@ -1,68 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - - "github.com/Rhymen/go-whatsapp" -) - -type StreamType string - -const ( - StreamUpdate = "update" - StreamSleep = "asleep" -) - -type StreamEvent struct { - Type StreamType - Boolean bool - Version string -} - -type StreamEventHandler interface { - whatsapp.Handler - HandleStreamEvent(StreamEvent) -} - -func (ext *ExtendedConn) handleMessageStream(message []json.RawMessage) { - var event StreamEvent - err := json.Unmarshal(message[0], &event.Type) - if err != nil { - ext.jsonParseError(err) - return - } - - if event.Type == StreamUpdate && len(message) > 4 { - json.Unmarshal(message[1], event.Boolean) - json.Unmarshal(message[2], event.Version) - } - - for _, handler := range ext.handlers { - streamHandler, ok := handler.(StreamEventHandler) - if !ok { - continue - } - - if ext.shouldCallSynchronously(streamHandler) { - streamHandler.HandleStreamEvent(event) - } else { - go streamHandler.HandleStreamEvent(event) - } - } -} diff --git a/whatsapp-ext/whatsapp.go b/whatsapp-ext/whatsapp.go deleted file mode 100644 index 5c356c3..0000000 --- a/whatsapp-ext/whatsapp.go +++ /dev/null @@ -1,206 +0,0 @@ -// matrix-skype - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2019 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package whatsappExt - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "strings" - - "github.com/Rhymen/go-whatsapp" -) - -const ( - OldUserSuffix = "@c.us" - //NewUserSuffix = "@s.whatsapp.net" - NewUserSuffix = "@s.skype.net" -) - -type ExtendedConn struct { - *whatsapp.Conn - - handlers []whatsapp.Handler -} - -func ExtendConn(conn *whatsapp.Conn) *ExtendedConn { - ext := &ExtendedConn{ - Conn: conn, - } - ext.Conn.AddHandler(ext) - return ext -} - -func (ext *ExtendedConn) AddHandler(handler whatsapp.Handler) { - ext.Conn.AddHandler(handler) - ext.handlers = append(ext.handlers, handler) -} - - -func (ext *ExtendedConn) RemoveHandler(handler whatsapp.Handler) bool { - ext.Conn.RemoveHandler(handler) - for i, v := range ext.handlers { - if v == handler { - ext.handlers = append(ext.handlers[:i], ext.handlers[i+1:]...) - return true - } - } - return false -} - -func (ext *ExtendedConn) RemoveHandlers() { - ext.Conn.RemoveHandlers() - ext.handlers = make([]whatsapp.Handler, 0) -} - -func (ext *ExtendedConn) shouldCallSynchronously(handler whatsapp.Handler) bool { - sh, ok := handler.(whatsapp.SyncHandler) - return ok && sh.ShouldCallSynchronously() -} - -func (ext *ExtendedConn) ShouldCallSynchronously() bool { - return true -} - -type GroupInfo struct { - JID string `json:"jid"` - OwnerJID string `json:"owner"` - - Name string `json:"subject"` - NameSetTime int64 `json:"subjectTime"` - NameSetBy string `json:"subjectOwner"` - - Topic string `json:"desc"` - TopicID string `json:"descId"` - TopicSetAt int64 `json:"descTime"` - TopicSetBy string `json:"descOwner"` - - GroupCreated int64 `json:"creation"` - - Status int16 `json:"status"` - - Participants []struct { - JID string `json:"id"` - IsAdmin bool `json:"isAdmin"` - IsSuperAdmin bool `json:"isSuperAdmin"` - } `json:"participants"` -} - -func (ext *ExtendedConn) GetGroupMetaData(jid string) (*GroupInfo, error) { - data, err := ext.Conn.GetGroupMetaData(jid) - if err != nil { - return nil, fmt.Errorf("failed to get group metadata: %v", err) - } - content := <-data - - info := &GroupInfo{} - err = json.Unmarshal([]byte(content), info) - if err != nil { - return info, fmt.Errorf("failed to unmarshal group metadata: %v", err) - } - - for index, participant := range info.Participants { - info.Participants[index].JID = strings.Replace(participant.JID, OldUserSuffix, NewUserSuffix, 1) - } - info.NameSetBy = strings.Replace(info.NameSetBy, OldUserSuffix, NewUserSuffix, 1) - info.TopicSetBy = strings.Replace(info.TopicSetBy, OldUserSuffix, NewUserSuffix, 1) - - return info, nil -} - -type ProfilePicInfo struct { - URL string `json:"eurl"` - Tag string `json:"tag"` - - Status int16 `json:"status"` -} - -func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) { - resp, err := http.Get(ppi.URL) - if err != nil { - return nil, err - } - return resp.Body, nil -} - -func (ppi *ProfilePicInfo) DownloadBytes() ([]byte, error) { - body, err := ppi.Download() - if err != nil { - return nil, err - } - defer body.Close() - data, err := ioutil.ReadAll(body) - return data, err -} - -func (ext *ExtendedConn) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) { - data, err := ext.Conn.GetProfilePicThumb(jid) - if err != nil { - return nil, fmt.Errorf("failed to get avatar: %v", err) - } - content := <-data - info := &ProfilePicInfo{} - err = json.Unmarshal([]byte(content), info) - if err != nil { - return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) - } - return info, nil -} - -func (ext *ExtendedConn) HandleGroupInvite(groupJid string, numbers[]string) (err error) { - var parts []string - parts = append(parts, numbers...) - _, err = ext.Conn.AddMember(groupJid, parts) - if err != nil { - fmt.Printf("%s Handle Invite err", err) - } - return -} - -func (ext *ExtendedConn) HandleGroupJoin(code string) (jid string, err error) { - return ext.Conn.GroupAcceptInviteCode(code) -} - -func (ext *ExtendedConn) HandleGroupKick(groupJid string, numbers[]string) (err error) { - var parts []string - parts = append(parts, numbers...) - _, err = ext.Conn.RemoveMember(groupJid, parts) - if err != nil { - fmt.Printf("%s Handle kick err", err) - } - return -} - -func (ext *ExtendedConn) HandleGroupCreate(subject string, numbers[]string) (err error) { - var parts []string - parts = append(parts, numbers...) - _, err = ext.Conn.CreateGroup(subject, parts) - if err != nil { - fmt.Printf("%s HandleGroupCreate err", err) - } - return -} - -func (ext *ExtendedConn) HandleGroupLeave(groupJid string) (err error) { - _, err = ext.Conn.LeaveGroup(groupJid) - if err != nil { - fmt.Printf("%s HandleGroupLeave err", err) - } - return -} From c4ed4894067f38aa87a946c69eadfcea725ff883 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 3 Dec 2020 22:25:14 +0800 Subject: [PATCH 046/109] update command "create" --- commands.go | 113 +++++++++++++++++++++++++++++++++++++++------------- matrix.go | 2 +- 2 files changed, 86 insertions(+), 29 deletions(-) diff --git a/commands.go b/commands.go index 2df83ca..819dcf4 100644 --- a/commands.go +++ b/commands.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" skype "github.com/kelaresg/go-skypeapi" "github.com/kelaresg/matrix-skype/database" @@ -38,6 +39,7 @@ func NewCommandHandler(bridge *Bridge) *CommandHandler { type CommandEvent struct { Bot *appservice.IntentAPI Bridge *Bridge + Portal *Portal Handler *CommandHandler RoomID id.RoomID User *User @@ -142,7 +144,7 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { handler.CommandCreate(ce) } default: - ce.Reply("Unknown Command") + handler.CommandSpecialMux(ce) } } @@ -1151,50 +1153,105 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) { // } //} -const cmdCreateHelp = `create <_topic_> <_member user id_>,... - Create a group.` +const cmdCreateHelp = `create - Create a group chat.` func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { - if len(ce.Args) < 2 { - ce.Reply("**Usage:** `create ,...`") + if ce.Portal != nil { + ce.Reply("This is already a portal room") return } - user := ce.User - topic := ce.Args[0] - members := skype.Members{} + members, err := ce.Bot.JoinedMembers(ce.RoomID) + if err != nil { + ce.Reply("Failed to get room members: %v", err) + return + } - // The user who created the group must be in the members and have "Admin" rights - userId := ce.User.Conn.UserProfile.Username + var roomNameEvent event.RoomNameEventContent + err = ce.Bot.StateEvent(ce.RoomID, event.StateRoomName, "", &roomNameEvent) + if err != nil && !errors.Is(err, mautrix.MNotFound) { + ce.Reply("Failed to get room name") + return + } else if len(roomNameEvent.Name) == 0 { + ce.Reply("Please set a name for the room first") + return + } + + var encryptionEvent event.EncryptionEventContent + err = ce.Bot.StateEvent(ce.RoomID, event.StateEncryption, "", &encryptionEvent) + if err != nil && !errors.Is(err, mautrix.MNotFound) { + ce.Reply("Failed to get room encryption status") + return + } + + var participants []string + for userID := range members.Joined { + jid, ok := handler.bridge.ParsePuppetMXID(userID) + if ok && jid != ce.User.JID { + participants = append(participants, jid) + } + } + + selfMembers := skype.Members{} member2 := skype.Member{ - Id: "8:" + userId, + Id: strings.Replace(ce.User.JID, skypeExt.NewUserSuffix,"", 1), Role: "Admin", } - members.Members = append(members.Members, member2) - members.Properties = skype.Properties{ + selfMembers.Members = append(selfMembers.Members, member2) + selfMembers.Properties = skype.Properties{ HistoryDisclosed: "true", - Topic: topic, + Topic: roomNameEvent.Name, + } + handler.log.Debugln("Create Group", roomNameEvent.Name, "with", selfMembers) + err = ce.User.Conn.HandleGroupCreate(selfMembers) + if err != nil { + ce.Reply("Failed to create group: %v", err) + return } - handler.log.Debugln("Create Group", topic, "with", members) - err := user.Conn.HandleGroupCreate(members) - inputArr := strings.Split(ce.Args[1], ",") - members = skype.Members{} - for _, memberId := range inputArr { - members.Members = append(members.Members, skype.Member{ + participantMembers := skype.Members{} + for _, participant := range participants { + participantArr := strings.Split(participant, "@") + memberId := id.Dec(participantArr[0]) + cond1 := "8-live-" + cond2 := "8-" + if strings.HasPrefix(memberId, cond1) { + memberId = strings.Replace(memberId, cond1, "8:live:", 1) + } else if strings.HasPrefix(memberId, cond2){ + memberId = strings.Replace(memberId, cond2, "8:", 1) + } + participantMembers.Members = append(participantMembers.Members, skype.Member{ Id: memberId, Role: "Admin", }) } - conversationId, ok := <-user.Conn.CreateChan + conversationId, ok := <-ce.User.Conn.CreateChan if ok { - err = user.Conn.AddMember(members, conversationId) - } - if err != nil { - ce.Reply("Please confirm that parameters is correct.") - } else { - ce.Reply("Syncing group list...") - time.Sleep(time.Duration(3) * time.Second) - ce.Reply("Syncing group list completed") + portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(conversationId)) + portal.roomCreateLock.Lock() + defer portal.roomCreateLock.Unlock() + if len(portal.MXID) != 0 { + portal.log.Warnln("Detected race condition in room creation") + // TODO race condition, clean up the old room + } + portal.MXID = ce.RoomID + portal.Name = roomNameEvent.Name + portal.Encrypted = encryptionEvent.Algorithm == id.AlgorithmMegolmV1 + if !portal.Encrypted && handler.bridge.Config.Bridge.Encryption.Default { + _, err = portal.MainIntent().SendStateEvent(portal.MXID, event.StateEncryption, "", &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}) + if err != nil { + portal.log.Warnln("Failed to enable e2be:", err) + } + portal.Encrypted = true + } + + portal.Update() + portal.UpdateBridgeInfo() + + err = ce.User.Conn.AddMember(participantMembers, conversationId) + ce.Reply("Successfully created Skype group %s", portal.Key.JID) } + + //ce.User.addPortalToCommunity(portal) } diff --git a/matrix.go b/matrix.go index 2c13705..4b66c4f 100644 --- a/matrix.go +++ b/matrix.go @@ -226,7 +226,7 @@ func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, pup _, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.") _, _ = intent.LeaveRoom(evt.RoomID) } else { - _, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a WhatsApp group.") + _, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a Skype group.") } } From 6bd6b9208ecd0a7c86a52f1b1c7128810d6ccef9 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 3 Dec 2020 22:25:51 +0800 Subject: [PATCH 047/109] add special commands --- commands-special.go | 96 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 commands-special.go diff --git a/commands-special.go b/commands-special.go new file mode 100644 index 0000000..ef76e2f --- /dev/null +++ b/commands-special.go @@ -0,0 +1,96 @@ +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + skype "github.com/kelaresg/go-skypeapi" + "strings" + "time" +) + +func (handler *CommandHandler) CommandSpecialMux(ce *CommandEvent) { + switch ce.Command { + case "special-create": + if !ce.User.HasSession() { + ce.Reply("You are not logged in. Use the `login` command to log into Skype.") + return + } + switch ce.Command { + case "special-create": + handler.CommandSpecialCreate(ce) + } + default: + ce.Reply("Unknown Command") + } +} + +func (handler *CommandHandler) CommandSpecialHelp(ce *CommandEvent) { + cmdPrefix := "" + if ce.User.ManagementRoom != ce.RoomID || ce.User.IsRelaybot { + cmdPrefix = handler.bridge.Config.Bridge.CommandPrefix + " " + } + + ce.Reply("* " + strings.Join([]string{ + cmdPrefix + cmdSpecialCreateHelp, + }, "\n* ")) +} + +const cmdSpecialCreateHelp = `special-create <_topic_> <_member user id_>,... - Create a group.` + +func (handler *CommandHandler) CommandSpecialCreate(ce *CommandEvent) { + if len(ce.Args) < 2 { + ce.Reply("**Usage:** `special-create ,...`") + return + } + + user := ce.User + topic := ce.Args[0] + members := skype.Members{} + + // The user who created the group must be in the members and have "Admin" rights + userId := ce.User.Conn.UserProfile.Username + member2 := skype.Member{ + Id: "8:" + userId, + Role: "Admin", + } + + members.Members = append(members.Members, member2) + members.Properties = skype.Properties{ + HistoryDisclosed: "true", + Topic: topic, + } + + handler.log.Debugln("Create Group", topic, "with", members) + err := user.Conn.HandleGroupCreate(members) + inputArr := strings.Split(ce.Args[1], ",") + members = skype.Members{} + for _, memberId := range inputArr { + members.Members = append(members.Members, skype.Member{ + Id: memberId, + Role: "Admin", + }) + } + conversationId, ok := <-user.Conn.CreateChan + if ok { + err = user.Conn.AddMember(members, conversationId) + } + if err != nil { + ce.Reply("Please confirm that parameters is correct.") + } else { + ce.Reply("Syncing group list...") + time.Sleep(time.Duration(3) * time.Second) + ce.Reply("Syncing group list completed") + } +} + From bc1988b4a9a8f635eb5ca1eed6351e181e829d65 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 4 Dec 2020 20:53:40 +0800 Subject: [PATCH 048/109] User ID can be encrypted --- commands.go | 15 +---- config/config.go | 5 ++ portal.go | 8 +-- skype-ext/userid.go | 154 -------------------------------------------- 4 files changed, 12 insertions(+), 170 deletions(-) delete mode 100644 skype-ext/userid.go diff --git a/commands.go b/commands.go index 819dcf4..c25db4d 100644 --- a/commands.go +++ b/commands.go @@ -7,7 +7,6 @@ import ( "github.com/kelaresg/matrix-skype/database" skypeExt "github.com/kelaresg/matrix-skype/skype-ext" "math" - "time" "sort" "strconv" @@ -1162,6 +1161,7 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { } members, err := ce.Bot.JoinedMembers(ce.RoomID) + handler.log.Debugln("Create Group-1", members) if err != nil { ce.Reply("Failed to get room members: %v", err) return @@ -1203,24 +1203,15 @@ func (handler *CommandHandler) CommandCreate(ce *CommandEvent) { HistoryDisclosed: "true", Topic: roomNameEvent.Name, } - handler.log.Debugln("Create Group", roomNameEvent.Name, "with", selfMembers) + handler.log.Debugln("Create Group", roomNameEvent.Name, "with", selfMembers, participants) err = ce.User.Conn.HandleGroupCreate(selfMembers) if err != nil { ce.Reply("Failed to create group: %v", err) return } - participantMembers := skype.Members{} for _, participant := range participants { - participantArr := strings.Split(participant, "@") - memberId := id.Dec(participantArr[0]) - cond1 := "8-live-" - cond2 := "8-" - if strings.HasPrefix(memberId, cond1) { - memberId = strings.Replace(memberId, cond1, "8:live:", 1) - } else if strings.HasPrefix(memberId, cond2){ - memberId = strings.Replace(memberId, cond2, "8:", 1) - } + memberId := strings.Replace(participant, skypeExt.NewUserSuffix, "", 1) participantMembers.Members = append(participantMembers.Members, skype.Member{ Id: memberId, Role: "Admin", diff --git a/config/config.go b/config/config.go index 78af0fd..208ed1b 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ package config import ( "io/ioutil" + "maunium.net/go/mautrix/patch" "gopkg.in/yaml.v2" @@ -98,6 +99,10 @@ func (config *Config) MakeAppService() (*appservice.AppService, error) { as.HomeserverURL = config.Homeserver.Address as.Host.Hostname = config.AppService.Hostname as.Host.Port = config.AppService.Port + patch.ThirdPartyIdEncrypt = true + patch.AsBotName = config.AppService.Bot.Username + patch.AsUserPrefix = "skype&" + patch.XorKey = "hudsds1y" var err error as.Registration, err = config.GetRegistration() return as, err diff --git a/portal.go b/portal.go index d818c28..ff67b1c 100644 --- a/portal.go +++ b/portal.go @@ -641,10 +641,10 @@ func (portal *Portal) SyncSkype(user *User, chat skype.Conversation) { } else { fmt.Println("SyncSkype ensureUserInvited", portal.MXID) portal.ensureUserInvited(user) - rep, err := portal.MainIntent().SetPowerLevel(portal.MXID, user.MXID, 95) - if err != nil { - portal.log.Warnfln("SyncSkype: SetPowerLevel err: ", err, rep) - } + //rep, err := portal.MainIntent().SetPowerLevel(portal.MXID, user.MXID, 95) + //if err != nil { + // portal.log.Warnfln("SyncSkype: SetPowerLevel err: ", err, rep) + //} //if portal.IsPrivateChat() { // preUserIds,_ := portal.GetMatrixUsers() diff --git a/skype-ext/userid.go b/skype-ext/userid.go deleted file mode 100644 index 2150b97..0000000 --- a/skype-ext/userid.go +++ /dev/null @@ -1,154 +0,0 @@ -package skypeExt - -import ( - "bytes" - "encoding/hex" - "fmt" - "strings" -) - -// UserID represents a Matrix user ID. -// https://matrix.org/docs/spec/appendices#user-identifiers -type UserID string - -func NewUserID(localpart, homeserver string) UserID { - return UserID(fmt.Sprintf("@%s:%s", localpart, homeserver)) -} - -func NewEncodedUserID(localpart, homeserver string) UserID { - return NewUserID(EncodeUserLocalpart(localpart), homeserver) -} - -// Parse parses the user ID into the localpart and server name. -// See http://matrix.org/docs/spec/intro.html#user-identifiers -func (userID UserID) Parse() (localpart, homeserver string, err error) { - if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') { - err = fmt.Errorf("%s is not a valid user id", userID) - return - } - parts := strings.SplitN(string(userID), ":", 2) - localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1] - return -} - -func (userID UserID) ParseAndDecode() (localpart, homeserver string, err error) { - localpart, homeserver, err = userID.Parse() - if err == nil { - localpart, err = DecodeUserLocalpart(localpart) - } - return -} - -func (userID UserID) String() string { - return string(userID) -} - -const lowerhex = "0123456789abcdef" - -// encode the given byte using quoted-printable encoding (e.g "=2f") -// and writes it to the buffer -// See https://golang.org/src/mime/quotedprintable/writer.go -func encode(buf *bytes.Buffer, b byte) { - buf.WriteByte('=') - buf.WriteByte(lowerhex[b>>4]) - buf.WriteByte(lowerhex[b&0x0f]) -} - -// escape the given alpha character and writes it to the buffer -func escape(buf *bytes.Buffer, b byte) { - buf.WriteByte('_') - if b == '_' { - buf.WriteByte('_') // another _ - } else { - buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z - } -} - -func shouldEncode(b byte) bool { - return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z') -} - -func shouldEscape(b byte) bool { - return (b >= 'A' && b <= 'Z') || b == '_' -} - -func isValidByte(b byte) bool { - return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-' -} - -func isValidEscapedChar(b byte) bool { - return b == '_' || (b >= 'a' && b <= 'z') -} - -// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form. -// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets -// -// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z -// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges -// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs) -// and converted to lower-case hex with a leading "=". For example: -// Alph@Bet_50up => _alph=40_bet=5f50up -func EncodeUserLocalpart(str string) string { - strBytes := []byte(str) - var outputBuffer bytes.Buffer - for _, b := range strBytes { - if shouldEncode(b) { - encode(&outputBuffer, b) - } else if shouldEscape(b) { - escape(&outputBuffer, b) - } else { - outputBuffer.WriteByte(b) - } - } - return outputBuffer.String() -} - -// DecodeUserLocalpart decodes the given string back into the original input string. -// Returns an error if the given string is not a valid user ID localpart encoding. -// See http://matrix.org/docs/spec/intro.html#mapping-from-other-character-sets -// -// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For -// example: -// _alph=40_bet=5f50up => Alph@Bet_50up -// Returns an error if the input string contains characters outside the -// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has -// an invalid _ escaped byte (e.g. "_5"). -func DecodeUserLocalpart(str string) (string, error) { - strBytes := []byte(str) - var outputBuffer bytes.Buffer - for i := 0; i < len(strBytes); i++ { - b := strBytes[i] - if !isValidByte(b) { - return "", fmt.Errorf("Byte pos %d: Invalid byte", i) - } - - if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _ - if i+1 >= len(strBytes) { - return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i) - } - if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping - return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i) - } - if strBytes[i+1] == '_' { - outputBuffer.WriteByte('_') - } else { - outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z - } - i++ // skip next byte since we just handled it - } else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8 - if i+2 >= len(strBytes) { - return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i) - } - dst := make([]byte, 1) - _, err := hex.Decode(dst, strBytes[i+1:i+3]) - if err != nil { - return "", err - } - outputBuffer.WriteByte(dst[0]) - i += 2 // skip next 2 bytes since we just handled it - } else { // pass through - outputBuffer.WriteByte(b) - } - } - return outputBuffer.String(), nil -} From 64ed2c1474ebed3845977c2bf0225b61746bc31b Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 4 Dec 2020 20:59:43 +0800 Subject: [PATCH 049/109] fix invite puppet --- portal.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/portal.go b/portal.go index ff67b1c..c415c2a 100644 --- a/portal.go +++ b/portal.go @@ -2342,17 +2342,14 @@ func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) { } func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) { - number, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) - puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) - fmt.Println("HandleMatrixInvite", puppet) + jid, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) + puppet := portal.bridge.GetPuppetByJID(jid) if puppet != nil { - number = strings.Replace(number, "8:", "", 1) - number = strings.Replace(number, skypeExt.NewUserSuffix, "", 1) - err := sender.Conn.HandleGroupInvite(portal.Key.JID, []string{number}) + jid = strings.Replace(jid, skypeExt.NewUserSuffix, "", 1) + err := sender.Conn.HandleGroupInvite(portal.Key.JID, []string{jid}) if err != nil { portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err) return } - //portal.log.Infoln("Add %s response: %s", puppet.JID, <-resp) } } From f4499a63841f09ce2743692297ff133d072e787f Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 8 Dec 2020 11:52:19 +0800 Subject: [PATCH 050/109] better --- matrix.go | 23 +++++++++++++++++++++-- portal.go | 5 ++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/matrix.go b/matrix.go index 4b66c4f..36e1c37 100644 --- a/matrix.go +++ b/matrix.go @@ -5,6 +5,7 @@ import ( skype "github.com/kelaresg/go-skypeapi" "github.com/kelaresg/matrix-skype/database" "maunium.net/go/mautrix" + "maunium.net/go/mautrix/patch" "strconv" "strings" "time" @@ -210,7 +211,15 @@ func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, pup } var hasBridgeBot, hasOtherUsers bool for mxid, _ := range members.Joined { - if mxid == intent.UserID || mxid == inviter.MXID { + fmt.Println() + fmt.Println() + fmt.Println("HandlePuppetInvite mxid", mxid) + fmt.Println("HandlePuppetInvite intent.UserID", intent.UserID) + fmt.Println("HandlePuppetInvite patch.Parse(intent.UserID)", id.UserID(patch.Parse(string(intent.UserID)))) + fmt.Println("HandlePuppetInvite inviter.MXID", inviter.MXID) + fmt.Println() + fmt.Println() + if mxid == id.UserID(patch.Parse(string(intent.UserID))) || mxid == inviter.MXID { continue } else if mxid == mx.bridge.Bot.UserID { hasBridgeBot = true @@ -223,7 +232,7 @@ func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, pup mx.handlePrivatePortal(evt.RoomID, inviter, puppet, key) } else if !hasBridgeBot { mx.log.Debugln("Leaving multi-user room", evt.RoomID, "as", puppet.MXID, "after accepting invite from", evt.Sender) - _, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.") + _, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a skype group.") _, _ = intent.LeaveRoom(evt.RoomID) } else { _, _ = intent.SendNotice(evt.RoomID, "This puppet will remain inactive until this room is bridged to a Skype group.") @@ -231,6 +240,8 @@ func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, pup } func (mx *MatrixHandler) HandleMembership(evt *event.Event) { + fmt.Println("HandleMembership0 evt.Sender:", evt.Sender) + fmt.Println("HandleMembership0 evt.GetStateKey:", evt.GetStateKey()) if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { return } @@ -274,6 +285,14 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { } } } else { + fmt.Println() + fmt.Println() + fmt.Println("HandleMembership evt.RoomID", evt.RoomID) + fmt.Println("HandleMembership id.UserID(evt.GetStateKey())", id.UserID(evt.GetStateKey())) + fmt.Println("HandleMembership event.MembershipLeave", event.MembershipLeave) + fmt.Println("HandleMembership user.", event.MembershipLeave) + fmt.Println() + //mx.as.StateStore.SetMembership(evt.RoomID, id.UserID(evt.GetStateKey()), event.MembershipLeave) portal.HandleMatrixKick(user, evt) } } else if content.Membership == event.MembershipInvite && !isSelf { diff --git a/portal.go b/portal.go index c415c2a..417ce83 100644 --- a/portal.go +++ b/portal.go @@ -1486,13 +1486,12 @@ func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Res geo := fmt.Sprintf("geo:%.6f,%.6f", float32(latitude)/1000000, float32(longitude)/1000000) content := &event.MessageEventContent{ MsgType: event.MsgText, - Body: fmt.Sprintf("Location: %s%s
", locationMessage.A.Href, locationMessage.Address, geo), + Body: fmt.Sprintf("Location: %s%s
\n", locationMessage.A.Href, locationMessage.Address, geo), Format: event.FormatHTML, - FormattedBody: fmt.Sprintf("Location: %s%s
", locationMessage.A.Href, locationMessage.Address, geo), + FormattedBody: fmt.Sprintf("Location: %s%s
\n", locationMessage.A.Href, locationMessage.Address, geo), GeoURI: geo, } - //portal.SetReply(content, message.ContextInfo) portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) From be7c67981b66f50c9927e3bd9896db2e88160aa6 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 8 Dec 2020 14:54:32 +0800 Subject: [PATCH 051/109] fix kick/remove puppet --- portal.go | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/portal.go b/portal.go index 417ce83..b848d79 100644 --- a/portal.go +++ b/portal.go @@ -2,16 +2,14 @@ package main import ( "bytes" - //whatsappExt "github.com/kelaresg/matrix-skype/whatsapp-ext" + "maunium.net/go/mautrix/patch" - //"encoding/gob" "encoding/hex" "encoding/json" "encoding/xml" "fmt" skype "github.com/kelaresg/go-skypeapi" skypeExt "github.com/kelaresg/matrix-skype/skype-ext" - //whatsappExt "github.com/kelaresg/matrix-skype/whatsapp-ext" "html" "image" "image/gif" @@ -28,14 +26,9 @@ import ( "github.com/pkg/errors" log "maunium.net/go/maulogger/v2" - - "maunium.net/go/mautrix/crypto/attachment" - - //"github.com/Rhymen/go-whatsapp" - //waProto "github.com/Rhymen/go-whatsapp/binary/proto" - "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/crypto/attachment" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" @@ -43,7 +36,6 @@ import ( "github.com/kelaresg/matrix-skype/database" "github.com/kelaresg/matrix-skype/types" - //"github.com/kelaresg/matrix-skype/whatsapp-ext" ) func (bridge *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal { @@ -779,7 +771,7 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { // UserID: member.MXID, // }) // if err != nil { -// portal.log.Errorln("Error %s member from whatsapp: %v", action, err) +// portal.log.Errorln("Error %s member from skype: %v", action, err) // } // } //} @@ -789,13 +781,14 @@ func (portal *Portal) membershipRemove(content string) { err := xml.Unmarshal([]byte(content), &xmlFormat) for _, target := range xmlFormat.Targets { member := portal.bridge.GetPuppetByJID(target) - - memberMaxid := strings.Replace(string(member.MXID), "@skype&8:", "@skype&8-", 1) - _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ - UserID: id.UserID(memberMaxid), - }) - if err != nil { - portal.log.Errorln("Error %v member from whatsapp:", err) + memberMXID := id.UserID(patch.Parse(string(member.MXID))) + if portal.bridge.AS.StateStore.IsInRoom(portal.MXID, memberMXID) { + _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ + UserID: member.MXID, + }) + if err != nil { + portal.log.Errorln("Error kick member from matrix after kick from skype: %v", err) + } } } } @@ -2326,17 +2319,15 @@ func (portal *Portal) HandleMatrixLeave(sender *User) { } func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) { - number, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) - puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey())) - fmt.Println("HandleMatrixKick", puppet) + jid, _:= portal.bridge.ParsePuppetMXID(id.UserID(evt.GetStateKey())) + puppet := portal.bridge.GetPuppetByJID(jid) if puppet != nil { - number = strings.Replace(number, skypeExt.NewUserSuffix, "", 1) - err := sender.Conn.HandleGroupKick(portal.Key.JID, []string{number}) + jid = strings.Replace(jid, skypeExt.NewUserSuffix, "", 1) + err := sender.Conn.HandleGroupKick(portal.Key.JID, []string{jid}) if err != nil { portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err) return } - //portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp) } } From b62a28ef6489db5db8ca33e36cfdc6688aad6a06 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 8 Dec 2020 16:25:09 +0800 Subject: [PATCH 052/109] can set puppet id encryption in the config.yaml --- config/bridge.go | 6 ++++++ config/config.go | 6 +++--- example-config.yaml | 8 ++++++++ go.mod | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index 0c4e019..31b08ac 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -61,6 +61,12 @@ type BridgeConfig struct { RequireCrossSigning bool `yaml:"require_cross_signing"` RequireVerification bool `yaml:"require_verification"` } `yaml:"key_sharing"` + + PuppetId struct { + Allow bool `yaml:"allow"` + Key string `yaml:"key"` + UsernameTemplatePrefix string `yaml:"username_template_prefix"` + } `yaml:"puppet_id"` } `yaml:"encryption"` Permissions PermissionConfig `yaml:"permissions"` diff --git a/config/config.go b/config/config.go index 208ed1b..fb19b7d 100644 --- a/config/config.go +++ b/config/config.go @@ -99,10 +99,10 @@ func (config *Config) MakeAppService() (*appservice.AppService, error) { as.HomeserverURL = config.Homeserver.Address as.Host.Hostname = config.AppService.Hostname as.Host.Port = config.AppService.Port - patch.ThirdPartyIdEncrypt = true + patch.ThirdPartyIdEncrypt = config.Bridge.Encryption.PuppetId.Allow patch.AsBotName = config.AppService.Bot.Username - patch.AsUserPrefix = "skype&" - patch.XorKey = "hudsds1y" + patch.AsUserPrefix = config.Bridge.Encryption.PuppetId.UsernameTemplatePrefix + patch.XorKey = config.Bridge.Encryption.PuppetId.Key var err error as.Registration, err = config.GetRegistration() return as, err diff --git a/example-config.yaml b/example-config.yaml index 78c8cfc..d0c4d59 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -168,6 +168,14 @@ bridge: # It is recommended to also set private_chat_portal_meta to true when using this. default: false + puppet_id: + # when set to true, the matrixid of the contact (puppet) from the bridge to the matrix will be encrypted into another string + default: false + # 8 characters + key: '12dsf323' + # Use the username_template prefix. (Warning: At present, username_template cannot be too complicated, otherwise this function may cause unknown errors) + username_template_prefix: 'skype&' + # Permissions for using the bridge. # Permitted values: # relaybot - Talk through the relaybot (if enabled), no access otherwise diff --git a/go.mod b/go.mod index ac80bb8..b0f7011 100644 --- a/go.mod +++ b/go.mod @@ -18,4 +18,4 @@ require ( maunium.net/go/mautrix v0.8.0-rc.4 ) -replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc +replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201208081810-787323a21113 From fe59155d3a49fdc8b404d2b25367c4e0caef9b1c Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 11 Dec 2020 20:05:13 +0800 Subject: [PATCH 053/109] do not record password in log --- commands.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commands.go b/commands.go index c25db4d..9deb7b0 100644 --- a/commands.go +++ b/commands.go @@ -72,6 +72,9 @@ func (handler *CommandHandler) Handle(roomID id.RoomID, user *User, message stri Command: strings.ToLower(args[0]), Args: args[1:], } + if ce.Command == "login" { + message = "" + } handler.log.Debugfln("%s sent '%s' in %s", user.MXID, message, roomID) if roomID == handler.bridge.Config.Bridge.Relaybot.ManagementRoom { handler.CommandRelaybot(ce) From 5b6d5024aa706cdf1bd5d7fc4e4d8a2b7bc20cd2 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sun, 13 Dec 2020 15:00:46 +0800 Subject: [PATCH 054/109] update go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b0f7011..a4fda10 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/chai2010/webp v1.1.0 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858 + github.com/kelaresg/go-skypeapi v0.1.2-0.20201211120317-8651f9f08575 github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 From 3c3ae50a1e37941d72b027cde07471c02ad935ce Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Sun, 13 Dec 2020 15:29:24 +0800 Subject: [PATCH 055/109] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index c4e449f..18ea5e4 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). -~~Note: Use `go get github.com/kelaresg/go-skypeapi@{latest_commit_id}`, for now is: `go get github.com/kelaresg/go-skypeapi@226d1ec92858504b03e32017cd007420b3d7f205`~~ This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 16af59c848443e0aa99938a34edf592b465badce Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 14 Dec 2020 18:35:35 +0800 Subject: [PATCH 056/109] adjust the response notice after creating a room --- commands.go | 8 ++++---- portal.go | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/commands.go b/commands.go index 9deb7b0..aaf075d 100644 --- a/commands.go +++ b/commands.go @@ -50,11 +50,11 @@ type CommandEvent struct { func (ce *CommandEvent) Reply(msg string, args ...interface{}) { content := format.RenderMarkdown(fmt.Sprintf(msg, args...), true, false) content.MsgType = event.MsgNotice - room := ce.User.ManagementRoom - if len(room) == 0 { - room = ce.RoomID + intent := ce.Bot + if ce.Portal != nil && ce.Portal.IsPrivateChat() { + intent = ce.Portal.MainIntent() } - _, err := ce.Bot.SendMessageEvent(room, event.EventMessage, content) + _, err := intent.SendMessageEvent(ce.RoomID, event.EventMessage, content) if err != nil { ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err) } diff --git a/portal.go b/portal.go index b848d79..069c9fc 100644 --- a/portal.go +++ b/portal.go @@ -1044,7 +1044,8 @@ func (portal *Portal) getBridgeInfo() (string, BridgeInfoContent) { AvatarURL: portal.AvatarURL.CUString(), }, } - bridgeInfoStateKey := fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) + // bridgeInfoStateKey := fmt.Sprintf("net.maunium.whatsapp://whatsapp/%s", portal.Key.JID) ?? + bridgeInfoStateKey := portal.Key.JID return bridgeInfoStateKey, bridgeInfo } @@ -2290,6 +2291,9 @@ func (portal *Portal) Cleanup(puppetsOnly bool) { } else if !puppetsOnly { _, err = intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"}) if err != nil { + content := format.RenderMarkdown("Error leaving room(Deleting portal from skype), you can leave this room manually.", true, false) + content.MsgType = event.MsgNotice + _, _ = portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, content) portal.log.Errorln("Error kicking user while cleaning up portal:", err) } } From da535e73462b467ce887875137d02ee59ab07fd4 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 14 Dec 2020 19:25:07 +0800 Subject: [PATCH 057/109] Fix the bug that messages cannot be deleted synchronously if the room is created in the matrix client (maybe there is a better way) --- portal.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/portal.go b/portal.go index 069c9fc..a46c5f7 100644 --- a/portal.go +++ b/portal.go @@ -1338,6 +1338,10 @@ func (portal *Portal) HandleMessageRevokeSkype(user *User, message skype.Resourc } _, err := intent.RedactEvent(portal.MXID, msg.MXID) if err != nil { + // TODO Maybe there is a better implementation + if strings.Index(err.Error(), "M_FORBIDDEN") > -1 { + _, err = portal.MainIntent().RedactEvent(portal.MXID, msg.MXID) + } portal.log.Errorln("Failed to redact %s: %v", msg.JID, err) return } From 06a7d25ec4eb55cd64f0cc8b3cd6ca04476f4f83 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 17 Dec 2020 16:54:28 +0800 Subject: [PATCH 058/109] get old skype account username --- user.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user.go b/user.go index 216e94d..e0b7259 100644 --- a/user.go +++ b/user.go @@ -372,6 +372,9 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err if len(user.Conn.UserProfile.LastName) > 0 { username = username + user.Conn.UserProfile.LastName } + if username == "" { + username = user.Conn.UserProfile.Username + } ce.Reply("Successfully logged in as @" + username) user.Conn.Subscribes() // subscribe basic event From f892087014b1e635afa472730aab9f65a5f66d00 Mon Sep 17 00:00:00 2001 From: Dan Pastusek Date: Mon, 21 Dec 2020 16:02:22 -0700 Subject: [PATCH 059/109] fix for postgres schemas --- database/upgrades/2020-08-25-message-id-column.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/upgrades/2020-08-25-message-id-column.go b/database/upgrades/2020-08-25-message-id-column.go index a74f824..00a862c 100644 --- a/database/upgrades/2020-08-25-message-id-column.go +++ b/database/upgrades/2020-08-25-message-id-column.go @@ -6,7 +6,7 @@ import ( func init() { upgrades[17] = upgrade{"Add id column to messages", func(tx *sql.Tx, ctx context) error { - _, err := tx.Exec(`ALTER TABLE message ADD COLUMN id CHAR(13) DEFAULT ""`) + _, err := tx.Exec(`ALTER TABLE message ADD COLUMN id CHAR(13) DEFAULT ''`) if err != nil { return err } From 3c28de4155e0899b03cddd2921d0b21acaadb8d6 Mon Sep 17 00:00:00 2001 From: Tom Price Date: Wed, 23 Dec 2020 01:30:08 +0000 Subject: [PATCH 060/109] Unescape HTML in a skype message to prevent element issues. This remains secure as matrix clients SHOULD only render specific HTML tags, so there is no expectation that server side messages are secure by default. Add basic tests for formatter.parseSkype for the new escaping bug. This could be expanded in the future to include all the other functions. Closes #1 --- formatting.go | 2 + formatting_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 formatting_test.go diff --git a/formatting.go b/formatting.go index 22e0ff6..f87d147 100644 --- a/formatting.go +++ b/formatting.go @@ -18,6 +18,7 @@ var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)") var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)") var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)") var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```") + //var mentionRegex = regexp.MustCompile("@[0-9]+") //var mentionRegex = regexp.MustCompile("@(.*)") var mentionRegex = regexp.MustCompile("]+\\bid=\"([^\"]+)\"(.*?)*") @@ -125,6 +126,7 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { } if output != content.Body { output = strings.Replace(output, "\n", "
", -1) + content.Body = html.UnescapeString(content.Body) // skype messages arrive escaped which causes element rendering issues #1 // parse @user message r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) diff --git a/formatting_test.go b/formatting_test.go new file mode 100644 index 0000000..6070483 --- /dev/null +++ b/formatting_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "github.com/kelaresg/matrix-skype/database" + "github.com/kelaresg/matrix-skype/types" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "reflect" + "regexp" + "sync" + "testing" +) + +func TestFormatter_ParseSkype(t *testing.T) { + type fields struct { + bridge *Bridge + matrixHTMLParser *format.HTMLParser + waReplString map[*regexp.Regexp]string + waReplFunc map[*regexp.Regexp]func(string) string + waReplFuncText map[*regexp.Regexp]func(string) string + } + type args struct { + content *event.MessageEventContent + } + type expect struct { + content *event.MessageEventContent + } + testUser := &User{ + User: &database.User{ + MXID: "mxtestid", + }, + } + testBridge := &Bridge{ + usersLock: *new(sync.Mutex), + usersByJID: map[types.SkypeID]*User{"test": testUser}, + } + testFormatter := &Formatter{ + bridge: testBridge, + } + tests := []struct { + name string + args args + expect expect + }{ + { + "simple message", + args{ + &event.MessageEventContent{ + Body: "This is a very simple message.", + }, + }, + expect{ + &event.MessageEventContent{ + Body: "This is a very simple message.", + }, + }, + }, + { + "simple punctuation test", + args{ + &event.MessageEventContent{ + Body: "It's the inclusion of "simple" punctuation that causes most of the problems.", + }, + }, + expect{ + &event.MessageEventContent{ + Body: "It's the inclusion of \"simple\" punctuation that causes most of the problems.", + Format: event.FormatHTML, + }, + }, + }, + { + "full punctuation test", + args{ + &event.MessageEventContent{ + Body: "&<>"'", // use a few different encodings + Format: event.FormatHTML, + }, + }, + expect{ + &event.MessageEventContent{ + Body: "&<>\"'", + Format: event.FormatHTML, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testFormatter.ParseSkype(tt.args.content) + if !reflect.DeepEqual(tt.args.content, tt.expect.content) { + t.Errorf("content = %v, wanted %v", tt.args.content, tt.expect.content) + } + }) + } +} From 443d8ca9d13021a389a4d5d35f74be35fa1ade02 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 12 Jan 2021 16:56:31 +0800 Subject: [PATCH 061/109] add double puppeting and community --- user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/user.go b/user.go index e0b7259..ae9d624 100644 --- a/user.go +++ b/user.go @@ -398,6 +398,7 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix user.addToJIDMap() _ = ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) + user.PostLogin() return } From 2b5abc74d2f24cb8c0d624b8c12ac154a90ad89c Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 12 Jan 2021 17:14:29 +0800 Subject: [PATCH 062/109] optimize double puppeting logic --- portal.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/portal.go b/portal.go index a46c5f7..8fefab4 100644 --- a/portal.go +++ b/portal.go @@ -342,7 +342,7 @@ func (portal *Portal) getMessageIntentSkype(user *User, info skype.Resource) *ap } func (portal *Portal) handlePrivateChatFromMe(fromMe bool) func() { - if portal.IsPrivateChat() && fromMe { + if portal.IsPrivateChat() && fromMe && len(portal.bridge.Config.Bridge.LoginSharedSecret) == 0 { var privateChatPuppet *Puppet var privateChatPuppetInvited bool privateChatPuppet = portal.bridge.GetPuppetByJID(portal.Key.Receiver) @@ -924,7 +924,9 @@ func (portal *Portal) beginBackfill() func() { portal.privateChatBackfillInvitePuppet = nil portal.backfillLock.Unlock() if privateChatPuppet != nil && privateChatPuppetInvited { - //_, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) + if len(portal.bridge.Config.Bridge.LoginSharedSecret) > 0 { + _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) + } } } } From 449e7865d4a0fcbefe828ac342305c4ee43af1d1 Mon Sep 17 00:00:00 2001 From: pidong <1163765691@qq.com> Date: Tue, 12 Jan 2021 17:27:16 +0800 Subject: [PATCH 063/109] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 18ea5e4..5c48bcb 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re * Typing status The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). +[matrix room(#goskypebridge:matrix.org)](https://app.element.io/#/room/#goskypebridge:matrix.org) This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) From 989cb8089963c5727d57bdfdbeb7a39e54bf8762 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 12 Jan 2021 20:17:11 +0800 Subject: [PATCH 064/109] fix mention user --- formatting.go | 10 ++++++---- portal.go | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/formatting.go b/formatting.go index f87d147..654fe68 100644 --- a/formatting.go +++ b/formatting.go @@ -81,7 +81,8 @@ func NewFormatter(bridge *Bridge) *Formatter { }, mentionRegex: func(str string) string { mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + skypeExt.NewUserSuffix) - return fmt.Sprintf(`%s`, mxid, displayname) + mxid = id.UserID(html.EscapeString(string(mxid))) + return fmt.Sprintf(`%s:`, bridge.Config.Homeserver.Domain, mxid, displayname) }, } formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{ @@ -96,7 +97,8 @@ func NewFormatter(bridge *Bridge) *Formatter { } } //mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) - return fmt.Sprintf(`%s`, mxid, displayname) + mxid = id.UserID(html.EscapeString(string(mxid))) + return fmt.Sprintf(`%s:`, bridge.Config.Homeserver.Domain, mxid, displayname) // _, displayname = formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) //fmt.Println("ParseWhatsAp4", displayname) //return displayname @@ -136,7 +138,7 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { if len(matches) > 0 { for _, match := range matches { mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) - content.FormattedBody = strings.ReplaceAll(content.Body, match[0], fmt.Sprintf(`%s`, mxid, displayname)) + content.FormattedBody = strings.ReplaceAll(content.Body, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) content.Body = content.FormattedBody } } @@ -151,7 +153,7 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { //href1 := fmt.Sprintf(`https://matrix.to/#/!kpouCkfhzvXgbIJmkP:oliver.matrix.host/$fHQNRydqqqAVS8usHRmXn0nIBM_FC-lo2wI2Uol7wu8?via=oliver.matrix.host`) href1 := "" //mxid `@skype&8-live-xxxxxx:name.matrix.server` - href2 := fmt.Sprintf(`https://matrix.to/#/%s`, mxid) + href2 := fmt.Sprintf(`https://%s/#/%s`, formatter.bridge.Config.Homeserver.Domain, mxid) newContent := fmt.Sprintf(`
%s
%s
%s`, href1, href2, diff --git a/portal.go b/portal.go index 8fefab4..718ad5d 100644 --- a/portal.go +++ b/portal.go @@ -1895,7 +1895,7 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) //replyToID := content.GetReplyTo() var newContent string //if len(replyToID) > 0 { - rQuote := regexp.MustCompile(`
]+\bhref="(.*?)://matrix\.to/#/@([^"]+):(.*?)">(.*?)
([^"]+)
(.*)`) + rQuote := regexp.MustCompile(`
]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)
([^"]+)
(.*)`) quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) fmt.Println("matches0: ", content.FormattedBody) fmt.Println("matches1: ", quoteMatches) @@ -1904,7 +1904,7 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) if len(match) > 2 { var skyId string if strings.Index(match[4], "@skype") > -1 { - skyId = strings.ReplaceAll(match[2], "skype&", "") + skyId = patch.ParseLocalPart(html.UnescapeString(match[2]), false) skyId = strings.ReplaceAll(skyId, "skype&", "") skyId = strings.ReplaceAll(skyId, "-", ":") } else { @@ -1970,13 +1970,16 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) text = "/me " + text } if len(content.FormattedBody) > 0 { - r := regexp.MustCompile(`]+\bhref="(.*?)://matrix\.to/#/@skype&([^"]+):(.*?)">(.*?)*`) + //r := regexp.MustCompile(`]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@skype&([^"]+):(.*?)">(.*?)*`) + r := regexp.MustCompile(`]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)*`) matches := r.FindAllStringSubmatch(content.FormattedBody, -1) fmt.Println("matches: ", matches) if len(matches) > 0 { for _, match := range matches { if len(match) > 2 { - skyId := strings.ReplaceAll(match[2], "-", ":") + skyId := patch.ParseLocalPart(html.UnescapeString(match[2]), false) + skyId = strings.ReplaceAll(skyId, "skype&", "") + skyId = strings.ReplaceAll(skyId, "-", ":") content.FormattedBody = strings.ReplaceAll(content.FormattedBody, match[0], fmt.Sprintf(`%s`, skyId, match[4])) } } From 613d45ed68b1d83381f6c5ba69ab9178ff1b3c74 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 13 Jan 2021 18:18:52 +0800 Subject: [PATCH 065/109] Filter starred(bookmarks) room --- user.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user.go b/user.go index ae9d624..25949e9 100644 --- a/user.go +++ b/user.go @@ -532,6 +532,10 @@ func (user *User) syncPortals(chatMap map[string]skype.Conversation, createAll b if chat.Id == "48:calllogs" { continue } + // Filter starred(bookmarks) + if chat.Id == "48:starred" { + continue + } // Filter conversations that have not sent messages if chat.LastMessage.Id == "" { continue From b494176a2622f41b0fa946c041c302d2c73879ee Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 14 Jan 2021 20:21:57 +0800 Subject: [PATCH 066/109] optimize skype->matrix message parse --- formatting.go | 62 +++++++++------------- portal.go | 4 +- test/main.go | 139 -------------------------------------------------- 3 files changed, 28 insertions(+), 177 deletions(-) delete mode 100644 test/main.go diff --git a/formatting.go b/formatting.go index 654fe68..c30c3f0 100644 --- a/formatting.go +++ b/formatting.go @@ -79,30 +79,8 @@ func NewFormatter(bridge *Bridge) *Formatter { } return fmt.Sprintf("%s", str) }, - mentionRegex: func(str string) string { - mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + skypeExt.NewUserSuffix) - mxid = id.UserID(html.EscapeString(string(mxid))) - return fmt.Sprintf(`%s:`, bridge.Config.Homeserver.Domain, mxid, displayname) - }, } formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{ - mentionRegex: func(str string) string { - r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) - matches := r.FindAllStringSubmatch(str, -1) - displayname := "" - var mxid id.UserID - if len(matches) > 0 { - for _, match := range matches { - mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) - } - } - //mxid, displayname := formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) - mxid = id.UserID(html.EscapeString(string(mxid))) - return fmt.Sprintf(`%s:`, bridge.Config.Homeserver.Domain, mxid, displayname) - // _, displayname = formatter.getMatrixInfoByJID(str[1:] + whatsappExt.NewUserSuffix) - //fmt.Println("ParseWhatsAp4", displayname) - //return displayname - }, } return formatter } @@ -119,29 +97,39 @@ func (formatter *Formatter) getMatrixInfoByJID(jid types.SkypeID) (mxid id.UserI } func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { - output := html.EscapeString(content.Body) + // parse a tag + reg:= regexp.MustCompile(`(?U)((.*))`) + bodyMatch := reg.FindAllStringSubmatch(content.Body, -1) + for _, match := range bodyMatch { + content.Body = strings.ReplaceAll(content.Body, match[1], match[2]) + } + + output := content.Body for regex, replacement := range formatter.waReplString { output = regex.ReplaceAllString(output, replacement) } for regex, replacer := range formatter.waReplFunc { output = regex.ReplaceAllStringFunc(output, replacer) } + content.Body = html.UnescapeString(content.Body) + + // parse @user message + r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) + matches := r.FindAllStringSubmatch(content.Body, -1) + displayname := "" + var mxid id.UserID + if len(matches) > 0 { + for _, match := range matches { + mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) + number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) + output = strings.ReplaceAll(content.Body, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) + content.Body = strings.Replace(content.Body, number, displayname, -1) + } + } + if output != content.Body { output = strings.Replace(output, "\n", "
", -1) - content.Body = html.UnescapeString(content.Body) // skype messages arrive escaped which causes element rendering issues #1 - - // parse @user message - r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) - matches := r.FindAllStringSubmatch(content.Body, -1) - displayname := "" - var mxid id.UserID - if len(matches) > 0 { - for _, match := range matches { - mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) - content.FormattedBody = strings.ReplaceAll(content.Body, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) - content.Body = content.FormattedBody - } - } + content.FormattedBody = output // parse quote message content.Body = strings.ReplaceAll(content.Body, "\n", "") diff --git a/portal.go b/portal.go index 718ad5d..3407d40 100644 --- a/portal.go +++ b/portal.go @@ -1895,7 +1895,7 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) //replyToID := content.GetReplyTo() var newContent string //if len(replyToID) > 0 { - rQuote := regexp.MustCompile(`
]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)
([^"]+)
(.*)`) + rQuote := regexp.MustCompile(`
]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)
([^"]+)
(.*)`) quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) fmt.Println("matches0: ", content.FormattedBody) fmt.Println("matches1: ", quoteMatches) @@ -1980,6 +1980,8 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) skyId := patch.ParseLocalPart(html.UnescapeString(match[2]), false) skyId = strings.ReplaceAll(skyId, "skype&", "") skyId = strings.ReplaceAll(skyId, "-", ":") + // Adapt to the message format sent by the matrix front end + content.FormattedBody = strings.ReplaceAll(content.FormattedBody, match[0] + ":", fmt.Sprintf(`%s`, skyId, match[4])) content.FormattedBody = strings.ReplaceAll(content.FormattedBody, match[0], fmt.Sprintf(`%s`, skyId, match[4])) } } diff --git a/test/main.go b/test/main.go deleted file mode 100644 index ce4ec5d..0000000 --- a/test/main.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "regexp" - "strings" - "time" -) - -func find(htm string, re *regexp.Regexp) [][]string { - imgs := re.FindAllStringSubmatch(htm, -1) - //fmt.Println(re.FindAllStringIndex(htm, -1)) - //return imgs - //out := make([]string, len(imgs)) - for _, img := range imgs { - for _, img2 := range img { - fmt.Println(img2) - } - } - return imgs -} -func main () { - a := "2020-07-15T03:47:51.217Z" - t, _ := time.Parse(time.RFC3339, a) - fmt.Println(t.Unix()) - return - //str := `1lyle21211lyle2121` - // str := `
In reply to @skype&8-live-1163765691:oliver.matrix.host
qqqqqqq
9999999` - //str := `[1594719165] Oliver1 Zhao2↵: 3333333↵↵<<< 1111111` - str := `[1594808528] Oliver1 Zhao2 -: 00000000 -<<< 1111111111` - //r,_:=regexp.Compile(".*") - //r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) - //r := regexp.MustCompile(`]+\bhref="(.*?)://matrix\.to/#/@skype&([^"]+):(.*?)">(.*?)*`) - str = strings.ReplaceAll(str, "\n", "") - r := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)".*>.*?(.*?).*?(.*)`) - //patten := `` - find(str, r) - //fmt.Println(find(str, r)) -} - //cli, _ := skype.NewConn() - //_ = cli.Login("1", "3") - //avatar := &skypeExt.ProfilePicInfo{ - // URL: "https://api.asm.skype.com/v1/objects/0-ea-d6-ee876d1872e567ed85d89efae9b05971/views/swx_avatar", - // Tag: "https://api.asm.skype.com/v1/objects/0-ea-d6-ee876d1872e567ed85d89efae9b05971/views/swx_avatar", - // Status: 0, - // Authorization: "skype_token " + cli.LoginInfo.SkypeToken, - //} - - //data, _ := avatar.DownloadBytes() - //fmt.Println("DownloadBytes: ", string(data)) - //type user struct { - // Presences map[string]*skypeExt.Presence - //} - //a := user{Presences:make(map[string]*skypeExt.Presence)} - //a.Presences["1"] = &skypeExt.Presence{ - // Id: "1", - // Availability: "313", - // Status: "31312", - //} - //fmt.Printf("%+v", a) - //body := `{"eventMessages":[{"id":1005,"type":"EventMessage","resourceType":"NewMessage","time":"2020-06-19T11:19:57Z","resourceLink":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/conversations/19:77d9cf34f8d6419fbb3542bd6304ac33@thread.skype/messages/1592565597315","resource":{"contentformat":"FN=MS%20Shell%20Dlg; EF=; CO=0; CS=0; PF=0","messagetype":"ThreadActivity/PictureUpdate","originalarrivaltime":"2020-06-19T11:19:57.315Z","ackrequired":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/conversations/ALL/messages/1592565597315/ack","type":"Message","version":"1592565597315","contenttype":"text/plain; charset=UTF-8","origincontextid":"8571783950420663070","isactive":false,"from":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/contacts/19:77d9cf34f8d6419fbb3542bd6304ac33@thread.skype","id":"1592565597315","conversationLink":"https://azwcus1-client-s.gateway.messenger.live.com/v1/users/ME/conversations/19:77d9cf34f8d6419fbb3542bd6304ac33@thread.skype","counterpartymessageid":"1592565597315","threadtopic":"gteat4","content":"15925655974408:live:.cid.d3feb90dceeb51ccURL@https://api.asm.skype.com/v1/objects/0-ea-d1-df4643685906b8826aaf6faddbbd572d/views/avatar_fullsize","composetime":"2020-06-19T11:19:57.315Z"}}]}` - //var bodyContent struct { - // EventMessages []skype.Conversation `json:"eventMessages"` - //} - //_ = json.Unmarshal([]byte(body), &bodyContent) - //if len(bodyContent.EventMessages) > 0 { - // for _, message := range bodyContent.EventMessages { - // if message.Type == "EventMessage" { - // messageType := skypeExt.ChatActionType(message.Resource.MessageType) - // switch messageType { - // case skypeExt.TopicUpdate: - // topicContent := skype.TopicContent{} - // //把xml数据解析成bs对象 - // xml.Unmarshal([]byte(message.Resource.Content), &topicContent) - // message.Resource.SendId = topicContent.Initiator + skypeExt.NewUserSuffix - // //go portal.UpdateName(cmd.ThreadTopic, cmd.SendId) - // case skypeExt.PictureUpdate: - // topicContent := skype.PictureContent{} - // //把xml数据解析成bs对象 - // xml.Unmarshal([]byte(message.Resource.Content), &topicContent) - // message.Resource.SendId = topicContent.Initiator + skypeExt.NewUserSuffix - // url := strings.TrimPrefix(topicContent.Value, "URL@") - // fmt.Println(url) - // //avatar := &skypeExt.ProfilePicInfo{ - // // URL: url, - // // Tag: topicContent.Value, - // // Status: 0, - // //} - // //go portal.UpdateAvatar(user, avatar) - // } - // } - // } - //} - - //fmt.Println("hello https://tool.lu/") - //membersStr := "[\"8:live:1163765691\",\"8:live:zhaosl_4\"]" - //var members []string - //_ = json.Unmarshal([]byte(membersStr), &members) - //fmt.Println(members) - //for _, participant := range members { - // fmt.Println(participant) - //} - //type a string - //type b struct { - // C a - //} - //v := b{ - // C: a("8:live:1163765691"), - //} - //fmt.Println(strings.HasSuffix("28:0d5d6cff-595d-49d7-9cf8-973173f5233b@s.skype.net", skypeExt.NewUserSuffix)) - //return - //rand.Seed(time.Now().UnixNano()) - //fmt.Sprintf("%04v", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000)) - //currentTimeNanoStr := strconv.FormatInt(time.Now().UnixNano(), 10) - //currentTimeNanoStr = currentTimeNanoStr[:len(currentTimeNanoStr)-3] - ////clientmessageid := currentTimeNanoStr + randomStr - //fmt.Println(fmt.Sprintf("%04v", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000))) - //return - ////Parse("@whatsapp_8:live:1163765691:oliver.matrix.host") - //userIDRegex, _ := regexp.Compile("@whatsapp_8:live:1163765691:oliver.matrix.host") - //match := userIDRegex.FindStringSubmatch(string("@whatsapp_8:live:1163765691:oliver.matrix.host")) - //fmt.Println(match) -//} - -func Parse(userID string)(localpart, homeserver string, err error) { - if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') { - err = fmt.Errorf("%s is not a valid user id", userID) - return - } - parts := strings.Split(string(userID), ":") - localpart, homeserver = strings.TrimPrefix(strings.Join(parts[:len(parts)-1], ":"), "@"), parts[len(parts)-1] - localpart = base64.StdEncoding.EncodeToString([]byte(localpart)) - fmt.Println(localpart, homeserver) - return -} From 4d176a532fa58c570f2671bdae284e0d97578fcf Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 15 Jan 2021 11:19:13 +0800 Subject: [PATCH 067/109] the matrix event should be ignored when not logged in skype --- matrix.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matrix.go b/matrix.go index 36e1c37..2d61c8b 100644 --- a/matrix.go +++ b/matrix.go @@ -374,6 +374,9 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { fmt.Println() fmt.Printf("shouldIgnoreEvent: user%+v", *user) fmt.Println() + if user.Conn == nil { + return true + } if !user.RelaybotWhitelisted { fmt.Println("user.RelaybotWhitelisted true", user.RelaybotWhitelisted) return true From ac8847818932ba2e523ab53b80a58bba26d5394a Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 15 Jan 2021 11:41:50 +0800 Subject: [PATCH 068/109] fixed the issue caused by the commit last time --- matrix.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/matrix.go b/matrix.go index 2d61c8b..a7ed35d 100644 --- a/matrix.go +++ b/matrix.go @@ -257,7 +257,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { } user := mx.bridge.GetUserByMXID(evt.Sender) - if user == nil || !user.Whitelisted || !user.IsConnected() { + if user == nil || user.Conn == nil || !user.Whitelisted || !user.IsConnected() { return } @@ -307,7 +307,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { } portal := mx.bridge.GetPortalByMXID(evt.RoomID) - if portal == nil || portal.IsPrivateChat() { + if user.Conn == nil || portal == nil || portal.IsPrivateChat() { return } @@ -374,9 +374,6 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { fmt.Println() fmt.Printf("shouldIgnoreEvent: user%+v", *user) fmt.Println() - if user.Conn == nil { - return true - } if !user.RelaybotWhitelisted { fmt.Println("user.RelaybotWhitelisted true", user.RelaybotWhitelisted) return true @@ -424,7 +421,7 @@ func (mx *MatrixHandler) HandleMessage(evt *event.Event) { fmt.Println() fmt.Printf("HandleMessage portal: %+v", portal) fmt.Println() - if portal != nil && (user.Whitelisted || portal.HasRelaybot()) { + if user.Conn != nil && portal != nil && (user.Whitelisted || portal.HasRelaybot()) { portal.HandleMatrixMessage(user, evt) } } @@ -452,7 +449,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) { } portal := mx.bridge.GetPortalByMXID(evt.RoomID) - if portal != nil { + if user.Conn != nil && portal != nil { portal.HandleMatrixRedaction(user, evt) } } From 944878eb48991e36f734abe880b298cc89ba8686 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 15 Jan 2021 15:07:12 +0800 Subject: [PATCH 069/109] optimize reply message --- database/message.go | 5 +++++ formatting.go | 37 ++++++++++++++++++++++--------------- formatting_test.go | 2 +- portal.go | 24 +++++++++++++++++------- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/database/message.go b/database/message.go index fff6f86..fc7ebdc 100644 --- a/database/message.go +++ b/database/message.go @@ -51,6 +51,11 @@ func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message { "FROM message WHERE mxid=$1", mxid) } +func (mq *MessageQuery) GetByID(id string) *Message { + return mq.get("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + + "FROM message WHERE id=$1", id) +} + func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message { msg := mq.get("SELECT id, chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + "FROM message WHERE chat_jid=$1 AND chat_receiver=$2 ORDER BY timestamp DESC LIMIT 1", chat.JID, chat.Receiver) diff --git a/formatting.go b/formatting.go index c30c3f0..94d55aa 100644 --- a/formatting.go +++ b/formatting.go @@ -96,8 +96,8 @@ func (formatter *Formatter) getMatrixInfoByJID(jid types.SkypeID) (mxid id.UserI return } -func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { - // parse a tag +func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomMXID id.RoomID) { + // parse '' tag reg:= regexp.MustCompile(`(?U)((.*))`) bodyMatch := reg.FindAllStringSubmatch(content.Body, -1) for _, match := range bodyMatch { @@ -113,7 +113,7 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { } content.Body = html.UnescapeString(content.Body) - // parse @user message + // parse mention user message r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) matches := r.FindAllStringSubmatch(content.Body, -1) displayname := "" @@ -130,30 +130,37 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent) { if output != content.Body { output = strings.Replace(output, "\n", "
", -1) content.FormattedBody = output + content.Format = event.FormatHTML - // parse quote message + // parse quote message(set reply) content.Body = strings.ReplaceAll(content.Body, "\n", "") - quoteReg := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)".*>.*?(.*?).*?(.*)`) + quoteReg := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)" conversation.* messageid="([^"]+)".*>.*?(.*?).*?(.*)`) quoteMatches := quoteReg.FindAllStringSubmatch(content.Body, -1) if len(quoteMatches) > 0 { for _, match := range quoteMatches { + msgMXID := "" + msg := formatter.bridge.DB.Message.GetByID(match[4]) + if msg != nil { + msgMXID = string(msg.MXID) + } mxid, displayname = formatter.getMatrixInfoByJID("8:" + match[1] + skypeExt.NewUserSuffix) - //href1 := fmt.Sprintf(`https://matrix.to/#/!kpouCkfhzvXgbIJmkP:oliver.matrix.host/$fHQNRydqqqAVS8usHRmXn0nIBM_FC-lo2wI2Uol7wu8?via=oliver.matrix.host`) - href1 := "" - //mxid `@skype&8-live-xxxxxx:name.matrix.server` - href2 := fmt.Sprintf(`https://%s/#/%s`, formatter.bridge.Config.Homeserver.Domain, mxid) - newContent := fmt.Sprintf(`
%s
%s
%s`, + href1 := fmt.Sprintf(`https://%s/#/room/%s/%s?via=%s`, formatter.bridge.Config.Homeserver.Domain, RoomMXID, msgMXID, formatter.bridge.Config.Homeserver.Domain) + href2 := fmt.Sprintf(`https://%s/#/user/%s`, formatter.bridge.Config.Homeserver.Domain, mxid) + newContent := fmt.Sprintf(`
In reply to %s
%s
%s`, href1, href2, mxid, - match[4], - match[5]) + match[5], + match[6]) content.FormattedBody = newContent - content.Body = match[4] + "\n" + match[5] + content.Body = fmt.Sprintf("> <%s> %s\n\n%s", mxid, match[5], match[6]) + inRelateTo := &event.RelatesTo{ + Type: event.RelReply, + EventID: id.EventID(msgMXID), + } + content.SetRelatesTo(inRelateTo) } } - - content.Format = event.FormatHTML } } diff --git a/formatting_test.go b/formatting_test.go index 6070483..472fa2c 100644 --- a/formatting_test.go +++ b/formatting_test.go @@ -87,7 +87,7 @@ func TestFormatter_ParseSkype(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - testFormatter.ParseSkype(tt.args.content) + testFormatter.ParseSkype(tt.args.content, "") if !reflect.DeepEqual(tt.args.content, tt.expect.content) { t.Errorf("content = %v, wanted %v", tt.args.content, tt.expect.content) } diff --git a/portal.go b/portal.go index 3407d40..901f003 100644 --- a/portal.go +++ b/portal.go @@ -298,8 +298,8 @@ func (portal *Portal) markHandledSkype(source *User, message *skype.Resource, mx msg.ID = message.Id } msg.Insert() -fmt.Println("markHandledSkype1", msg.Chat.JID) -fmt.Println("markHandledSkype2", msg.JID) + fmt.Println("markHandledSkype1", msg.Chat.JID) + fmt.Println("markHandledSkype2", msg.JID) portal.recentlyHandledLock.Lock() index := portal.recentlyHandledIndex portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength @@ -1317,6 +1317,16 @@ func (portal *Portal) SetReplySkype(content *event.MessageEventContent, info sky portal.log.Warnln("Failed to get reply target:", err) return } + if evt.Type == event.EventEncrypted { + _ = evt.Content.ParseRaw(evt.Type) + decryptedEvt, err := portal.bridge.Crypto.Decrypt(evt) + if err != nil { + portal.log.Warnln("Failed to decrypt reply target:", err) + } else { + evt = decryptedEvt + } + } + _ = evt.Content.ParseRaw(evt.Type) content.SetReply(evt) } return @@ -1450,8 +1460,8 @@ func (portal *Portal) HandleTextMessage(source *User, message skype.Resource) { MsgType: event.MsgText, } - portal.bridge.Formatter.ParseSkype(content) - portal.SetReplySkype(content, message) + portal.bridge.Formatter.ParseSkype(content, portal.MXID) + // portal.SetReplySkype(content, message) fmt.Println() fmt.Printf("portal HandleTextMessage2: %+v", content) @@ -1492,7 +1502,7 @@ func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Res GeoURI: geo, } - portal.SetReplySkype(content, message) + // portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) @@ -1522,7 +1532,7 @@ func (portal *Portal) HandleContactMessageSkype(source *User, message skype.Reso MsgType: event.MsgText, } - portal.SetReplySkype(content, message) + // portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) @@ -1659,7 +1669,7 @@ func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn * } else { content.URL = uploaded.ContentURI.CUString() } - portal.SetReplySkype(content, info) + // portal.SetReplySkype(content, info) fmt.Println() fmt.Println("mediaMessage.UrlThumbnail", mediaMessage.UrlThumbnail) From 71b23199899f14d5fbdfc00200ef3d52468d200b Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 15 Jan 2021 18:57:11 +0800 Subject: [PATCH 070/109] optimize reply message --- database/message.go | 7 ++-- formatting.go | 48 +++++++++++++--------- portal.go | 99 +++++++++++++++++++++------------------------ 3 files changed, 77 insertions(+), 77 deletions(-) diff --git a/database/message.go b/database/message.go index fc7ebdc..528629b 100644 --- a/database/message.go +++ b/database/message.go @@ -4,8 +4,6 @@ import ( "bytes" "database/sql" "encoding/json" - skype "github.com/kelaresg/go-skypeapi" - log "maunium.net/go/maulogger/v2" "github.com/kelaresg/matrix-skype/types" @@ -84,7 +82,7 @@ type Message struct { MXID id.EventID Sender types.SkypeID Timestamp uint64 - Content *skype.Resource + Content string } func (msg *Message) Scan(row Scannable) *Message { @@ -103,7 +101,8 @@ func (msg *Message) Scan(row Scannable) *Message { } func (msg *Message) decodeBinaryContent(content []byte) { - msg.Content = &skype.Resource{} + //msg.Content = &skype.Resource{} + msg.Content = "" reader := bytes.NewReader(content) dec := json.NewDecoder(reader) err := dec.Decode(&msg.Content) diff --git a/formatting.go b/formatting.go index 94d55aa..5572247 100644 --- a/formatting.go +++ b/formatting.go @@ -113,55 +113,65 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM } content.Body = html.UnescapeString(content.Body) - // parse mention user message - r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) - matches := r.FindAllStringSubmatch(content.Body, -1) - displayname := "" - var mxid id.UserID - if len(matches) > 0 { - for _, match := range matches { - mxid, displayname = formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) - number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) - output = strings.ReplaceAll(content.Body, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) - content.Body = strings.Replace(content.Body, number, displayname, -1) - } - } - + var backStr string if output != content.Body { output = strings.Replace(output, "\n", "
", -1) content.FormattedBody = output content.Format = event.FormatHTML + var mxid id.UserID // parse quote message(set reply) content.Body = strings.ReplaceAll(content.Body, "\n", "") quoteReg := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)" conversation.* messageid="([^"]+)".*>.*?(.*?).*?(.*)`) quoteMatches := quoteReg.FindAllStringSubmatch(content.Body, -1) + if len(quoteMatches) > 0 { for _, match := range quoteMatches { + for index, a := range match { + fmt.Println("index: ", index) + fmt.Println("ParseSkype quoteMatches a:", a) + fmt.Println() + } msgMXID := "" msg := formatter.bridge.DB.Message.GetByID(match[4]) if msg != nil { msgMXID = string(msg.MXID) } - mxid, displayname = formatter.getMatrixInfoByJID("8:" + match[1] + skypeExt.NewUserSuffix) + mxid, _ = formatter.getMatrixInfoByJID("8:" + match[1] + skypeExt.NewUserSuffix) href1 := fmt.Sprintf(`https://%s/#/room/%s/%s?via=%s`, formatter.bridge.Config.Homeserver.Domain, RoomMXID, msgMXID, formatter.bridge.Config.Homeserver.Domain) href2 := fmt.Sprintf(`https://%s/#/user/%s`, formatter.bridge.Config.Homeserver.Domain, mxid) - newContent := fmt.Sprintf(`
In reply to %s
%s
%s`, + newContent := fmt.Sprintf(`
In reply to %s
%s
`, href1, href2, mxid, - match[5], - match[6]) + match[5]) content.FormattedBody = newContent - content.Body = fmt.Sprintf("> <%s> %s\n\n%s", mxid, match[5], match[6]) + content.Body = fmt.Sprintf("> <%s> %s\n\n", mxid, match[5]) inRelateTo := &event.RelatesTo{ Type: event.RelReply, EventID: id.EventID(msgMXID), } content.SetRelatesTo(inRelateTo) + backStr = match[6] } } } + + // parse mention user message + r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) + matches := r.FindAllStringSubmatch(backStr, -1) + if len(matches) > 0 { + for _, match := range matches { + mxid, displayname := formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) + number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) + output = strings.ReplaceAll(backStr, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) + content.FormattedBody = content.FormattedBody + output + content.Body = content.Body + strings.Replace(backStr, number, displayname, -1) + } + } else { + content.Body = content.Body + backStr + content.FormattedBody = content.FormattedBody + backStr + } } func (formatter *Formatter) ParseMatrix(html string) string { diff --git a/portal.go b/portal.go index 901f003..4b89ffd 100644 --- a/portal.go +++ b/portal.go @@ -5,7 +5,6 @@ import ( "maunium.net/go/mautrix/patch" "encoding/hex" - "encoding/json" "encoding/xml" "fmt" skype "github.com/kelaresg/go-skypeapi" @@ -293,7 +292,8 @@ func (portal *Portal) markHandledSkype(source *User, message *skype.Resource, mx // msg.Sender = message.Jid //} } - msg.Content = message + + msg.Content = message.Content if len(message.Id)>0 { msg.ID = message.Id } @@ -1890,9 +1890,6 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) return nil, sender, content } - //ts := uint64(evt.Timestamp / 1000) - //status := waProto.WebMessageInfo_ERROR - //fromMe := true currentTimeNanoStr := strconv.FormatInt(time.Now().UnixNano(), 10) currentTimeNanoStr = currentTimeNanoStr[:len(currentTimeNanoStr)-3] clientMessageId := currentTimeNanoStr + fmt.Sprintf("%04v", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(10000)) @@ -1902,53 +1899,51 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) Timestamp: time.Now().Unix(), } //ctxInfo := &waProto.ContextInfo{} - //replyToID := content.GetReplyTo() + replyToID := content.GetReplyTo() var newContent string - //if len(replyToID) > 0 { - rQuote := regexp.MustCompile(`
]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)
([^"]+)
(.*)`) - quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) - fmt.Println("matches0: ", content.FormattedBody) - fmt.Println("matches1: ", quoteMatches) - if len(quoteMatches) > 0 { - for _, match := range quoteMatches { - if len(match) > 2 { - var skyId string - if strings.Index(match[4], "@skype") > -1 { - skyId = patch.ParseLocalPart(html.UnescapeString(match[2]), false) - skyId = strings.ReplaceAll(skyId, "skype&", "") - skyId = strings.ReplaceAll(skyId, "-", ":") - } else { - skyId = strings.ReplaceAll(sender.JID, skypeExt.NewUserSuffix, "") - } - if len(skyId) < 2 { - continue - } - skypeUsername := strings.Replace(skyId, "8:", "", 1) - puppet := sender.bridge.GetPuppetByJID(skyId + skypeExt.NewUserSuffix) - time.Now().Unix() - newContent = fmt.Sprintf(`[%s] %s↵: %s↵↵<<< %s`, - skypeUsername, - puppet.Displayname, - strconv.Itoa(int(time.Now().Unix())), - portal.Key.JID, - time.Now().UnixNano() / 1e6, - strconv.Itoa(int(time.Now().UnixNano())) + "1", - time.Now().Unix(), - puppet.Displayname, - match[5], - match[6]) - content.FormattedBody = newContent - } + if len(replyToID) > 0 { + rQuote := regexp.MustCompile(`(.*?)(.*)`) + quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) + backStr := "" + if len(quoteMatches) > 0 { + if len(quoteMatches[0]) > 2 { + backStr = quoteMatches[0][2] } } + content.RemoveReplyFallback() - //if len(newContent) > 0 { - // newContent = content.Body - //} - //content.FormattedBody = newContent - //msg := portal.bridge.DB.Message.GetByMXID(replyToID) - //} + msg := portal.bridge.DB.Message.GetByMXID(replyToID) + if msg != nil && len(msg.Content) > 0 { + messageId := msg.ID + if len(messageId) < 1 { + messageId = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + } + author := strings.Replace(msg.Sender, skypeExt.NewUserSuffix, "", 1) + author = strings.Replace(author, "8:", "", 1) + conversation := msg.Chat.Receiver + cuid := msg.JID + r := []rune(messageId) + timestamp := string(r[:len(r) - 3]) + quoteMessage := msg.Content + + puppet := sender.bridge.GetPuppetByJID(msg.Sender) + + newContent = fmt.Sprintf(`[%s] %s: %s\n\n<<< %s`, + author, + puppet.Displayname, + timestamp, + conversation, + messageId, + cuid, + timestamp, + puppet.Displayname, + quoteMessage, + backStr) + content.FormattedBody = newContent + } + } + relaybotFormatted := false if sender.NeedsRelaybot(portal) { if !portal.HasRelaybot() { @@ -2115,24 +2110,20 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { fmt.Println("portal HandleMatrixMessage info is nil: ") return } - infoRaw, err := json.Marshal(info) - if err != nil { - fmt.Println("portal HandleMatrixMessage Marshal info err: ", err) - return - } + fmt.Println("portal HandleMatrixMessage start markHandledSkype: ") portal.markHandledSkype(sender, &skype.Resource{ ClientMessageId: info.ClientMessageId, Jid: portal.Key.JID,//receiver id(conversation id) Timestamp: time.Now().Unix(), - Content: string(infoRaw), + Content: info.Content, }, evt.ID) portal.log.Debugln("Sending event", evt.ID, "to Skype") errChan := make(chan error, 1) //go sender.Conn.Conn.SendMsg(portal.Key.JID, info.Content, info.ClientMessageId, errChan) go SendMsg(sender, portal.Key.JID, info, errChan) - + var err error var errorEventID id.EventID select { case err = <-errChan: From 977b8996c779bafc876f91e33b838c7000d657c1 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 15 Jan 2021 19:55:49 +0800 Subject: [PATCH 071/109] fix send media message bug --- portal.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/portal.go b/portal.go index 4b89ffd..7cfa128 100644 --- a/portal.go +++ b/portal.go @@ -2111,12 +2111,19 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { return } + var content string + if info.Type != string(event.MsgText) { + content = info.SendMediaMessage.FileName // URIObject + } else { + content = info.SendTextMessage.Content + } + fmt.Println("portal HandleMatrixMessage start markHandledSkype: ") portal.markHandledSkype(sender, &skype.Resource{ ClientMessageId: info.ClientMessageId, Jid: portal.Key.JID,//receiver id(conversation id) Timestamp: time.Now().Unix(), - Content: info.Content, + Content: content, }, evt.ID) portal.log.Debugln("Sending event", evt.ID, "to Skype") From 43b937d645ac948471a8e86d1a388d3ca13fa268 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 15 Jan 2021 20:29:17 +0800 Subject: [PATCH 072/109] fix mention user bug --- formatting.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/formatting.go b/formatting.go index 5572247..641ecfa 100644 --- a/formatting.go +++ b/formatting.go @@ -159,14 +159,26 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM // parse mention user message r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) - matches := r.FindAllStringSubmatch(backStr, -1) + var originStr string + if len(backStr) == 0 { + originStr = content.Body + } else { + originStr = backStr + } + matches := r.FindAllStringSubmatch(originStr, -1) if len(matches) > 0 { for _, match := range matches { mxid, displayname := formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) - output = strings.ReplaceAll(backStr, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) - content.FormattedBody = content.FormattedBody + output - content.Body = content.Body + strings.Replace(backStr, number, displayname, -1) + output = strings.ReplaceAll(originStr, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) + if len(backStr) == 0 { + content.Format = event.FormatHTML + content.Body = strings.Replace(originStr, number, displayname, -1) + content.FormattedBody = output + } else { + content.FormattedBody = content.FormattedBody + output + content.Body = content.Body + strings.Replace(backStr, number, displayname, -1) + } } } else { content.Body = content.Body + backStr From 9ca4638475624f033e1c8739ed7b343c14675732 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 18 Jan 2021 15:08:10 +0800 Subject: [PATCH 073/109] muliti mention --- portal.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/portal.go b/portal.go index 7cfa128..a876ba2 100644 --- a/portal.go +++ b/portal.go @@ -1901,11 +1901,10 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) //ctxInfo := &waProto.ContextInfo{} replyToID := content.GetReplyTo() var newContent string - + backStr := "" if len(replyToID) > 0 { rQuote := regexp.MustCompile(`(.*?)(.*)`) quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) - backStr := "" if len(quoteMatches) > 0 { if len(quoteMatches[0]) > 2 { backStr = quoteMatches[0][2] @@ -1929,7 +1928,7 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) puppet := sender.bridge.GetPuppetByJID(msg.Sender) - newContent = fmt.Sprintf(`[%s] %s: %s\n\n<<< %s`, + newContent = fmt.Sprintf(`[%s] %s: %s\n\n<<< `, author, puppet.Displayname, timestamp, @@ -1938,8 +1937,7 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) cuid, timestamp, puppet.Displayname, - quoteMessage, - backStr) + quoteMessage) content.FormattedBody = newContent } } @@ -1975,9 +1973,12 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) text = "/me " + text } if len(content.FormattedBody) > 0 { - //r := regexp.MustCompile(`]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@skype&([^"]+):(.*?)">(.*?)*`) - r := regexp.MustCompile(`]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)*`) - matches := r.FindAllStringSubmatch(content.FormattedBody, -1) + matchStr := content.FormattedBody + if len(backStr) > 0 { + matchStr = backStr + } + r := regexp.MustCompile(`(?m)]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)`) + matches := r.FindAllStringSubmatch(matchStr, -1) fmt.Println("matches: ", matches) if len(matches) > 0 { for _, match := range matches { @@ -1986,10 +1987,19 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) skyId = strings.ReplaceAll(skyId, "skype&", "") skyId = strings.ReplaceAll(skyId, "-", ":") // Adapt to the message format sent by the matrix front end - content.FormattedBody = strings.ReplaceAll(content.FormattedBody, match[0] + ":", fmt.Sprintf(`%s`, skyId, match[4])) - content.FormattedBody = strings.ReplaceAll(content.FormattedBody, match[0], fmt.Sprintf(`%s`, skyId, match[4])) + matchStr = strings.ReplaceAll(matchStr, match[0] + ":", fmt.Sprintf(`%s`, skyId, match[4])) + matchStr = strings.ReplaceAll(matchStr, match[0], fmt.Sprintf(`%s`, skyId, match[4])) } } + if len(backStr) > 0 { + content.FormattedBody = content.FormattedBody + matchStr + } else { + content.FormattedBody = matchStr + } + } else { + if len(backStr) > 0 { + content.FormattedBody = content.FormattedBody + backStr + } } info.SendTextMessage = &skype.SendTextMessage{ From 2e904aa7532cab5943ef1c952b7ca25973b1fa53 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 18 Jan 2021 15:35:43 +0800 Subject: [PATCH 074/109] optimize mention users(skype->matrix) --- formatting.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/formatting.go b/formatting.go index 641ecfa..5d3c0fa 100644 --- a/formatting.go +++ b/formatting.go @@ -158,8 +158,9 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM } // parse mention user message - r := regexp.MustCompile(`]+\bid="([^"]+)"(.*?)*`) + r := regexp.MustCompile(`(?m)]+\bid="([^"]+)"(.*?)`) var originStr string + var originBodyStr string if len(backStr) == 0 { originStr = content.Body } else { @@ -170,15 +171,16 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM for _, match := range matches { mxid, displayname := formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) - output = strings.ReplaceAll(originStr, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) - if len(backStr) == 0 { - content.Format = event.FormatHTML - content.Body = strings.Replace(originStr, number, displayname, -1) - content.FormattedBody = output - } else { - content.FormattedBody = content.FormattedBody + output - content.Body = content.Body + strings.Replace(backStr, number, displayname, -1) - } + originStr = strings.ReplaceAll(originStr, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) + originBodyStr = strings.Replace(originStr, number, displayname, -1) + } + if len(backStr) == 0 { + content.Format = event.FormatHTML + content.Body = originBodyStr + content.FormattedBody = originStr + } else { + content.Body = content.Body + originBodyStr + content.FormattedBody = content.FormattedBody + originStr } } else { content.Body = content.Body + backStr From 949fbc7ba87d2500655bd675240bc7b6e6b8c944 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 19 Jan 2021 18:22:19 +0800 Subject: [PATCH 075/109] optimize the process that logout with double puppeting enabled --- commands.go | 18 +++++++++++++++++- user.go | 24 +++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/commands.go b/commands.go index aaf075d..f3ff562 100644 --- a/commands.go +++ b/commands.go @@ -251,6 +251,7 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { if ce.User.Conn == nil { return } + _ = ce.User.Conn.GetConversations("", ce.User.bridge.Config.Bridge.InitialChatSync) ce.User.Conn.LoggedIn = false username := "" password := "" @@ -269,7 +270,22 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { Username: username, Password: password, } + portals := ce.User.GetPortals() + newPortals := ce.User.GetPortalsNew() + allPortals := newPortals[0:] + for _, portal := range portals { + var newPortalsHas bool + for _, newPortal := range newPortals { + if portal.Key == newPortal.Key { + newPortalsHas = true + } + } + if !newPortalsHas { + allPortals = append(allPortals, portal) + } + } + leave := func(portal *Portal) { if len(portal.MXID) > 0 { _, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ @@ -278,7 +294,7 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { }) } } - for _, portal := range portals { + for _, portal := range allPortals { leave(portal) } ce.Reply("Logged out successfully.") diff --git a/user.go b/user.go index 25949e9..21f92c5 100644 --- a/user.go +++ b/user.go @@ -156,6 +156,28 @@ func (user *User) GetPortals() []*Portal { return portals } +func (user *User) GetPortalsNew() []*Portal { + keys := make([]database.PortalKey, len(user.Conn.Store.Chats)) + i := 0 + for jid, _ := range user.Conn.Store.Chats { + keys[i] = database.NewPortalKey(jid, user.JID) + i++ + } + + portals := make([]*Portal, len(keys)) + + user.bridge.portalsLock.Lock() + for i, key := range keys { + portal, ok := user.bridge.portalsByJID[key] + if !ok { + portal = user.bridge.loadDBPortal(user.bridge.DB.Portal.GetByJID(key), &key) + } + portals[i] = portal + } + user.bridge.portalsLock.Unlock() + return portals +} + func (bridge *Bridge) NewUser(dbUser *database.User) *User { user := &User{ User: dbUser, @@ -394,9 +416,9 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err go user.Conn.Poll() user.ConnectionErrors = 0 - user.SetSession(user.Conn.LoginInfo) user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix user.addToJIDMap() + user.SetSession(user.Conn.LoginInfo) _ = ce.User.Conn.GetConversations("", user.bridge.Config.Bridge.InitialChatSync) user.PostLogin() return From e1c5d2db21e9c72e5f78d2876e694c064f96b140 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 20 Jan 2021 18:07:04 +0800 Subject: [PATCH 076/109] fix edit message --- formatting.go | 10 ++++++ go.mod | 2 +- go.sum | 6 ++++ portal.go | 84 +++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/formatting.go b/formatting.go index 5d3c0fa..1ef6bd0 100644 --- a/formatting.go +++ b/formatting.go @@ -186,6 +186,16 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM content.Body = content.Body + backStr content.FormattedBody = content.FormattedBody + backStr } + + //filter edit tag + e := regexp.MustCompile(`( 0 { + message.ClientMessageId = message.SkypeEditedId + message.Id + msg := source.bridge.DB.Message.GetByJID(portal.Key, message.SkypeEditedId) + inRelateTo := &event.RelatesTo{ + Type: event.RelReplace, + EventID: msg.MXID, + } + content.SetRelatesTo(inRelateTo) + content.NewContent = &event.MessageEventContent{ + MsgType: content.MsgType, + Body: content.Body, + FormattedBody: content.FormattedBody, + Format: content.Format, + } + } // portal.SetReplySkype(content, message) fmt.Println() @@ -1898,8 +1912,52 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) Jid: portal.Key.JID,//receiver id(conversation id) Timestamp: time.Now().Unix(), } - //ctxInfo := &waProto.ContextInfo{} + replyToID := content.GetReplyTo() + + // reedit message + if content.NewContent != nil { + a := strings.Replace(sender.JID, skypeExt.NewUserSuffix, "", 1) + a = strings.Replace(a, "8:", "", 1) + tsMs := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + r := []rune(tsMs) + ts := string(r[:len(r) - 3]) + msg := portal.bridge.DB.Message.GetByMXID(content.RelatesTo.EventID) + if msg != nil && len(msg.JID) > 0 { + info.SkypeEditedId = msg.JID + //info.ClientMessageId = info.ClientMessageId + info.SkypeEditedId + content.Body = content.Body + fmt.Sprintf("", a, tsMs, ts) + content.Body = strings.TrimPrefix(content.Body, " * ") + if len(content.FormattedBody) > 0 { + content.FormattedBody = content.FormattedBody + fmt.Sprintf("", a, tsMs, ts) + content.FormattedBody = strings.TrimPrefix(content.FormattedBody, " * ") + } + } + + // in reedit message we can't obtain the "relayId" from RelatesTo.EventID cause the matrix message doesn't put it in "RelatesTo". + // so i get relayId with use regexp, but it's not a good way, + // now there is no way to get relayId if reedit message with add a mention user + // TODO maybe we can record the relayId to DB + rQuote := regexp.MustCompile(`
In reply to.*
(.*)`) + quoteMatches := rQuote.FindAllStringSubmatch(content.FormattedBody, -1) + if len(replyToID) < 1 { + if len(quoteMatches) > 0 { + if len(quoteMatches[0]) > 0 { + replyToID = id.EventID(quoteMatches[0][1]) + } + + //Filter out the " * " in the matrix editing message (i don't why the matrix need a * in the edit message body) + if len(quoteMatches[0]) > 1 { + needReplace := quoteMatches[0][2] + afterReplace := strings.TrimPrefix(needReplace, " * ") + content.Body = strings.Replace(content.Body, needReplace, afterReplace, 1) + content.FormattedBody = strings.Replace(content.FormattedBody, needReplace, afterReplace, 1) + } + } + } + } + + // reply message var newContent string backStr := "" if len(replyToID) > 0 { @@ -1959,8 +2017,10 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) if evt.Type == event.EventSticker { content.MsgType = event.MsgImage } + fmt.Println("convertMatrixMessage content.MsgType: ", content.MsgType) fmt.Println("convertMatrixMessage content.Body: ", content.Body) + fmt.Println("convertMatrixMessage content.NewBody: ", content.NewContent) fmt.Println("convertMatrixMessage content.FormattedBody: ", content.FormattedBody) info.Type = string(content.MsgType) switch content.MsgType { @@ -1977,6 +2037,8 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) if len(backStr) > 0 { matchStr = backStr } + + // mention user message r := regexp.MustCompile(`(?m)]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)`) matches := r.FindAllStringSubmatch(matchStr, -1) fmt.Println("matches: ", matches) @@ -2001,7 +2063,9 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) content.FormattedBody = content.FormattedBody + backStr } } + } + if len(content.FormattedBody) > 0 { info.SendTextMessage = &skype.SendTextMessage{ Content : content.FormattedBody, } @@ -2010,18 +2074,6 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) Content : content.Body, } } - //ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1) - //for index, mention := range ctxInfo.MentionedJid { - // ctxInfo.MentionedJid[index] = mention[1:] + whatsappExt.NewUserSuffix - //} - //if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil { - // info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{ - // Text: &text, - // ContextInfo: ctxInfo, - // } - //} else { - // info.Message.Conversation = &text - //} case event.MsgImage: caption, fileSize , data := portal.preprocessMatrixMediaSkype(relaybotFormatted, content, evt.ID) //if media == nil { From 96c71900a70ca673ac40f426c72d3bd58d8d3798 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 20 Jan 2021 19:14:30 +0800 Subject: [PATCH 077/109] fix nil bug --- portal.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/portal.go b/portal.go index 4b580af..0fbc9e6 100644 --- a/portal.go +++ b/portal.go @@ -1463,16 +1463,18 @@ func (portal *Portal) HandleTextMessage(source *User, message skype.Resource) { if len(message.SkypeEditedId) > 0 { message.ClientMessageId = message.SkypeEditedId + message.Id msg := source.bridge.DB.Message.GetByJID(portal.Key, message.SkypeEditedId) - inRelateTo := &event.RelatesTo{ - Type: event.RelReplace, - EventID: msg.MXID, - } - content.SetRelatesTo(inRelateTo) - content.NewContent = &event.MessageEventContent{ - MsgType: content.MsgType, - Body: content.Body, - FormattedBody: content.FormattedBody, - Format: content.Format, + if msg != nil && len(msg.MXID) > 0 { + inRelateTo := &event.RelatesTo{ + Type: event.RelReplace, + EventID: msg.MXID, + } + content.SetRelatesTo(inRelateTo) + content.NewContent = &event.MessageEventContent{ + MsgType: content.MsgType, + Body: content.Body, + FormattedBody: content.FormattedBody, + Format: content.Format, + } } } // portal.SetReplySkype(content, message) From 6b37ca0e8d06b3b3457f2d5d2e8835c2b155172d Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 20 Jan 2021 19:31:25 +0800 Subject: [PATCH 078/109] fix the bug that in the 1-1 room show that message delete by someone, but actually is other one do --- portal.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/portal.go b/portal.go index 0fbc9e6..ab46068 100644 --- a/portal.go +++ b/portal.go @@ -1338,7 +1338,8 @@ func (portal *Portal) HandleMessageRevokeSkype(user *User, message skype.Resourc if message.GetFromMe(user.Conn.Conn) { if portal.IsPrivateChat() { intent = portal.bridge.GetPuppetByJID(user.JID).CustomIntent() - } else { + } + if intent == nil { intent = portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) } } From f73a4751322fbc7b4ea1c2fdd59b8a73cdfe42f7 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 21 Jan 2021 10:37:01 +0800 Subject: [PATCH 079/109] delete unused pkg --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index e35541b..db6133e 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e golang.org/x/image v0.0.0-20200618115811-c13761719519 gopkg.in/yaml.v2 v2.3.0 From 5211e5a6152abd85eee1fe602ad27fb459f5e700 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 25 Jan 2021 20:19:34 +0800 Subject: [PATCH 080/109] add "server_name" to config.yaml --- config/config.go | 1 + example-config.yaml | 2 ++ formatting.go | 11 ++++++----- portal.go | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/config/config.go b/config/config.go index fb19b7d..b999fd7 100644 --- a/config/config.go +++ b/config/config.go @@ -29,6 +29,7 @@ type Config struct { Homeserver struct { Address string `yaml:"address"` Domain string `yaml:"domain"` + ServerName string `yaml:"server_name"` } `yaml:"homeserver"` AppService struct { diff --git a/example-config.yaml b/example-config.yaml index d0c4d59..4069c05 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -4,6 +4,8 @@ homeserver: address: https://example.com # The domain of the homeserver (for MXIDs, etc). domain: example.com + # If you don’t know what this is, no need to modify(for parse "mention user/reply message, etc") + server_name: matrix.to # Application service host/registration related details. # Changing these values requires regeneration of the registration. diff --git a/formatting.go b/formatting.go index 1ef6bd0..8940f3e 100644 --- a/formatting.go +++ b/formatting.go @@ -138,8 +138,8 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM msgMXID = string(msg.MXID) } mxid, _ = formatter.getMatrixInfoByJID("8:" + match[1] + skypeExt.NewUserSuffix) - href1 := fmt.Sprintf(`https://%s/#/room/%s/%s?via=%s`, formatter.bridge.Config.Homeserver.Domain, RoomMXID, msgMXID, formatter.bridge.Config.Homeserver.Domain) - href2 := fmt.Sprintf(`https://%s/#/user/%s`, formatter.bridge.Config.Homeserver.Domain, mxid) + href1 := fmt.Sprintf(`https://%s/#/room/%s/%s?via=%s`, formatter.bridge.Config.Homeserver.ServerName, RoomMXID, msgMXID, formatter.bridge.Config.Homeserver.Domain) + href2 := fmt.Sprintf(`https://%s/#/user/%s`, formatter.bridge.Config.Homeserver.ServerName, mxid) newContent := fmt.Sprintf(`
In reply to %s
%s
`, href1, href2, @@ -170,9 +170,10 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM if len(matches) > 0 { for _, match := range matches { mxid, displayname := formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) - number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) - originStr = strings.ReplaceAll(originStr, match[0], fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.Domain, mxid, displayname)) - originBodyStr = strings.Replace(originStr, number, displayname, -1) + // number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) + replaceStr := fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.ServerName, mxid, displayname) + originStr = strings.ReplaceAll(originStr, match[0], replaceStr) + originBodyStr = strings.ReplaceAll(originStr, replaceStr, displayname + ":") } if len(backStr) == 0 { content.Format = event.FormatHTML diff --git a/portal.go b/portal.go index ab46068..0bd0ffb 100644 --- a/portal.go +++ b/portal.go @@ -2042,7 +2042,7 @@ func (portal *Portal) convertMatrixMessageSkype(sender *User, evt *event.Event) } // mention user message - r := regexp.MustCompile(`(?m)]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.Domain + `/#/@([^"]+):(.*?)">(.*?)`) + r := regexp.MustCompile(`(?m)]+\bhref="(.*?)://` + portal.bridge.Config.Homeserver.ServerName + `/#/@([^"]+):(.*?)">(.*?)`) matches := r.FindAllStringSubmatch(matchStr, -1) fmt.Println("matches: ", matches) if len(matches) > 0 { From c07674ad6329633b7d6286ae4fc334e4ab12c357 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 28 Jan 2021 20:08:04 +0800 Subject: [PATCH 081/109] delete useless code --- user.go | 54 ------------------------------------------------------ 1 file changed, 54 deletions(-) diff --git a/user.go b/user.go index 21f92c5..018679c 100644 --- a/user.go +++ b/user.go @@ -12,8 +12,6 @@ import ( "sync" "time" - //"github.com/pkg/errors" - "github.com/skip2/go-qrcode" log "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" @@ -328,58 +326,6 @@ func (user *User) IsLoginInProgress() bool { return user.Conn != nil && user.Conn.IsLoginInProgress() } -func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- id.EventID) { - var qrEventID id.EventID - for code := range qrChan { - if code == "stop" { - return - } - qrCode, err := qrcode.Encode(code, qrcode.Low, 256) - if err != nil { - user.log.Errorln("Failed to encode QR code:", err) - ce.Reply("Failed to encode QR code: %v", err) - return - } - - bot := user.bridge.AS.BotClient() - - resp, err := bot.UploadBytes(qrCode, "image/png") - if err != nil { - user.log.Errorln("Failed to upload QR code:", err) - ce.Reply("Failed to upload QR code: %v", err) - return - } - - if qrEventID == "" { - sendResp, err := bot.SendImage(ce.RoomID, code, resp.ContentURI) - if err != nil { - user.log.Errorln("Failed to send QR code to user:", err) - return - } - qrEventID = sendResp.EventID - eventIDChan <- qrEventID - } else { - _, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{ - MsgType: event.MsgImage, - Body: code, - URL: resp.ContentURI.CUString(), - NewContent: &event.MessageEventContent{ - MsgType: event.MsgImage, - Body: code, - URL: resp.ContentURI.CUString(), - }, - RelatesTo: &event.RelatesTo{ - Type: event.RelReplace, - EventID: qrEventID, - }, - }) - if err != nil { - user.log.Errorln("Failed to send edited QR code to user:", err) - } - } - } -} - func (user *User) Login(ce *CommandEvent, name string, password string) (err error) { if user.contactsPresence == nil { user.contactsPresence = make(map[string]*skypeExt.Presence) From 6fd52056fbab3cb235ee12d4630d1786f5759aef Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 2 Feb 2021 19:19:02 +0800 Subject: [PATCH 082/109] fix bug # when a user is not logged in to skype and sends a message in a synchronized matrix room, the program will crash --- matrix.go | 6 +++--- portal.go | 54 +++++++++++++++++++++++++++--------------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/matrix.go b/matrix.go index a7ed35d..69fcfde 100644 --- a/matrix.go +++ b/matrix.go @@ -257,7 +257,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { } user := mx.bridge.GetUserByMXID(evt.Sender) - if user == nil || user.Conn == nil || !user.Whitelisted || !user.IsConnected() { + if user == nil || user.Conn == nil || user.Conn.LoginInfo == nil || !user.Whitelisted || !user.IsConnected() { return } @@ -307,7 +307,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { } portal := mx.bridge.GetPortalByMXID(evt.RoomID) - if user.Conn == nil || portal == nil || portal.IsPrivateChat() { + if user.Conn == nil || user.Conn.LoginInfo == nil || portal == nil || portal.IsPrivateChat() { return } @@ -449,7 +449,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) { } portal := mx.bridge.GetPortalByMXID(evt.RoomID) - if user.Conn != nil && portal != nil { + if user.Conn != nil && user.Conn.LoginInfo != nil && portal != nil { portal.HandleMatrixRedaction(user, evt) } } diff --git a/portal.go b/portal.go index 0bd0ffb..d7489e0 100644 --- a/portal.go +++ b/portal.go @@ -1420,8 +1420,8 @@ func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event. "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, } } - fmt.Println() - fmt.Printf("portal sendMessage0: %+v", content) + fmt.Println("portal sendMessage timestamp:", timestamp) + fmt.Printf("portal sendMessage: %+v", content) if portal.Encrypted && portal.bridge.Crypto != nil { encrypted, err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, wrappedContent) if err != nil { @@ -1431,12 +1431,8 @@ func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event. wrappedContent.Parsed = encrypted } if timestamp == 0 { - fmt.Println() - fmt.Printf("portal sendMessage1: %+v", content) return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent) } else { - fmt.Println() - fmt.Printf("portal sendMessage2: %+v", content) return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp) } } @@ -2226,30 +2222,34 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { func SendMsg(sender *User, chatThreadId string, content *skype.SendMessage, output chan<- error) (err error) { fmt.Println("message SendMsg type: ", content.Type) - switch event.MessageType(content.Type) { - case event.MsgText, event.MsgEmote, event.MsgNotice: - err = sender.Conn.SendText(chatThreadId, content) - case event.MsgImage: - fmt.Println("message SendMsg type m.image: ", content.Type) - err = sender.Conn.SendFile(chatThreadId, content) - case event.MsgVideo: - fmt.Println("message SendMsg type m.video: ", content.Type) - err = sender.Conn.SendFile(chatThreadId, content) - case event.MsgAudio: - fmt.Println("message SendMsg type m.audio: ", content.Type) - err = sender.Conn.SendFile(chatThreadId, content) - case event.MsgFile: - fmt.Println("message SendMsg type m.file: ", content.Type) - err = sender.Conn.SendFile(chatThreadId, content) - case event.MsgLocation: - fmt.Println("message SendMsg type m.location: ", content.Type) - //err = c.SendFile(chatThreadId, content) - default: - err = errors.New("send to skype(unknown message type)") + if sender.Conn.LoginInfo != nil { + switch event.MessageType(content.Type) { + case event.MsgText, event.MsgEmote, event.MsgNotice: + err = sender.Conn.SendText(chatThreadId, content) + case event.MsgImage: + fmt.Println("message SendMsg type m.image: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgVideo: + fmt.Println("message SendMsg type m.video: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgAudio: + fmt.Println("message SendMsg type m.audio: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgFile: + fmt.Println("message SendMsg type m.file: ", content.Type) + err = sender.Conn.SendFile(chatThreadId, content) + case event.MsgLocation: + fmt.Println("message SendMsg type m.location: ", content.Type) + //err = c.SendFile(chatThreadId, content) + default: + err = errors.New("send to skype(unknown message type)") + } + } else { + err = errors.New("Not logged into Skype or Skype session has expired") } if err != nil { - output <- fmt.Errorf("message sending responded with %d", err) + output <- err } else { output <- nil } From 87bac2ca76f2b4adf42918e66a9c61a061f490da Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sat, 6 Feb 2021 17:37:01 +0800 Subject: [PATCH 083/109] update go-skypeapi --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index db6133e..24cf9b1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/chai2010/webp v1.1.0 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4 + github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09 github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index beea6be..4c5ae4b 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/kelaresg/go-skypeapi v0.1.2-0.20201211120317-8651f9f08575 h1:4M0mtPS+ github.com/kelaresg/go-skypeapi v0.1.2-0.20201211120317-8651f9f08575/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4 h1:8tif5ndoTRHAOj4j2Ump9uyDaKoQ/W17dJ2K36G418U= github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09 h1:Hy1sgotN3qqglcmoT4G295UiX7Ucdyn/mGq/GlwDNgc= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= From 403e28baa1051516a62329d9a5bd382288f6b933 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 18 Feb 2021 17:55:15 +0800 Subject: [PATCH 084/109] better --- commands.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands.go b/commands.go index f3ff562..ad980dc 100644 --- a/commands.go +++ b/commands.go @@ -297,6 +297,10 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { for _, portal := range allPortals { leave(portal) } + ce.User.Conn.Store = &skype.Store{ + Contacts: make(map[string]skype.Contact), + Chats: make(map[string]skype.Conversation), + } ce.Reply("Logged out successfully.") } From c2a347096b4b9c2ce100dfffe652886ca4a4e5d7 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sat, 20 Feb 2021 14:54:00 +0800 Subject: [PATCH 085/109] better for leave room with command 'logout' --- commands.go | 30 +++++++++++++++--------------- database/user.go | 11 +++++++++++ portal.go | 3 ++- user.go | 17 ----------------- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/commands.go b/commands.go index ad980dc..d4ad9c0 100644 --- a/commands.go +++ b/commands.go @@ -251,7 +251,7 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { if ce.User.Conn == nil { return } - _ = ce.User.Conn.GetConversations("", ce.User.bridge.Config.Bridge.InitialChatSync) + //_ = ce.User.Conn.GetConversations("", ce.User.bridge.Config.Bridge.InitialChatSync) ce.User.Conn.LoggedIn = false username := "" password := "" @@ -272,19 +272,19 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { } portals := ce.User.GetPortals() - newPortals := ce.User.GetPortalsNew() - allPortals := newPortals[0:] - for _, portal := range portals { - var newPortalsHas bool - for _, newPortal := range newPortals { - if portal.Key == newPortal.Key { - newPortalsHas = true - } - } - if !newPortalsHas { - allPortals = append(allPortals, portal) - } - } + //newPortals := ce.User.GetPortalsNew() + //allPortals := newPortals[0:] + //for _, portal := range portals { + // var newPortalsHas bool + // for _, newPortal := range newPortals { + // if portal.Key == newPortal.Key { + // newPortalsHas = true + // } + // } + // if !newPortalsHas { + // allPortals = append(allPortals, portal) + // } + //} leave := func(portal *Portal) { if len(portal.MXID) > 0 { @@ -294,7 +294,7 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { }) } } - for _, portal := range allPortals { + for _, portal := range portals { leave(portal) } ce.User.Conn.Store = &skype.Store{ diff --git a/database/user.go b/database/user.go index 14ed243..45ac771 100644 --- a/database/user.go +++ b/database/user.go @@ -183,6 +183,17 @@ func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error { return tx.Commit() } +func (user *User) CreateUserPortal(newKey PortalKeyWithMeta) { + user.log.Debugfln("Creating new portal %s for %s", newKey.PortalKey.JID, newKey.PortalKey.Receiver) + _, err := user.db.Exec(`INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES ($1, $2, $3, $4)`, + user.jidPtr(), + newKey.PortalKey.JID, newKey.PortalKey.Receiver, + newKey.InCommunity) + if err != nil { + user.log.Warnfln("Failed to insert %s: %v", user.MXID, err) + } +} + func (user *User) IsInPortal(key PortalKey) bool { row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver) var exists bool diff --git a/portal.go b/portal.go index d7489e0..4c453d8 100644 --- a/portal.go +++ b/portal.go @@ -1237,7 +1237,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { _ = customPuppet.CustomIntent().EnsureJoined(portal.MXID) } } - user.addPortalToCommunity(portal) + inCommunity := user.addPortalToCommunity(portal) if portal.IsPrivateChat() { puppet := user.bridge.GetPuppetByJID(portal.Key.JID) user.addPuppetToCommunity(puppet) @@ -1249,6 +1249,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { } } } + user.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity}) err = portal.FillInitialHistory(user) if err != nil { portal.log.Errorln("Failed to fill history:", err) diff --git a/user.go b/user.go index 018679c..f3ca29e 100644 --- a/user.go +++ b/user.go @@ -463,23 +463,6 @@ func (user *User) intPostLogin() { } } -func (user *User) HandleChatList(chats []skype.Conversation) { - user.log.Infoln("Chat list received") - chatMap := make(map[string]skype.Conversation) - //for _, chat := range user.Conn.Store.Chats { - // chatMap[chat.Jid] = chat - //} - for _, chat := range chats { - cid, _ := chat.Id.(string) - chatMap[cid] = chat - } - select { - case user.chatListReceived <- struct{}{}: - default: - } - go user.syncPortals(chatMap, false) -} - func (user *User) syncPortals(chatMap map[string]skype.Conversation, createAll bool) { if chatMap == nil { chatMap = user.Conn.Store.Chats From 067228b70074750b986c9be622e5fdaae449c74e Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sat, 20 Feb 2021 22:39:29 +0800 Subject: [PATCH 086/109] update go-skypeapi --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 24cf9b1..178fc70 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/chai2010/webp v1.1.0 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09 + github.com/kelaresg/go-skypeapi v0.1.2-0.20210220112556-a87881796d7b github.com/lib/pq v1.7.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 From 9bb85282de2b170775c115e5a92099b270dbbb29 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 22 Feb 2021 17:49:22 +0800 Subject: [PATCH 087/109] fix the username response(old skype account) --- commands.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commands.go b/commands.go index d4ad9c0..9d35cde 100644 --- a/commands.go +++ b/commands.go @@ -479,6 +479,9 @@ func (handler *CommandHandler) CommandPing(ce *CommandEvent) { if len(ce.User.Conn.UserProfile.LastName) > 0 { username = username + ce.User.Conn.UserProfile.LastName } + if username == "" { + username = ce.User.Conn.UserProfile.Username + } ce.Reply("You're logged in as @" + username) } } From e91fa5506c415086f08dddf4015b6fd38ab3e392 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Mon, 22 Feb 2021 18:59:11 +0800 Subject: [PATCH 088/109] fix detect the type of media message --- go.mod | 1 + go.sum | 2 ++ portal.go | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 178fc70..73626dd 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/chai2010/webp v1.1.0 + github.com/gabriel-vasile/mimetype v1.1.2 github.com/gorilla/websocket v1.4.2 github.com/kelaresg/go-skypeapi v0.1.2-0.20210220112556-a87881796d7b github.com/lib/pq v1.7.0 diff --git a/go.sum b/go.sum index 4c5ae4b..a5b19c2 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU= +github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= diff --git a/portal.go b/portal.go index 4c453d8..192e404 100644 --- a/portal.go +++ b/portal.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "github.com/gabriel-vasile/mimetype" "maunium.net/go/mautrix/patch" "encoding/hex" @@ -1622,8 +1623,7 @@ func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn * } // synapse doesn't handle webp well, so we convert it. This can be dropped once https://github.com/matrix-org/synapse/issues/4382 is fixed - mimeType := http.DetectContentType(data) - //length := len(data) + mimeType := mimetype.Detect(data).String() if mimeType == "image/webp" { img, err := decodeWebp(bytes.NewReader(data)) if err != nil { From 6d18d6bb328e9fad2d6abda28cb2c5561f51cb79 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 24 Feb 2021 15:44:20 +0800 Subject: [PATCH 089/109] forward message(still need to optimize if have a good idea) --- formatting.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/formatting.go b/formatting.go index 8940f3e..54bad50 100644 --- a/formatting.go +++ b/formatting.go @@ -122,7 +122,7 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM // parse quote message(set reply) content.Body = strings.ReplaceAll(content.Body, "\n", "") - quoteReg := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)" conversation.* messageid="([^"]+)".*>.*?(.*?).*?(.*)`) + quoteReg := regexp.MustCompile(`]+\bauthor="([^"]+)" authorname="([^"]+)" timestamp="([^"]+)" conversation="([^"]+)" messageid="([^"]+)".*>.*?(.*?).*?(.*)`) quoteMatches := quoteReg.FindAllStringSubmatch(content.Body, -1) if len(quoteMatches) > 0 { @@ -132,8 +132,14 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM fmt.Println("ParseSkype quoteMatches a:", a) fmt.Println() } + portal := formatter.bridge.GetPortalByMXID(RoomMXID) + if portal.Key.JID != match[4] { + content.FormattedBody = match[6] + content.Body = fmt.Sprintf("%s\n\n", match[6]) + continue + } msgMXID := "" - msg := formatter.bridge.DB.Message.GetByID(match[4]) + msg := formatter.bridge.DB.Message.GetByID(match[5]) if msg != nil { msgMXID = string(msg.MXID) } @@ -144,21 +150,21 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM href1, href2, mxid, - match[5]) + match[6]) content.FormattedBody = newContent - content.Body = fmt.Sprintf("> <%s> %s\n\n", mxid, match[5]) + content.Body = fmt.Sprintf("> <%s> %s\n\n", mxid, match[6]) inRelateTo := &event.RelatesTo{ Type: event.RelReply, EventID: id.EventID(msgMXID), } content.SetRelatesTo(inRelateTo) - backStr = match[6] + backStr = match[7] } } } // parse mention user message - r := regexp.MustCompile(`(?m)]+\bid="([^"]+)"(.*?)`) + r := regexp.MustCompile(`(?m)]+\bid="([^"]+)">(.*?)`) var originStr string var originBodyStr string if len(backStr) == 0 { @@ -170,8 +176,14 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM if len(matches) > 0 { for _, match := range matches { mxid, displayname := formatter.getMatrixInfoByJID(match[1] + skypeExt.NewUserSuffix) + replaceStr := "" + if len(displayname) < 1 { + // TODO need to optimize + replaceStr = match[2] + ":" + } else { + replaceStr = fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.ServerName, mxid, displayname) + } // number := "@" + strings.Replace(match[1], skypeExt.NewUserSuffix, "", 1) - replaceStr := fmt.Sprintf(`%s:`, formatter.bridge.Config.Homeserver.ServerName, mxid, displayname) originStr = strings.ReplaceAll(originStr, match[0], replaceStr) originBodyStr = strings.ReplaceAll(originStr, replaceStr, displayname + ":") } From eb8e1cb66030eb3107fc9c5725a07b6dc5fa5e97 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sat, 8 May 2021 16:41:12 +0800 Subject: [PATCH 090/109] update lib mautrix-go --- crypto.go | 10 +++++----- go.mod | 7 +++---- go.sum | 28 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crypto.go b/crypto.go index f67db9a..14ecc97 100644 --- a/crypto.go +++ b/crypto.go @@ -122,20 +122,20 @@ func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { return nil, fmt.Errorf("failed to initialize client: %w", err) } client.Logger = helper.baseLog.Sub("Bot") + client.Client = helper.bridge.AS.HTTPClient + client.DefaultHTTPRetries = helper.bridge.AS.DefaultHTTPRetries flows, err := client.GetLoginFlows() if err != nil { return nil, fmt.Errorf("failed to get supported login flows: %w", err) } - if !flows.HasFlow(mautrix.AuthTypeAppservice) { - // TODO after synapse 1.22, turn this into an error - helper.log.Warnln("Encryption enabled in config, but homeserver does not advertise appservice login") - //return nil, fmt.Errorf("homeserver does not support appservice login") + if !flows.HasFlow(mautrix.AuthTypeHalfyAppservice) { + return nil, fmt.Errorf("homeserver does not support appservice login") } // We set the API token to the AS token here to authenticate the appservice login // It'll get overridden after the login client.AccessToken = helper.bridge.AS.Registration.AppToken resp, err := client.Login(&mautrix.ReqLogin{ - Type: mautrix.AuthTypeAppservice, + Type: mautrix.AuthTypeHalfyAppservice, Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, DeviceID: deviceID, InitialDeviceDisplayName: "Skype Bridge", diff --git a/go.mod b/go.mod index 73626dd..c86ef07 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,14 @@ require ( github.com/gabriel-vasile/mimetype v1.1.2 github.com/gorilla/websocket v1.4.2 github.com/kelaresg/go-skypeapi v0.1.2-0.20210220112556-a87881796d7b - github.com/lib/pq v1.7.0 + github.com/lib/pq v1.9.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 - github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e golang.org/x/image v0.0.0-20200618115811-c13761719519 gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 - maunium.net/go/maulogger/v2 v2.1.1 + maunium.net/go/maulogger/v2 v2.2.4 maunium.net/go/mautrix v0.8.0-rc.4 ) -replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201208081810-787323a21113 +replace maunium.net/go/mautrix => github.com/pidongqianqian/mautrix-go v0.9.11-0.20210508035357-93e21d8c2bbe diff --git a/go.sum b/go.sum index a5b19c2..539beb3 100644 --- a/go.sum +++ b/go.sum @@ -115,6 +115,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -178,6 +180,8 @@ github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= +github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -185,6 +189,7 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -224,6 +229,8 @@ github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc h github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201208081810-787323a21113 h1:tetxcECwIGgb7Gl4AyY/9+MXmuUfp33FweuGU/Bb7AM= github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201208081810-787323a21113/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= +github.com/pidongqianqian/mautrix-go v0.9.11-0.20210508035357-93e21d8c2bbe h1:ZPK2QEV0zE5jUHl00AW0xl6kdCxtBaWxklcnMHtQGbs= +github.com/pidongqianqian/mautrix-go v0.9.11-0.20210508035357-93e21d8c2bbe/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -251,6 +258,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= @@ -288,13 +297,21 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= +github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= +github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= +github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= +github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE= +github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tulir/go-whatsapp v0.2.8 h1:0dpAQ/2ONT6T2//aAl+9IeFGMRtqOZpMEqJlhX9vxos= github.com/tulir/go-whatsapp v0.2.8/go.mod h1:gyw9zGup1/Y3ZQUueZaqz3iR/WX9a2Lth4aqEbXjkok= @@ -317,6 +334,8 @@ golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -361,6 +380,8 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYc golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs= golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -385,6 +406,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= @@ -393,6 +415,10 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -477,6 +503,8 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= +maunium.net/go/maulogger/v2 v2.2.4 h1:oV2GDeM4fx1uRysdpDC0FcrPg+thFicSd9XzPcYMbVY= +maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/mautrix v0.7.13 h1:qfnvLxvQafvLgHbdZF/+9qs9gyArYf8fUnzfQbjgQaU= maunium.net/go/mautrix v0.7.13/go.mod h1:Jn0ijwXwMFvJFIN9IljirIVKpZQbZP/Dk7pdX2qDmXk= maunium.net/go/mautrix-whatsapp v0.1.4 h1:qlkb3eXcKm1QE6AjrAl9aKxokHlwj7BNr+aUQFXFmWE= From 7b6370532ea533eb0887d49c04a193bb3ad4a613 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Sat, 8 May 2021 16:42:07 +0800 Subject: [PATCH 091/109] optimize forward message and details --- formatting.go | 6 +++++- matrix.go | 2 +- portal.go | 9 +++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/formatting.go b/formatting.go index 54bad50..9c8d881 100644 --- a/formatting.go +++ b/formatting.go @@ -136,7 +136,11 @@ func (formatter *Formatter) ParseSkype(content *event.MessageEventContent, RoomM if portal.Key.JID != match[4] { content.FormattedBody = match[6] content.Body = fmt.Sprintf("%s\n\n", match[6]) - continue + + // this means that there are forwarding messages across groups + if strings.HasSuffix(match[4], skypeExt.GroupSuffix) || strings.HasSuffix(portal.Key.JID, skypeExt.GroupSuffix){ + continue + } } msgMXID := "" msg := formatter.bridge.DB.Message.GetByID(match[5]) diff --git a/matrix.go b/matrix.go index 69fcfde..74a5396 100644 --- a/matrix.go +++ b/matrix.go @@ -366,7 +366,7 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { fmt.Println() return true } - isCustomPuppet, ok := evt.Content.Raw["net.maunium.whatsapp.puppet"].(bool) + isCustomPuppet, ok := evt.Content.Raw["net.maunium.skype.puppet"].(bool) if ok && isCustomPuppet && mx.bridge.GetPuppetByCustomMXID(evt.Sender) != nil { return true } diff --git a/portal.go b/portal.go index 192e404..206a289 100644 --- a/portal.go +++ b/portal.go @@ -1419,7 +1419,7 @@ func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event. wrappedContent := event.Content{Parsed: content} if timestamp != 0 && intent.IsCustomPuppet { wrappedContent.Raw = map[string]interface{}{ - "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, + "net.maunium.skype.puppet": intent.IsCustomPuppet, } } fmt.Println("portal sendMessage timestamp:", timestamp) @@ -1477,17 +1477,14 @@ func (portal *Portal) HandleTextMessage(source *User, message skype.Resource) { } } // portal.SetReplySkype(content, message) - - fmt.Println() - fmt.Printf("portal HandleTextMessage2: %+v", content) + fmt.Printf("\nportal HandleTextMessage2: %+v", content) _, _ = intent.UserTyping(portal.MXID, false, 0) resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) return } - fmt.Println() - fmt.Printf("portal HandleTextMessage3: %+v", content) + fmt.Printf("\nportal HandleTextMessage3: %+v", content) portal.finishHandlingSkype(source, &message, resp.EventID) } } From 55f909a47fce463fc8530f5fa9abcb239ea4f80b Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 12 May 2021 18:49:00 +0800 Subject: [PATCH 092/109] fix sync media message delete --- portal.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/portal.go b/portal.go index 206a289..474854d 100644 --- a/portal.go +++ b/portal.go @@ -1593,6 +1593,11 @@ func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI } func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn *skype.Conn, mediaType string) (data []byte, mediaMessage *skype.MediaMessageContent, err error), mediaType string, thumbnail []byte, info skype.Resource, sendAsSticker bool) { + if info.ClientMessageId == "" && info.Content == "" && len(info.SkypeEditedId) > 0 { + portal.HandleMessageRevokeSkype(source, info) + return + } + intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, info) if endHandlePrivateChatFromMe != nil { defer endHandlePrivateChatFromMe() From 459b54955a1996da383f6570a3129b2e698191c1 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 14 May 2021 18:34:39 +0800 Subject: [PATCH 093/109] add return orgid --- commands.go | 10 +++++++++- user.go | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/commands.go b/commands.go index 9d35cde..a6864e4 100644 --- a/commands.go +++ b/commands.go @@ -7,6 +7,7 @@ import ( "github.com/kelaresg/matrix-skype/database" skypeExt "github.com/kelaresg/matrix-skype/skype-ext" "math" + "maunium.net/go/mautrix/patch" "sort" "strconv" @@ -482,7 +483,14 @@ func (handler *CommandHandler) CommandPing(ce *CommandEvent) { if username == "" { username = ce.User.Conn.UserProfile.Username } - ce.Reply("You're logged in as @" + username) + + orgId := "" + if patch.ThirdPartyIdEncrypt { + orgId = patch.Enc(strings.TrimSuffix(ce.User.JID, skypeExt.NewUserSuffix)) + } else { + orgId = strings.TrimSuffix(ce.User.JID, skypeExt.NewUserSuffix) + } + ce.Reply("You're logged in as @" + username + ", orgid is " + orgId) } } diff --git a/user.go b/user.go index f3ca29e..891ef88 100644 --- a/user.go +++ b/user.go @@ -6,6 +6,7 @@ import ( "fmt" skype "github.com/kelaresg/go-skypeapi" skypeExt "github.com/kelaresg/matrix-skype/skype-ext" + "maunium.net/go/mautrix/patch" "sort" //"strconv" "strings" @@ -333,7 +334,13 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err err = user.Conn.Login(name, password) if err != nil { user.log.Errorln("Failed to login:", err) - ce.Reply(err.Error()) + orgId := "" + if patch.ThirdPartyIdEncrypt { + orgId = patch.Enc(strings.TrimSuffix(user.JID, skypeExt.NewUserSuffix)) + } else { + orgId = strings.TrimSuffix(user.JID, skypeExt.NewUserSuffix) + } + ce.Reply(err.Error() + ", orgid is " + orgId) return err } username := user.Conn.UserProfile.FirstName @@ -343,7 +350,14 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err if username == "" { username = user.Conn.UserProfile.Username } - ce.Reply("Successfully logged in as @" + username) + + orgId := "" + if patch.ThirdPartyIdEncrypt { + orgId = patch.Enc(strings.TrimSuffix(user.JID, skypeExt.NewUserSuffix)) + } else { + orgId = strings.TrimSuffix(user.JID, skypeExt.NewUserSuffix) + } + ce.Reply("Successfully logged in as @" + username + ", orgid is " + orgId) user.Conn.Subscribes() // subscribe basic event err = user.Conn.Conn.ContactList(user.Conn.UserProfile.Username) From 9a39b69dce54237b186a03e02b0bfaa51ed3cff8 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 26 May 2021 20:39:02 +0800 Subject: [PATCH 094/109] in progress --- commands.go | 10 +++++++++- user.go | 27 ++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/commands.go b/commands.go index a6864e4..4e4ae8f 100644 --- a/commands.go +++ b/commands.go @@ -271,7 +271,6 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { Username: username, Password: password, } - portals := ce.User.GetPortals() //newPortals := ce.User.GetPortalsNew() //allPortals := newPortals[0:] @@ -303,6 +302,15 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { Chats: make(map[string]skype.Conversation), } ce.Reply("Logged out successfully.") + + //defer func() { + // if r := recover(); r != nil { + // fmt.Printf("close ReRefresh:%s\n", r) + // } + //}() + if ce.User.Conn.Refresh != nil { + ce.User.Conn.Refresh <- -1 + } } // CommandLogout handles !logout command diff --git a/user.go b/user.go index 891ef88..ce91661 100644 --- a/user.go +++ b/user.go @@ -285,6 +285,7 @@ func (user *User) Connect(evenIfNoSession bool) bool { //_ = user.Conn.SetClientName("matrix-skype bridge", "mx-wa", SkypeVersion) user.log.Debugln("skype connection successful") user.Conn.AddHandler(user) + return user.RestoreSession() } @@ -371,9 +372,10 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err userIds = append(userIds, userId) } ce.User.Conn.SubscribeUsers(userIds) - go loopPresence(ce, user) + go loopPresence(user) } go user.Conn.Poll() + go user.monitorSession(ce) user.ConnectionErrors = 0 user.JID = "8:" + user.Conn.UserProfile.Username + skypeExt.NewUserSuffix @@ -384,12 +386,27 @@ func (user *User) Login(ce *CommandEvent, name string, password string) (err err return } -func loopPresence(ce *CommandEvent, user *User) { - for { - if user.Conn.LoggedIn == false { +func (user *User) monitorSession(ce *CommandEvent) { + user.Conn.Refresh = make(chan int) + for x := range user.Conn.Refresh { + fmt.Println("monitorSession: ", x) + if x > 0 { + user.SetSession(user.Conn.LoginInfo) + } else { ce.Reply("Session expired") - break + close(user.Conn.Refresh) } + } + + item, ok := <- user.Conn.Refresh + if !ok { + user.Conn.Refresh = nil + } + fmt.Println("monitorSession1", item, ok) +} + +func loopPresence(user *User) { + for { for cid, contact := range user.contactsPresence { puppet := user.bridge.GetPuppetByJID(cid) _ = puppet.DefaultIntent().SetPresence(event.Presence(strings.ToLower(contact.Availability))) From 9d7d8224c4b0aab7bdde6bbd1a865bac96011008 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 27 May 2021 18:31:20 +0800 Subject: [PATCH 095/109] automatic refresh of access token --- main.go | 6 ++--- portal.go | 77 ++++++++++++++++++++++++++++++++++++------------------- user.go | 14 +++++++++- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/main.go b/main.go index 9bc4dda..1bb2dcb 100644 --- a/main.go +++ b/main.go @@ -244,8 +244,8 @@ func (bridge *Bridge) Start() { bridge.Log.Fatalln("Failed to initialize database:", err) os.Exit(15) } - bridge.Log.Debugln("Checking connection to homeserver") - bridge.ensureConnection() + //bridge.Log.Debugln("Checking connection to homeserver") + //bridge.ensureConnection() if bridge.Crypto != nil { err := bridge.Crypto.Init() if err != nil { @@ -377,7 +377,7 @@ func (bridge *Bridge) Main() { func main() { flag.SetHelpTitles( - "matrix-skype - A Matrix-WhatsApp puppeting bridge.", + "matrix-skype - A Matrix-Skype puppeting bridge.", "matrix-skype [-h] [-c ] [-r ] [-g] [--migrate-db ]") err := flag.Parse() if err != nil { diff --git a/portal.go b/portal.go index 474854d..b27c42b 100644 --- a/portal.go +++ b/portal.go @@ -1476,19 +1476,34 @@ func (portal *Portal) HandleTextMessage(source *User, message skype.Resource) { } } } - // portal.SetReplySkype(content, message) fmt.Printf("\nportal HandleTextMessage2: %+v", content) _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) - if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) - return + resp, err := portal.trySendMessage(intent, event.EventMessage, content, source, message) + if err == nil { + portal.finishHandlingSkype(source, &message, resp.EventID) } - fmt.Printf("\nportal HandleTextMessage3: %+v", content) - portal.finishHandlingSkype(source, &message, resp.EventID) } } +func (portal *Portal) trySendMessage(intent *appservice.IntentAPI, eventType event.Type, content interface{}, source *User, message skype.Resource) (resp *mautrix.RespSendEvent, err error) { + resp, err = portal.sendMessage(intent, eventType, content, message.Timestamp * 1000) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + if strings.Index(err.Error(), "M_UNKNOWN_TOKEN (HTTP 401)") > -1 { + puppet := source.bridge.GetPuppetByJID(source.JID) + err, accessToken := source.UpdateAccessToken(puppet) + if err == nil && accessToken != "" { + intent.AccessToken = accessToken + resp, err = portal.sendMessage(intent, eventType, content, message.Timestamp * 1000) + if err != nil { + portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + } + } + } + } + return +} + func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Resource) { intent, endHandlePrivateChatFromMe := portal.startHandlingSkype(source, message) if endHandlePrivateChatFromMe != nil { @@ -1517,12 +1532,17 @@ func (portal *Portal) HandleLocationMessageSkype(source *User, message skype.Res // portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) - if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) - return + + resp, err := portal.trySendMessage(intent, event.EventMessage, content, source, message) + if err == nil { + portal.finishHandlingSkype(source, &message, resp.EventID) } - portal.finishHandlingSkype(source, &message, resp.EventID) + //resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) + //if err != nil { + // portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + // return + //} + //portal.finishHandlingSkype(source, &message, resp.EventID) } func (portal *Portal) HandleContactMessageSkype(source *User, message skype.Resource) { @@ -1547,24 +1567,28 @@ func (portal *Portal) HandleContactMessageSkype(source *User, message skype.Reso // portal.SetReplySkype(content, message) _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) - if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) - return + resp, err := portal.trySendMessage(intent, event.EventMessage, content, source, message) + if err == nil { + portal.finishHandlingSkype(source, &message, resp.EventID) } - portal.finishHandlingSkype(source, &message, resp.EventID) + //resp, err := portal.sendMessage(intent, event.EventMessage, content, message.Timestamp * 1000) + //if err != nil { + // portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) + // return + //} + //portal.finishHandlingSkype(source, &message, resp.EventID) } func (portal *Portal) sendMediaBridgeFailureSkype(source *User, intent *appservice.IntentAPI, info skype.Resource, downloadErr error) { portal.log.Errorfln("Failed to download media for %s: %v", info.Id, downloadErr) - resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ + resp, err := portal.trySendMessage(intent, event.EventMessage, &event.MessageEventContent{ MsgType: event.MsgNotice, Body: "Failed to bridge media", - }, int64(info.Timestamp*1000)) - if err != nil { - portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) - } else { + }, source, info) + if err == nil { portal.finishHandlingSkype(source, &info, resp.EventID) + } else { + portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) } } @@ -1747,10 +1771,10 @@ func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn * if sendAsSticker { eventType = event.EventSticker } - resp, err := portal.sendMessage(intent, eventType, content, info.Timestamp * 1000) - if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err) - return + + resp, err := portal.trySendMessage(intent, eventType, content, source, info) + if err == nil { + portal.finishHandlingSkype(source, &info, resp.EventID) } //if len(caption) > 0 { @@ -1767,7 +1791,6 @@ func (portal *Portal) HandleMediaMessageSkype(source *User, download func(conn * // } // // TODO store caption mxid? //} - portal.finishHandlingSkype(source, &info, resp.EventID) } func makeMessageID() *string { diff --git a/user.go b/user.go index ce91661..e022807 100644 --- a/user.go +++ b/user.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "encoding/xml" + "errors" "fmt" skype "github.com/kelaresg/go-skypeapi" skypeExt "github.com/kelaresg/matrix-skype/skype-ext" @@ -461,7 +462,17 @@ func (user *User) tryAutomaticDoublePuppeting() { return } fmt.Println("tryAutomaticDoublePuppeting2", user.MXID) - accessToken, err := puppet.loginWithSharedSecret(user.MXID) + _,_ = user.UpdateAccessToken(puppet) +} + +func (user *User) UpdateAccessToken(puppet *Puppet) (err error, accessToken string) { + if len(user.bridge.Config.Bridge.LoginSharedSecret) == 0 { + return errors.New("you didn't set LoginSharedSecret"), "" + } else if _, homeserver, _ := user.MXID.Parse(); homeserver != user.bridge.Config.Homeserver.Domain { + // user is on another homeserver + return errors.New("user is on another homeServer"), "" + } + accessToken, err = puppet.loginWithSharedSecret(user.MXID) if err != nil { user.log.Warnln("Failed to login with shared secret:", err) return @@ -472,6 +483,7 @@ func (user *User) tryAutomaticDoublePuppeting() { return } user.log.Infoln("Successfully automatically enabled custom puppet") + return } func (user *User) intPostLogin() { From 619d8c1142154e1f7bf89024246852963393b2fb Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 27 May 2021 18:53:03 +0800 Subject: [PATCH 096/109] update go skypeapi --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c86ef07..5e49c21 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/chai2010/webp v1.1.0 github.com/gabriel-vasile/mimetype v1.1.2 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20210220112556-a87881796d7b + github.com/kelaresg/go-skypeapi v0.1.2-0.20210526124154-2e6d23e27010 github.com/lib/pq v1.9.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 From 18da6d1e70becc7d311e13af9f4dc82d61e0ba34 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 27 May 2021 18:54:17 +0800 Subject: [PATCH 097/109] update --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 539beb3..3cddb7c 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,8 @@ github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4 h1:8tif5ndo github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09 h1:Hy1sgotN3qqglcmoT4G295UiX7Ucdyn/mGq/GlwDNgc= github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210526124154-2e6d23e27010 h1:Mvm+MyoqKBUKbbZhIBpjSKNoiixinmNHBJtJ6EbfpuE= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210526124154-2e6d23e27010/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= From 7609b9d8ae85c53b103dcc8e16ad3676e05650d2 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 3 Jun 2021 19:37:52 +0800 Subject: [PATCH 098/109] fix can't auto exit skype room even if user relogin skype and logout. fix cannot sync room to matrix(isn't recently room but synced before) --- commands.go | 64 ++++++++++++----------------------------------------- portal.go | 21 ++++++++++++++++-- 2 files changed, 33 insertions(+), 52 deletions(-) diff --git a/commands.go b/commands.go index 4e4ae8f..394fbcb 100644 --- a/commands.go +++ b/commands.go @@ -235,7 +235,7 @@ func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { ce.Reply("**Usage:** `login username password`") return } - + leavePortals(ce) if !ce.User.Connect(true) { ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.") return @@ -271,6 +271,19 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { Username: username, Password: password, } + + ce.User.Conn.Store = &skype.Store{ + Contacts: make(map[string]skype.Contact), + Chats: make(map[string]skype.Conversation), + } + ce.Reply("Logged out successfully.") + leavePortals(ce) + if ce.User.Conn.Refresh != nil { + ce.User.Conn.Refresh <- -1 + } +} + +func leavePortals(ce *CommandEvent) { portals := ce.User.GetPortals() //newPortals := ce.User.GetPortalsNew() //allPortals := newPortals[0:] @@ -297,57 +310,8 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { for _, portal := range portals { leave(portal) } - ce.User.Conn.Store = &skype.Store{ - Contacts: make(map[string]skype.Contact), - Chats: make(map[string]skype.Conversation), - } - ce.Reply("Logged out successfully.") - - //defer func() { - // if r := recover(); r != nil { - // fmt.Printf("close ReRefresh:%s\n", r) - // } - //}() - if ce.User.Conn.Refresh != nil { - ce.User.Conn.Refresh <- -1 - } } -// CommandLogout handles !logout command -//func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { -// if ce.User.Session == nil { -// ce.Reply("You're not logged in.") -// return -// } else if !ce.User.IsConnected() { -// ce.Reply("You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information.") -// return -// } -// puppet := handler.bridge.GetPuppetByJID(ce.User.JID) -// if puppet.CustomMXID != "" { -// err := puppet.SwitchCustomMXID("", "") -// if err != nil { -// ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err) -// } -// } -// err := ce.User.Conn.Logout() -// if err != nil { -// ce.User.log.Warnln("Error while logging out:", err) -// ce.Reply("Unknown error while logging out: %v", err) -// return -// } -// _, err = ce.User.Conn.Disconnect() -// if err != nil { -// ce.User.log.Warnln("Error while disconnecting after logout:", err) -// } -// ce.User.Conn.RemoveHandlers() -// ce.User.Conn = nil -// ce.User.removeFromJIDMap() -// // TODO this causes a foreign key violation, which should be fixed -// //ce.User.JID = "" -// ce.User.SetSession(nil) -// ce.Reply("Logged out successfully.") -//} - const cmdDeleteSessionHelp = `delete-session - Delete session information and disconnect from WhatsApp without sending a logout request` //func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) { diff --git a/portal.go b/portal.go index b27c42b..53d2b9c 100644 --- a/portal.go +++ b/portal.go @@ -176,6 +176,11 @@ func (portal *Portal) handleMessageLoop() { fmt.Printf("portal handleMessageLoop2: %+v", msg) return } + } else { + if !msg.source.IsInPortal(portal.Key) { + fmt.Println("portal handleMessageLoop InPortal:") + msg.source.CreateUserPortal(database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: false}) + } } fmt.Println() fmt.Printf("portal handleMessageLoop3: %+v", msg) @@ -1423,7 +1428,7 @@ func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event. } } fmt.Println("portal sendMessage timestamp:", timestamp) - fmt.Printf("portal sendMessage: %+v", content) + fmt.Printf("portal sendMessage: %+v\n", content) if portal.Encrypted && portal.bridge.Crypto != nil { encrypted, err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, wrappedContent) if err != nil { @@ -1499,6 +1504,15 @@ func (portal *Portal) trySendMessage(intent *appservice.IntentAPI, eventType eve portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) } } + } else if strings.Index(err.Error(), "M_FORBIDDEN (HTTP 403)") > -1 { + puppet := source.bridge.GetPuppetByJID(message.Jid) + intentP := puppet.IntentFor(portal) + _, err = intentP.InviteUser(portal.MXID, &mautrix.ReqInviteUser{ + UserID: intent.UserID, + }) + if err == nil { + resp, err = portal.sendMessage(intent, eventType, content, message.Timestamp * 1000) + } } } return @@ -2233,6 +2247,9 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { } if err != nil { portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) + if !sender.Conn.LoggedIn { + err = errors.New("Skype account has been logged out.") + } portal.sendErrorMessage(err) } else { portal.log.Debugfln("Handled Matrix event %s", evt.ID) @@ -2248,7 +2265,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { func SendMsg(sender *User, chatThreadId string, content *skype.SendMessage, output chan<- error) (err error) { fmt.Println("message SendMsg type: ", content.Type) - if sender.Conn.LoginInfo != nil { + if sender.Conn.LoginInfo != nil && sender.Conn.LoggedIn != false { switch event.MessageType(content.Type) { case event.MsgText, event.MsgEmote, event.MsgNotice: err = sender.Conn.SendText(chatThreadId, content) From 090dd9394a138152cc2fe657d504d6a6641502a0 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 4 Jun 2021 12:59:13 +0800 Subject: [PATCH 099/109] update goskyapi --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5e49c21..43e87ab 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/chai2010/webp v1.1.0 github.com/gabriel-vasile/mimetype v1.1.2 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20210526124154-2e6d23e27010 + github.com/kelaresg/go-skypeapi v0.1.2-0.20210603114428-9c103d451110 github.com/lib/pq v1.9.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 From 8104db9562868b01a19cc01945b6d608dead7184 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 9 Jun 2021 19:31:53 +0800 Subject: [PATCH 100/109] fix bug --- main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 1bb2dcb..b4d7d64 100644 --- a/main.go +++ b/main.go @@ -244,8 +244,8 @@ func (bridge *Bridge) Start() { bridge.Log.Fatalln("Failed to initialize database:", err) os.Exit(15) } - //bridge.Log.Debugln("Checking connection to homeserver") - //bridge.ensureConnection() + bridge.Log.Debugln("Checking connection to homeserver") + bridge.ensureConnection() if bridge.Crypto != nil { err := bridge.Crypto.Init() if err != nil { @@ -258,8 +258,8 @@ func (bridge *Bridge) Start() { bridge.Provisioning.Init() } bridge.LoadRelaybot() - bridge.Log.Debugln("Checking connection to homeserver") - bridge.ensureConnection() + //bridge.Log.Debugln("Checking connection to homeserver") + //bridge.ensureConnection() bridge.Log.Debugln("Starting application service HTTP server") go bridge.AS.Start() bridge.Log.Debugln("Starting event processor") From 98e3f1669c43a0ed83458b082643270b1228a048 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 25 Jun 2021 19:13:27 +0800 Subject: [PATCH 101/109] logout when session expired --- commands.go | 3 ++- user.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/commands.go b/commands.go index 394fbcb..b3861d9 100644 --- a/commands.go +++ b/commands.go @@ -277,9 +277,10 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { Chats: make(map[string]skype.Conversation), } ce.Reply("Logged out successfully.") - leavePortals(ce) if ce.User.Conn.Refresh != nil { ce.User.Conn.Refresh <- -1 + } else { + leavePortals(ce) } } diff --git a/user.go b/user.go index e022807..40a2118 100644 --- a/user.go +++ b/user.go @@ -396,6 +396,7 @@ func (user *User) monitorSession(ce *CommandEvent) { } else { ce.Reply("Session expired") close(user.Conn.Refresh) + leavePortals(ce) } } From 94d62d1996cd91fdf77e2c534d22595ee17baa17 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 6 Jul 2021 15:35:32 +0800 Subject: [PATCH 102/109] update go.sum --- go.sum | 123 ++------------------------------------------------------- 1 file changed, 3 insertions(+), 120 deletions(-) diff --git a/go.sum b/go.sum index 3cddb7c..57f742e 100644 --- a/go.sum +++ b/go.sum @@ -19,9 +19,7 @@ github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154Oa github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -29,7 +27,6 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -43,7 +40,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ= github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOYvd3Ow= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= @@ -55,7 +51,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -69,7 +64,6 @@ github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pm github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= @@ -85,26 +79,14 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -113,8 +95,6 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -153,21 +133,12 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858 h1:dHavsrpbaOA/fqwB39zndM1uHIy84EDKhldWajIcfJg= -github.com/kelaresg/go-skypeapi v0.1.2-0.20201126103218-226d1ec92858/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= -github.com/kelaresg/go-skypeapi v0.1.2-0.20201211120317-8651f9f08575 h1:4M0mtPS+W+In2sjh/g+/gfagn7mMG6igvMOLl2zgiKc= -github.com/kelaresg/go-skypeapi v0.1.2-0.20201211120317-8651f9f08575/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4 h1:8tif5ndoTRHAOj4j2Ump9uyDaKoQ/W17dJ2K36G418U= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210120095455-33c3f50415c4/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09 h1:Hy1sgotN3qqglcmoT4G295UiX7Ucdyn/mGq/GlwDNgc= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210128115834-086b2e03dd09/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210526124154-2e6d23e27010 h1:Mvm+MyoqKBUKbbZhIBpjSKNoiixinmNHBJtJ6EbfpuE= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210526124154-2e6d23e27010/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210603114428-9c103d451110 h1:QD7o9DfiLKVTP3jzYLFa/3UY8XDEH+l+QEFLFPHeyVg= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210603114428-9c103d451110/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= @@ -178,10 +149,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= -github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= -github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= @@ -190,7 +157,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -204,9 +170,7 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -218,19 +182,6 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4 h1:FkwHv3b/K6jsNo8rFk7VCsdJ7HL61AcjqAS0DqDYLyk= -github.com/pidongqianqian/mautrix-go v0.5.0-rc.3.0.20200613150057-bd5519f2ccd4/go.mod h1:LnkFnB1yjCbb8V+upoEHDGvI/F38NHSTWYCe2RRJgSY= -github.com/pidongqianqian/mautrix-go v0.7.2/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= -github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106123139-e1c6c37e09d6 h1:RKxi11Gkg48exSKQSFBYGduK5xfJ72MdcaeMMfsMF4w= -github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106123139-e1c6c37e09d6/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= -github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d h1:cw6XWBIvj8DaRO/rWzndd4q5Yzbei6Sgj/UUPHuCWv4= -github.com/pidongqianqian/mautrix-go v0.7.3-0.20201106154702-3c2230569f1d/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= -github.com/pidongqianqian/mautrix-go v0.7.3-0.20201126040927-dbde233d88f6 h1:A2W0H1dcKLw1f4z5hA5BET0mAn3gyhwyYGBhYN6Dz9g= -github.com/pidongqianqian/mautrix-go v0.7.3-0.20201126040927-dbde233d88f6/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= -github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc h1:wXc6kSKzzYrLgqJwaCgJvKZc4Qfq85/3TivSItUFuNk= -github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201126070406-7b13ac473bcc/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= -github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201208081810-787323a21113 h1:tetxcECwIGgb7Gl4AyY/9+MXmuUfp33FweuGU/Bb7AM= -github.com/pidongqianqian/mautrix-go v0.8.0-rc.4.0.20201208081810-787323a21113/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= github.com/pidongqianqian/mautrix-go v0.9.11-0.20210508035357-93e21d8c2bbe h1:ZPK2QEV0zE5jUHl00AW0xl6kdCxtBaWxklcnMHtQGbs= github.com/pidongqianqian/mautrix-go v0.9.11-0.20210508035357-93e21d8c2bbe/go.mod h1:7IzKfWvpQtN+W2Lzxc0rLvIxFM3ryKX6Ys3S/ZoWbg8= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -242,36 +193,20 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= -github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= -github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -292,31 +227,19 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= -github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.6.8 h1:CTmXMClGYPAmln7652e69B7OLXfTi5ABcPPwjIWUv7w= github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= -github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= -github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= -github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= -github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE= github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tulir/go-whatsapp v0.2.8 h1:0dpAQ/2ONT6T2//aAl+9IeFGMRtqOZpMEqJlhX9vxos= -github.com/tulir/go-whatsapp v0.2.8/go.mod h1:gyw9zGup1/Y3ZQUueZaqz3iR/WX9a2Lth4aqEbXjkok= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -327,15 +250,10 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -345,8 +263,7 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -374,14 +291,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs= -golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU= golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -392,7 +303,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -403,27 +313,18 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -447,7 +348,6 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -469,19 +369,10 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= @@ -490,9 +381,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= @@ -503,12 +392,6 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0XA= -maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/maulogger/v2 v2.2.4 h1:oV2GDeM4fx1uRysdpDC0FcrPg+thFicSd9XzPcYMbVY= maunium.net/go/maulogger/v2 v2.2.4/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.7.13 h1:qfnvLxvQafvLgHbdZF/+9qs9gyArYf8fUnzfQbjgQaU= -maunium.net/go/mautrix v0.7.13/go.mod h1:Jn0ijwXwMFvJFIN9IljirIVKpZQbZP/Dk7pdX2qDmXk= -maunium.net/go/mautrix-whatsapp v0.1.4 h1:qlkb3eXcKm1QE6AjrAl9aKxokHlwj7BNr+aUQFXFmWE= -maunium.net/go/mautrix-whatsapp v0.1.4/go.mod h1:yC5pjdUQckJzuMX5rrg2237kz/7zP+7qO1uckOODe5M= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= From 5c9d806f9127c6e138bc29f2ab073b4d6b7e4209 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 8 Jul 2021 17:39:48 +0800 Subject: [PATCH 103/109] fix can't delete message in the dm from matrix side --- portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal.go b/portal.go index 53d2b9c..7c7004c 100644 --- a/portal.go +++ b/portal.go @@ -288,7 +288,7 @@ func (portal *Portal) markHandledSkype(source *User, message *skype.Resource, mx if message.GetFromMe(source.Conn.Conn) { msg.Sender = source.JID } else if portal.IsPrivateChat() { - msg.Sender = portal.Key.JID + msg.Sender = source.JID } else { msg.Sender = source.JID //if len(msg.Sender) == 0 { From cb3d11d072d4e850d3707e185cb661b336a67867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaron=20Vi=C3=ABtor?= Date: Sun, 8 Aug 2021 17:19:20 +0200 Subject: [PATCH 104/109] Updated default templates to no longer use "&" The "&" token is not allowed in user IDs. See: https://matrix.org/docs/spec/appendices#user-identifiers --- example-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index 4069c05..8688df9 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -55,7 +55,7 @@ appservice: bridge: # Localpart template of MXIDs for Skype users. # {{.}} is replaced with the phone number of the Skype user. - username_template: skype&{{.}} + username_template: skype-{{.}} # Displayname template for Skype users. # {{.Notify}} - nickname set by the Skype user # {{.Jid}} - phone number (international format) @@ -71,7 +71,7 @@ bridge: # (Note that, by default, non-admins might not have your homeserver's permission to create # communities.) # {{.Localpart}} is the MXID localpart and {{.Server}} is the MXID server part of the user. - community_template: skype&{{.Localpart}}={{.Server}} + community_template: skype-{{.Localpart}}={{.Server}} # Skype connection timeout in seconds. connection_timeout: 20 @@ -176,7 +176,7 @@ bridge: # 8 characters key: '12dsf323' # Use the username_template prefix. (Warning: At present, username_template cannot be too complicated, otherwise this function may cause unknown errors) - username_template_prefix: 'skype&' + username_template_prefix: 'skype-' # Permissions for using the bridge. # Permitted values: From 83a5b0af2c0d0e2fcc1b5f1a9b210f266ed17bfa Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 13 Aug 2021 23:19:45 +0800 Subject: [PATCH 105/109] fix login with old skype account --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 43e87ab..97fa4af 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/chai2010/webp v1.1.0 github.com/gabriel-vasile/mimetype v1.1.2 github.com/gorilla/websocket v1.4.2 - github.com/kelaresg/go-skypeapi v0.1.2-0.20210603114428-9c103d451110 + github.com/kelaresg/go-skypeapi v0.1.2-0.20210813144457-5bc29092a74e github.com/lib/pq v1.9.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 57f742e..91676c5 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210603114428-9c103d451110 h1:QD7o9DfiLKVTP3jzYLFa/3UY8XDEH+l+QEFLFPHeyVg= -github.com/kelaresg/go-skypeapi v0.1.2-0.20210603114428-9c103d451110/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210813144457-5bc29092a74e h1:/C3k1dx0v2nVQ/AYvKvPJEJVba4K6rAJ+CRG38yhO0c= +github.com/kelaresg/go-skypeapi v0.1.2-0.20210813144457-5bc29092a74e/go.mod h1:2Mxwa2Flo+PKlVYEM4QBNgs408C/mUSBXRzAb6+B+Zg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= From 98791fb757f19fda21c903c3cdf3b1e1bffc4cfe Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 17 Sep 2021 18:14:34 +0800 Subject: [PATCH 106/109] auto update custom user access token --- custompuppet.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custompuppet.go b/custompuppet.go index 58cab5c..30143e9 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -4,6 +4,7 @@ import ( "crypto/hmac" "crypto/sha512" "encoding/hex" + "strings" "time" "github.com/pkg/errors" @@ -100,8 +101,13 @@ func (puppet *Puppet) StartCustomMXID() error { } resp, err := intent.Whoami() if err != nil { - puppet.clearCustomMXID() - return err + if strings.Index(err.Error(), "M_UNKNOWN_TOKEN (HTTP 401)") > -1 { + err, _ = puppet.customUser.UpdateAccessToken(puppet) + } + if err != nil { + puppet.clearCustomMXID() + return err + } } if resp.UserID != puppet.CustomMXID { puppet.clearCustomMXID() From bc68761f8e607f0cbce73955ead89c903d753753 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Fri, 17 Sep 2021 19:01:21 +0800 Subject: [PATCH 107/109] fix nil address bug --- custompuppet.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custompuppet.go b/custompuppet.go index 30143e9..95379ec 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -102,7 +102,10 @@ func (puppet *Puppet) StartCustomMXID() error { resp, err := intent.Whoami() if err != nil { if strings.Index(err.Error(), "M_UNKNOWN_TOKEN (HTTP 401)") > -1 { - err, _ = puppet.customUser.UpdateAccessToken(puppet) + puppet.log.Debugln("StartCustomMXID UpdateAccessToken: ", puppet.MXID) + if puppet.customUser != nil { + err, _ = puppet.customUser.UpdateAccessToken(puppet) + } } if err != nil { puppet.clearCustomMXID() From 0a41ec3e8f399e5c198ad046da601fe662eeb063 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Tue, 19 Oct 2021 19:31:24 +0800 Subject: [PATCH 108/109] Fix compatibility bug under postgres --- commands.go | 1 + .../2021-10-19-update-user-fields-varying.go | 20 +++++++++++++++++++ database/upgrades/upgrades.go | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 database/upgrades/2021-10-19-update-user-fields-varying.go diff --git a/commands.go b/commands.go index b3861d9..5ab2e97 100644 --- a/commands.go +++ b/commands.go @@ -250,6 +250,7 @@ const cmdLogoutHelp = `logout - Logout from Skype` func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { if ce.User.Conn == nil { + ce.Reply("You're not logged into Skype.") return } //_ = ce.User.Conn.GetConversations("", ce.User.bridge.Config.Bridge.InitialChatSync) diff --git a/database/upgrades/2021-10-19-update-user-fields-varying.go b/database/upgrades/2021-10-19-update-user-fields-varying.go new file mode 100644 index 0000000..3b9de18 --- /dev/null +++ b/database/upgrades/2021-10-19-update-user-fields-varying.go @@ -0,0 +1,20 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[19] = upgrade{"Update user fields varying.", func(tx *sql.Tx, c context) error { + if c.dialect == Postgres { + _, err := tx.Exec(`ALTER TABLE "user" ALTER COLUMN skype_token TYPE varchar(1500), + ALTER COLUMN registration_token TYPE varchar(1500), + ALTER COLUMN registration_token_str TYPE varchar(1500)`) + if err != nil { + return err + } + } + + return nil + }} +} diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go index a870745..e224593 100644 --- a/database/upgrades/upgrades.go +++ b/database/upgrades/upgrades.go @@ -39,7 +39,7 @@ type upgrade struct { fn upgradeFunc } -const NumberOfUpgrades = 19 +const NumberOfUpgrades = 20 var upgrades [NumberOfUpgrades]upgrade From 9b6801398b16de79cf21e9fb1e23018aac5e4ccf Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Wed, 27 Oct 2021 17:18:35 +0800 Subject: [PATCH 109/109] Update Dockerfile --- Dockerfile | 10 ++++------ README.md | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 19a1a06..23985d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,17 @@ -FROM golang:1-alpine AS builder +FROM golang:1-alpine3.14 AS builder -RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories -RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev@edge_community +RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev COPY . /build WORKDIR /build RUN go build -o /usr/bin/matrix-skype -FROM alpine:latest +FROM alpine:3.14 ENV UID=1337 \ GID=1337 -RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories -RUN apk add --no-cache su-exec ca-certificates olm@edge_community +RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl COPY --from=builder /usr/bin/matrix-skype /usr/bin/matrix-skype COPY --from=builder /build/example-config.yaml /opt/matrix-skype/example-config.yaml diff --git a/README.md b/README.md index 5c48bcb..c5f58aa 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ matrix-skype is a library for bridging matrix and skype, about matrix, please re The skype api lib of matrix-skype is [go-skypeapi](https://github.com/kelaresg/go-skypeapi). [matrix room(#goskypebridge:matrix.org)](https://app.element.io/#/room/#goskypebridge:matrix.org) -This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp(matrix-skype currently does not support docker installation) +This matrix-skype bridge is based on [mautrix-whatsapp](https://github.com/tulir/mautrix-whatsapp),so the installation and usage methods are very similar to mautrix-whatsapp > # mautrix-whatsapp > A Matrix-WhatsApp puppeting bridge based on the [Rhymen/go-whatsapp](https://github.com/Rhymen/go-whatsapp)