Add all repos
This commit is contained in:
parent
faa12c60bc
commit
8a91c9b89b
369 changed files with 29047 additions and 28 deletions
12
zammad-addon-metamigo/.gitignore
vendored
Normal file
12
zammad-addon-metamigo/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.yalc
|
||||
yalc.lock
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
.idea/
|
||||
dist/
|
||||
.env*
|
||||
yarn-error.log
|
||||
docker-compose.yml
|
||||
coverage
|
||||
.pgpass
|
||||
.npmrc
|
||||
25
zammad-addon-metamigo/.gitlab-ci.yml
Normal file
25
zammad-addon-metamigo/.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
image: registry.gitlab.com/guardianproject-ops/docker-python-node:python3.8-nodejs12
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- trigger
|
||||
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y make ruby
|
||||
- gem install rufo -v 0.12.0
|
||||
- pip install inflection
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- echo "Skipping tests" # make test
|
||||
|
||||
build:
|
||||
stage: build
|
||||
artifacts:
|
||||
paths:
|
||||
- dist/*.szpm
|
||||
script:
|
||||
- make
|
||||
1
zammad-addon-metamigo/.ruby-version
Normal file
1
zammad-addon-metamigo/.ruby-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
2.6.6
|
||||
616
zammad-addon-metamigo/LICENSE.md
Normal file
616
zammad-addon-metamigo/LICENSE.md
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
### GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
### Preamble
|
||||
|
||||
The GNU 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
|
||||
20
zammad-addon-metamigo/Makefile
Normal file
20
zammad-addon-metamigo/Makefile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.PHONY: build prep clean fmt new-migration
|
||||
|
||||
build: prep
|
||||
@./package.py
|
||||
@find dist/ -iname "*szpm"
|
||||
|
||||
prep:
|
||||
@mkdir -p dist
|
||||
|
||||
clean: prep
|
||||
@rm -rf dist/*
|
||||
|
||||
fmt:
|
||||
rufo --simple-exit src
|
||||
|
||||
new-migration:
|
||||
@./new-migration.py
|
||||
|
||||
test:
|
||||
@echo There are no tests
|
||||
70
zammad-addon-metamigo/README.md
Normal file
70
zammad-addon-metamigo/README.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# zammad-addon-metamigo
|
||||
|
||||
An addon that adds [metamigo](https://gitlab.com/digiresilience/link/metamigo) channels to Zammad.
|
||||
|
||||
## Channels
|
||||
|
||||
This channel creates a three channels: "Voice", "Signal" and "Whatsapp".
|
||||
|
||||
To submit a ticket: make a POST to the Submission Endpoint with the header
|
||||
`Authorization: SUBMISSION_TOKEN`.
|
||||
|
||||
The payload for the Voice channel must be a json object with the keys:
|
||||
|
||||
- `startTime` - string containing ISO date
|
||||
- `endTime` - string containing ISO date
|
||||
- `to` - fully qualified phone number
|
||||
- `from` - fully qualified phone number
|
||||
- `duration` - string containing the recording duration
|
||||
- `callSid` - the unique identifier for the call
|
||||
- `recording` - string base64 encoded binary of the recording
|
||||
- `mimeType` - string of the binary mime-type
|
||||
|
||||
The payload for the Signal channel must be a json object with the keys:
|
||||
|
||||
- TBD
|
||||
|
||||
The payload for the Whatsapp channel must be a json object with the keys:
|
||||
|
||||
- TBD
|
||||
|
||||
## Development
|
||||
|
||||
1. Edit the files in `src/`
|
||||
|
||||
Migration files should go in `src/db/addon/CHANNEL_NAME` ([see this post](https://community.zammad.org/t/automating-creation-of-custom-object-attributes/3831/2?u=abelxluck))
|
||||
|
||||
2. Update version and changelog in `metamigo-skeleton.szpm`
|
||||
3. Build a new package `make`
|
||||
|
||||
This outputs `dist/metamigo-vXXX.szpm`
|
||||
|
||||
4. Install the szpm using the zammad package manager.
|
||||
|
||||
5. Repeat
|
||||
|
||||
### Create a new migration
|
||||
|
||||
Included is a helper script to create new migrations. You must have the python
|
||||
`inflection` library installed.
|
||||
|
||||
- debian/ubuntu: `apt install python3-inflection`
|
||||
- pip: `pip install --user inflection`
|
||||
- or create your own venv
|
||||
|
||||
To make a new migration simply run:
|
||||
|
||||
```
|
||||
make new-migration
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[](https://gitlab.com/digiresilience/link/zamamd-addon-metamigo/blob/master/LICENSE.md)
|
||||
|
||||
This is a free software project licensed under the GNU Affero General
|
||||
Public License v3.0 (GNU AGPLv3) by [The Center for Digital
|
||||
Resilience](https://digiresilience.org) and [Guardian
|
||||
Project](https://guardianproject.info).
|
||||
|
||||
🐻
|
||||
24
zammad-addon-metamigo/metamigo.szpm.template
Normal file
24
zammad-addon-metamigo/metamigo.szpm.template
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "Metamigo",
|
||||
"version": "0.0.6",
|
||||
"vendor": "Center for Digital Resilience",
|
||||
"license": "AGPL-v3+",
|
||||
"url": "https://digiresilience.org",
|
||||
"buildhost": "",
|
||||
"builddate": "",
|
||||
"change_log": [
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"date": "",
|
||||
"log": "Initial version."
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language": "en",
|
||||
"text": "Tickets created from voicemails and messages"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
]
|
||||
}
|
||||
46
zammad-addon-metamigo/new-migration.py
Executable file
46
zammad-addon-metamigo/new-migration.py
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
import pathlib
|
||||
import os
|
||||
import json
|
||||
import glob
|
||||
import inflection
|
||||
from datetime import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
migration_template = """class {} < ActiveRecord::Migration[5.2]
|
||||
def self.up
|
||||
# add your code here
|
||||
end
|
||||
|
||||
def self.down
|
||||
# add your code here
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
|
||||
def load_skeleton():
|
||||
t = glob.glob('*.szpm.template')
|
||||
if len(t) != 1:
|
||||
raise Exception("Cannot find szpm template")
|
||||
with open(t[0], 'r', encoding='utf-8') as f:
|
||||
skeleton = json.load(f, object_pairs_hook=OrderedDict)
|
||||
return skeleton
|
||||
|
||||
|
||||
def main():
|
||||
skeleton = load_skeleton()
|
||||
name = skeleton["name"].lower()
|
||||
raw_name = input("Enter migration name (no spaces, no symbols!): ")
|
||||
migration_base_name = "{}_{}".format(name, inflection.underscore(raw_name))
|
||||
migration_name = inflection.camelize(migration_base_name, uppercase_first_letter=True)
|
||||
contents = migration_template.format(migration_name)
|
||||
time = datetime.utcnow().strftime("%Y%m%d%H%M%S")
|
||||
migration_file_name = "{}_{}.rb".format(time, migration_base_name)
|
||||
dir_path = os.path.join("src/db/addon", name)
|
||||
pathlib.Path(dir_path).mkdir(parents=True, exist_ok=True)
|
||||
with open(os.path.join(dir_path, migration_file_name), 'w') as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
main()
|
||||
75
zammad-addon-metamigo/package.py
Executable file
75
zammad-addon-metamigo/package.py
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import base64
|
||||
import json
|
||||
import datetime
|
||||
import platform
|
||||
import glob
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
# files matching this pattern are not included in the package
|
||||
ignored_patterns = [
|
||||
"\.gitkeep"
|
||||
]
|
||||
|
||||
|
||||
def encode(fname):
|
||||
data = open(fname, "rb").read()
|
||||
return base64.b64encode(data).decode('utf-8')
|
||||
|
||||
|
||||
def read_perm(fname):
|
||||
return int(oct(os.stat(fname).st_mode & 0o777)[-3:])
|
||||
|
||||
|
||||
def format_file(content, pkg_path, permission):
|
||||
return OrderedDict(
|
||||
location=pkg_path,
|
||||
permission=permission,
|
||||
encode="base64",
|
||||
content=content)
|
||||
|
||||
|
||||
def pkg_file(actual_path):
|
||||
print(" Packaging: {}".format(actual_path))
|
||||
pkg_path = actual_path[6:]
|
||||
contents = encode(actual_path)
|
||||
res = format_file(contents, pkg_path, read_perm(actual_path))
|
||||
return res
|
||||
|
||||
|
||||
def pkg_files():
|
||||
pkged_files = []
|
||||
for root, dirs, files in os.walk("./src/"):
|
||||
for f in files:
|
||||
if any(re.search(r, f) for r in ignored_patterns):
|
||||
continue
|
||||
actual_path = os.path.join(root, f)
|
||||
pkged_files.append(pkg_file(actual_path))
|
||||
return pkged_files
|
||||
|
||||
|
||||
def load_skeleton():
|
||||
t = glob.glob('*.szpm.template')
|
||||
if len(t) != 1:
|
||||
raise Exception("Cannot find szpm template")
|
||||
with open(t[0], 'r', encoding='utf-8') as f:
|
||||
skeleton = json.load(f, object_pairs_hook=OrderedDict)
|
||||
return skeleton
|
||||
|
||||
|
||||
def main():
|
||||
files = pkg_files()
|
||||
skeleton = load_skeleton()
|
||||
skeleton["files"] = files
|
||||
skeleton["builddate"] = datetime.datetime.utcnow().isoformat()
|
||||
skeleton["buildhost"] = platform.node()
|
||||
name = skeleton["name"].lower()
|
||||
version = skeleton["version"]
|
||||
pkg = json.dumps(skeleton, indent=2)
|
||||
with open("dist/{}-v{}.szpm".format(name, version), "w", encoding='utf-8') as f:
|
||||
f.write(pkg)
|
||||
|
||||
|
||||
main()
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
class Index extends App.ControllerSubContent
|
||||
requiredPermission: 'admin.channel_cdr_signal'
|
||||
events:
|
||||
'click .js-new': 'new'
|
||||
'click .js-edit': 'edit'
|
||||
'click .js-delete': 'delete'
|
||||
'click .js-disable': 'disable'
|
||||
'click .js-enable': 'enable'
|
||||
'click .js-rotate-token': 'rotateToken'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
#@interval(@load, 60000)
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
@startLoading()
|
||||
@ajax(
|
||||
id: 'cdr_signal_index'
|
||||
type: 'GET'
|
||||
url: "#{@apiPath}/channels_cdr_signal"
|
||||
processData: true
|
||||
success: (data) =>
|
||||
@stopLoading()
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@render(data)
|
||||
)
|
||||
|
||||
render: (data) =>
|
||||
|
||||
channels = []
|
||||
for channel_id in data.channel_ids
|
||||
channel = App.Channel.find(channel_id)
|
||||
if channel && channel.options
|
||||
displayName = '-'
|
||||
if channel.group_id
|
||||
group = App.Group.find(channel.group_id)
|
||||
displayName = group.displayName()
|
||||
channel.options.groupName = displayName
|
||||
channels.push channel
|
||||
@html App.view('cdr_signal/index')(
|
||||
channels: channels
|
||||
)
|
||||
|
||||
new: (e) =>
|
||||
e.preventDefault()
|
||||
new FormAdd(
|
||||
container: @el.parents('.content')
|
||||
load: @load
|
||||
)
|
||||
|
||||
edit: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
channel = App.Channel.find(id)
|
||||
new FormEdit(
|
||||
channel: channel
|
||||
container: @el.parents('.content')
|
||||
load: @load
|
||||
)
|
||||
|
||||
delete: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
new App.ControllerConfirm(
|
||||
message: 'Sure?'
|
||||
callback: =>
|
||||
@ajax(
|
||||
id: 'cdr_signal_delete'
|
||||
type: 'DELETE'
|
||||
url: "#{@apiPath}/channels_cdr_signal"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
|
||||
rotateToken: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
|
||||
new App.ControllerConfirm(
|
||||
message: 'This will break the submission form!'
|
||||
buttonSubmit: 'Reset token'
|
||||
head: 'Reset the submission token?'
|
||||
callback: =>
|
||||
@ajax(
|
||||
id: 'cdr_signal_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_signal_rotate_token"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
disable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'cdr_signal_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_signal_disable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
enable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'cdr_signal_enable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_signal_enable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
class FormAdd extends App.ControllerModal
|
||||
head: 'Add Web Form'
|
||||
shown: true
|
||||
button: 'Add'
|
||||
buttonCancel: true
|
||||
small: true
|
||||
|
||||
content: ->
|
||||
content = $(App.view('cdr_signal/form_add')())
|
||||
createOrgSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'organization_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Organization'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
createGroupSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'group_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Group'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
|
||||
content.find('.js-select').on('click', (e) =>
|
||||
@selectAll(e)
|
||||
)
|
||||
content.find('.js-messagesGroup').replaceWith createGroupSelection(1)
|
||||
content.find('.js-organization').replaceWith createOrgSelection(null)
|
||||
content
|
||||
|
||||
onClosed: =>
|
||||
return if !@isChanged
|
||||
@isChanged = false
|
||||
@load()
|
||||
|
||||
onSubmit: (e) =>
|
||||
@formDisable(e)
|
||||
@ajax(
|
||||
id: 'cdr_signal_app_verify'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_signal"
|
||||
data: JSON.stringify(@formParams())
|
||||
processData: true
|
||||
success: =>
|
||||
@isChanged = true
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(e)
|
||||
error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.')
|
||||
@el.find('.alert').removeClass('hidden').text(error_message)
|
||||
)
|
||||
|
||||
class FormEdit extends App.ControllerModal
|
||||
head: 'Web Form Info'
|
||||
shown: true
|
||||
buttonCancel: true
|
||||
|
||||
content: ->
|
||||
content = $(App.view('cdr_signal/form_edit')(channel: @channel))
|
||||
|
||||
createOrgSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'organization_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Organization'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
createGroupSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'group_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Group'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
|
||||
content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id)
|
||||
content.find('.js-organization').replaceWith createOrgSelection(@channel.options.organization_id)
|
||||
content
|
||||
|
||||
onClosed: =>
|
||||
return if !@isChanged
|
||||
@isChanged = false
|
||||
@load()
|
||||
|
||||
onSubmit: (e) =>
|
||||
@formDisable(e)
|
||||
params = @formParams()
|
||||
@channel.options = params
|
||||
@ajax(
|
||||
id: 'channel_cdr_signal_update'
|
||||
type: 'PUT'
|
||||
url: "#{@apiPath}/channels_cdr_signal/#{@channel.id}"
|
||||
data: JSON.stringify(@formParams())
|
||||
processData: true
|
||||
success: =>
|
||||
@isChanged = true
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(e)
|
||||
error_message = App.i18n.translateContent(data.error || 'Unable to save changes.')
|
||||
@el.find('.alert').removeClass('hidden').text(error_message)
|
||||
)
|
||||
|
||||
App.Config.set('cdr_signal', { prio: 5100, name: 'Signal', parent: '#channels', target: '#channels/cdr_signal', controller: Index, permission: ['admin.channel_cdr_signal'] }, 'NavBarAdmin')
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
class Index extends App.ControllerSubContent
|
||||
requiredPermission: 'admin.channel_cdr_voice'
|
||||
events:
|
||||
'click .js-new': 'new'
|
||||
'click .js-edit': 'edit'
|
||||
'click .js-delete': 'delete'
|
||||
'click .js-disable': 'disable'
|
||||
'click .js-enable': 'enable'
|
||||
'click .js-rotate-token': 'rotateToken'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
#@interval(@load, 60000)
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
@startLoading()
|
||||
@ajax(
|
||||
id: 'cdr_voice_index'
|
||||
type: 'GET'
|
||||
url: "#{@apiPath}/channels_cdr_voice"
|
||||
processData: true
|
||||
success: (data) =>
|
||||
@stopLoading()
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@render(data)
|
||||
)
|
||||
|
||||
render: (data) =>
|
||||
|
||||
channels = []
|
||||
for channel_id in data.channel_ids
|
||||
channel = App.Channel.find(channel_id)
|
||||
if channel && channel.options
|
||||
displayName = '-'
|
||||
if channel.group_id
|
||||
group = App.Group.find(channel.group_id)
|
||||
displayName = group.displayName()
|
||||
channel.options.groupName = displayName
|
||||
channels.push channel
|
||||
@html App.view('cdr_voice/index')(
|
||||
channels: channels
|
||||
)
|
||||
|
||||
new: (e) =>
|
||||
e.preventDefault()
|
||||
new FormAdd(
|
||||
container: @el.parents('.content')
|
||||
load: @load
|
||||
)
|
||||
|
||||
edit: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
channel = App.Channel.find(id)
|
||||
new FormEdit(
|
||||
channel: channel
|
||||
container: @el.parents('.content')
|
||||
load: @load
|
||||
)
|
||||
|
||||
delete: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
new App.ControllerConfirm(
|
||||
message: 'Sure?'
|
||||
callback: =>
|
||||
@ajax(
|
||||
id: 'cdr_voice_delete'
|
||||
type: 'DELETE'
|
||||
url: "#{@apiPath}/channels_cdr_voice"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
|
||||
rotateToken: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
|
||||
new App.ControllerConfirm(
|
||||
message: 'This will break the submission form!'
|
||||
buttonSubmit: 'Reset token'
|
||||
head: 'Reset the submission token?'
|
||||
callback: =>
|
||||
@ajax(
|
||||
id: 'cdr_voice_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_voice_rotate_token"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
disable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'cdr_voice_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_voice_disable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
enable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'cdr_voice_enable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_voice_enable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
class FormAdd extends App.ControllerModal
|
||||
head: 'Add Web Form'
|
||||
shown: true
|
||||
button: 'Add'
|
||||
buttonCancel: true
|
||||
small: true
|
||||
|
||||
content: ->
|
||||
content = $(App.view('cdr_voice/form_add')())
|
||||
createOrgSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'organization_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Organization'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
createGroupSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'group_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Group'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
|
||||
content.find('.js-select').on('click', (e) =>
|
||||
@selectAll(e)
|
||||
)
|
||||
content.find('.js-messagesGroup').replaceWith createGroupSelection(1)
|
||||
content.find('.js-organization').replaceWith createOrgSelection(null)
|
||||
content
|
||||
|
||||
onClosed: =>
|
||||
return if !@isChanged
|
||||
@isChanged = false
|
||||
@load()
|
||||
|
||||
onSubmit: (e) =>
|
||||
@formDisable(e)
|
||||
@ajax(
|
||||
id: 'cdr_voice_app_verify'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_voice"
|
||||
data: JSON.stringify(@formParams())
|
||||
processData: true
|
||||
success: =>
|
||||
@isChanged = true
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(e)
|
||||
error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.')
|
||||
@el.find('.alert').removeClass('hidden').text(error_message)
|
||||
)
|
||||
|
||||
class FormEdit extends App.ControllerModal
|
||||
head: 'Web Form Info'
|
||||
shown: true
|
||||
buttonCancel: true
|
||||
|
||||
content: ->
|
||||
content = $(App.view('cdr_voice/form_edit')(channel: @channel))
|
||||
|
||||
createOrgSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'organization_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Organization'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
createGroupSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'group_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Group'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
|
||||
content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id)
|
||||
content.find('.js-organization').replaceWith createOrgSelection(@channel.options.organization_id)
|
||||
content
|
||||
|
||||
onClosed: =>
|
||||
return if !@isChanged
|
||||
@isChanged = false
|
||||
@load()
|
||||
|
||||
onSubmit: (e) =>
|
||||
@formDisable(e)
|
||||
params = @formParams()
|
||||
@channel.options = params
|
||||
@ajax(
|
||||
id: 'channel_cdr_voice_update'
|
||||
type: 'PUT'
|
||||
url: "#{@apiPath}/channels_cdr_voice/#{@channel.id}"
|
||||
data: JSON.stringify(@formParams())
|
||||
processData: true
|
||||
success: =>
|
||||
@isChanged = true
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(e)
|
||||
error_message = App.i18n.translateContent(data.error || 'Unable to save changes.')
|
||||
@el.find('.alert').removeClass('hidden').text(error_message)
|
||||
)
|
||||
|
||||
App.Config.set('cdr_voice', { prio: 5100, name: 'Voice', parent: '#channels', target: '#channels/cdr_voice', controller: Index, permission: ['admin.channel_cdr_voice'] }, 'NavBarAdmin')
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
class Index extends App.ControllerSubContent
|
||||
requiredPermission: 'admin.channel_cdr_whatsapp'
|
||||
events:
|
||||
'click .js-new': 'new'
|
||||
'click .js-edit': 'edit'
|
||||
'click .js-delete': 'delete'
|
||||
'click .js-disable': 'disable'
|
||||
'click .js-enable': 'enable'
|
||||
'click .js-rotate-token': 'rotateToken'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
#@interval(@load, 60000)
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
@startLoading()
|
||||
@ajax(
|
||||
id: 'cdr_whatsapp_index'
|
||||
type: 'GET'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp"
|
||||
processData: true
|
||||
success: (data) =>
|
||||
@stopLoading()
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@render(data)
|
||||
)
|
||||
|
||||
render: (data) =>
|
||||
|
||||
channels = []
|
||||
for channel_id in data.channel_ids
|
||||
channel = App.Channel.find(channel_id)
|
||||
if channel && channel.options
|
||||
displayName = '-'
|
||||
if channel.group_id
|
||||
group = App.Group.find(channel.group_id)
|
||||
displayName = group.displayName()
|
||||
channel.options.groupName = displayName
|
||||
channels.push channel
|
||||
@html App.view('cdr_whatsapp/index')(
|
||||
channels: channels
|
||||
)
|
||||
|
||||
new: (e) =>
|
||||
e.preventDefault()
|
||||
new FormAdd(
|
||||
container: @el.parents('.content')
|
||||
load: @load
|
||||
)
|
||||
|
||||
edit: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
channel = App.Channel.find(id)
|
||||
new FormEdit(
|
||||
channel: channel
|
||||
container: @el.parents('.content')
|
||||
load: @load
|
||||
)
|
||||
|
||||
delete: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
new App.ControllerConfirm(
|
||||
message: 'Sure?'
|
||||
callback: =>
|
||||
@ajax(
|
||||
id: 'cdr_whatsapp_delete'
|
||||
type: 'DELETE'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
|
||||
rotateToken: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
|
||||
new App.ControllerConfirm(
|
||||
message: 'This will break the submission form!'
|
||||
buttonSubmit: 'Reset token'
|
||||
head: 'Reset the submission token?'
|
||||
callback: =>
|
||||
@ajax(
|
||||
id: 'cdr_whatsapp_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp_rotate_token"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
disable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'cdr_whatsapp_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp_disable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
enable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'cdr_whatsapp_enable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp_enable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
class FormAdd extends App.ControllerModal
|
||||
head: 'Add Web Form'
|
||||
shown: true
|
||||
button: 'Add'
|
||||
buttonCancel: true
|
||||
small: true
|
||||
|
||||
content: ->
|
||||
content = $(App.view('cdr_whatsapp/form_add')())
|
||||
createOrgSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'organization_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Organization'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
createGroupSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'group_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Group'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
|
||||
content.find('.js-select').on('click', (e) =>
|
||||
@selectAll(e)
|
||||
)
|
||||
content.find('.js-messagesGroup').replaceWith createGroupSelection(1)
|
||||
content.find('.js-organization').replaceWith createOrgSelection(null)
|
||||
content
|
||||
|
||||
onClosed: =>
|
||||
return if !@isChanged
|
||||
@isChanged = false
|
||||
@load()
|
||||
|
||||
onSubmit: (e) =>
|
||||
@formDisable(e)
|
||||
@ajax(
|
||||
id: 'cdr_whatsapp_app_verify'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp"
|
||||
data: JSON.stringify(@formParams())
|
||||
processData: true
|
||||
success: =>
|
||||
@isChanged = true
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(e)
|
||||
error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.')
|
||||
@el.find('.alert').removeClass('hidden').text(error_message)
|
||||
)
|
||||
|
||||
class FormEdit extends App.ControllerModal
|
||||
head: 'Web Form Info'
|
||||
shown: true
|
||||
buttonCancel: true
|
||||
|
||||
content: ->
|
||||
content = $(App.view('cdr_whatsapp/form_edit')(channel: @channel))
|
||||
|
||||
createOrgSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'organization_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Organization'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
createGroupSelection = (selected_id) ->
|
||||
return App.UiElement.select.render(
|
||||
name: 'group_id'
|
||||
multiple: false
|
||||
limit: 100
|
||||
null: false
|
||||
relation: 'Group'
|
||||
nulloption: true
|
||||
value: selected_id
|
||||
class: 'form-control--small'
|
||||
)
|
||||
|
||||
content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id)
|
||||
content.find('.js-organization').replaceWith createOrgSelection(@channel.options.organization_id)
|
||||
content
|
||||
|
||||
onClosed: =>
|
||||
return if !@isChanged
|
||||
@isChanged = false
|
||||
@load()
|
||||
|
||||
onSubmit: (e) =>
|
||||
@formDisable(e)
|
||||
params = @formParams()
|
||||
@channel.options = params
|
||||
@ajax(
|
||||
id: 'channel_cdr_whatsapp_update'
|
||||
type: 'PUT'
|
||||
url: "#{@apiPath}/channels_cdr_whatsapp/#{@channel.id}"
|
||||
data: JSON.stringify(@formParams())
|
||||
processData: true
|
||||
success: =>
|
||||
@isChanged = true
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(e)
|
||||
error_message = App.i18n.translateContent(data.error || 'Unable to save changes.')
|
||||
@el.find('.alert').removeClass('hidden').text(error_message)
|
||||
)
|
||||
|
||||
App.Config.set('cdr_whatsapp', { prio: 5100, name: 'Whatsapp', parent: '#channels', target: '#channels/cdr_whatsapp', controller: Index, permission: ['admin.channel_cdr_whatsapp'] }, 'NavBarAdmin')
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
class CdrSignalReply
|
||||
@action: (actions, ticket, article, ui) ->
|
||||
return actions if ui.permissionCheck('ticket.customer')
|
||||
|
||||
if article.sender.name is 'Customer' && article.type.name is 'cdr_signal'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'cdrSignalMessageReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
|
||||
actions
|
||||
|
||||
@perform: (articleContainer, type, ticket, article, ui) ->
|
||||
return true if type isnt 'cdrSignalMessageReply'
|
||||
|
||||
ui.scrollToCompose()
|
||||
|
||||
# get reference article
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
|
||||
articleNew = {
|
||||
to: ''
|
||||
cc: ''
|
||||
body: ''
|
||||
in_reply_to: ''
|
||||
}
|
||||
|
||||
if article.message_id
|
||||
articleNew.in_reply_to = article.message_id
|
||||
|
||||
# get current body
|
||||
articleNew.body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', {
|
||||
ticket: ticket
|
||||
type: type
|
||||
article: articleNew
|
||||
position: 'end'
|
||||
})
|
||||
|
||||
true
|
||||
|
||||
@articleTypes: (articleTypes, ticket, ui) ->
|
||||
return articleTypes if !ui.permissionCheck('ticket.agent')
|
||||
|
||||
return articleTypes if !ticket || !ticket.create_article_type_id
|
||||
|
||||
articleTypeCreate = App.TicketArticleType.find(ticket.create_article_type_id).name
|
||||
|
||||
return articleTypes if articleTypeCreate isnt 'cdr_signal'
|
||||
articleTypes.push {
|
||||
name: 'cdr_signal'
|
||||
icon: 'cdr-signal'
|
||||
attributes: []
|
||||
internal: false,
|
||||
features: ['attachment']
|
||||
maxTextLength: 10000
|
||||
warningTextLength: 5000
|
||||
}
|
||||
articleTypes
|
||||
|
||||
@setArticleTypePost: (type, ticket, ui) ->
|
||||
return if type isnt 'cdr_signal'
|
||||
rawHTML = ui.$('[data-name=body]').html()
|
||||
cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML)
|
||||
if cleanHTML && cleanHTML.html() != rawHTML
|
||||
ui.$('[data-name=body]').html(cleanHTML)
|
||||
|
||||
@params: (type, params, ui) ->
|
||||
if type is 'cdr_signal'
|
||||
App.Utils.htmlRemoveRichtext(ui.$('[data-name=body]'), false)
|
||||
params.content_type = 'text/plain'
|
||||
params.body = App.Utils.html2text(params.body, true)
|
||||
|
||||
params
|
||||
|
||||
App.Config.set('300-CdrSignalReply', CdrSignalReply, 'TicketZoomArticleAction')
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
class CdrWhatsappReply
|
||||
@action: (actions, ticket, article, ui) ->
|
||||
return actions if ui.permissionCheck('ticket.customer')
|
||||
|
||||
if article.sender.name is 'Customer' && article.type.name is 'cdr_whatsapp'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'cdrWhatsappMessageReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
|
||||
actions
|
||||
|
||||
@perform: (articleContainer, type, ticket, article, ui) ->
|
||||
return true if type isnt 'cdrWhatsappMessageReply'
|
||||
|
||||
ui.scrollToCompose()
|
||||
|
||||
# get reference article
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
|
||||
articleNew = {
|
||||
to: ''
|
||||
cc: ''
|
||||
body: ''
|
||||
in_reply_to: ''
|
||||
}
|
||||
|
||||
if article.message_id
|
||||
articleNew.in_reply_to = article.message_id
|
||||
|
||||
# get current body
|
||||
articleNew.body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', {
|
||||
ticket: ticket
|
||||
type: type
|
||||
article: articleNew
|
||||
position: 'end'
|
||||
})
|
||||
|
||||
true
|
||||
|
||||
@articleTypes: (articleTypes, ticket, ui) ->
|
||||
return articleTypes if !ui.permissionCheck('ticket.agent')
|
||||
|
||||
return articleTypes if !ticket || !ticket.create_article_type_id
|
||||
|
||||
articleTypeCreate = App.TicketArticleType.find(ticket.create_article_type_id).name
|
||||
|
||||
return articleTypes if articleTypeCreate isnt 'cdr_whatsapp'
|
||||
articleTypes.push {
|
||||
name: 'cdr_whatsapp'
|
||||
icon: 'cdr-whatsapp'
|
||||
attributes: []
|
||||
internal: false,
|
||||
features: ['attachment']
|
||||
maxTextLength: 10000
|
||||
warningTextLength: 5000
|
||||
}
|
||||
articleTypes
|
||||
|
||||
@setArticleTypePost: (type, ticket, ui) ->
|
||||
return if type isnt 'cdr_whatsapp'
|
||||
rawHTML = ui.$('[data-name=body]').html()
|
||||
cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML)
|
||||
if cleanHTML && cleanHTML.html() != rawHTML
|
||||
ui.$('[data-name=body]').html(cleanHTML)
|
||||
|
||||
@params: (type, params, ui) ->
|
||||
if type is 'cdr_whatsapp'
|
||||
App.Utils.htmlRemoveRichtext(ui.$('[data-name=body]'), false)
|
||||
params.content_type = 'text/plain'
|
||||
params.body = App.Utils.html2text(params.body, true)
|
||||
|
||||
params
|
||||
|
||||
App.Config.set('300-CdrWhatsappReply', CdrWhatsappReply, 'TicketZoomArticleAction')
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<div class="alert alert--danger hidden" role="alert"></div>
|
||||
<fieldset>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Phone number') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="phone_number" type="text" name="phone_number" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_token" type="text" name="bot_token" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_endpoint" type="text" name="bot_endpoint" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the group in which form submissions will get added to.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="js-messagesGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the organization to which submitters will be added to when they submit via this form.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="profile-organization js-organization"></div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="alert alert--danger hidden" role="alert"></div>
|
||||
|
||||
<fieldset>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Voice Line Number') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="phone_number" type="text" name="phone_number" value="<%= @channel.options.phone_number %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_token" type="text" name="bot_token" value="<%= @channel.options.bot_token %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_endpoint" type="text" name="bot_endpoint" value="<%= @channel.options.bot_endpoint %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the group in which incoming messages will be added.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="js-messagesGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the organization to which users will be added to when they send a message to this number.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="profile-organization js-organization"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="token"><%- @T('Endpoint URL') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="token" type="text" value="<%= "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/channels_cdr_signal_webhook/#{@channel.options.token}" %>" class="form-control input js-select" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<div class="page-header">
|
||||
<div class="page-header-title">
|
||||
<h1><%- @T('Signal') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="page-header-meta">
|
||||
<a class="btn btn--success js-new"><%- @T('Add Signal bot') %></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
<% if _.isEmpty(@channels): %>
|
||||
<div class="page-description">
|
||||
<p><%- @T('You have no configured %s right now.', 'Signal numbers') %></p>
|
||||
</div>
|
||||
<% else: %>
|
||||
|
||||
<% for channel in @channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
|
||||
<div class="action-block action-row">
|
||||
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.phone_number %></h2>
|
||||
</div>
|
||||
<div class="action-flow action-flow--row">
|
||||
<div class="action-block">
|
||||
<h3><%- @T('Group') %></h3>
|
||||
<% if channel.options: %>
|
||||
<%= channel.options.groupName %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="action-block">
|
||||
<h3><%- @T('Endpoint URL') %></h3>
|
||||
<%- @T('Click the edit button to view the endpoint details ') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-controls">
|
||||
<div class="btn btn--danger btn--secondary js-delete"><%- @T('Delete') %></div>
|
||||
<div class="btn btn--danger btn--secondary js-rotate-token"><%- @T('Reset Token') %></div>
|
||||
<% if channel.active is true: %>
|
||||
<div class="btn btn--secondary js-disable"><%- @T('Disable') %></div>
|
||||
<% else: %>
|
||||
<div class="btn btn--secondary js-enable"><%- @T('Enable') %></div>
|
||||
<% end %>
|
||||
<div class="btn js-edit"><%- @T('Edit') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<div class="alert alert--danger hidden" role="alert"></div>
|
||||
<fieldset>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Phone number') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="phone_number" type="text" name="phone_number" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the group in which form submissions will get added to.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="js-messagesGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the organization to which submitters will be added to when they submit via this form.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="profile-organization js-organization"></div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<div class="alert alert--danger hidden" role="alert"></div>
|
||||
|
||||
<fieldset>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Voice Line Number') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="phone_number" type="text" name="phone_number" value="<%= @channel.options.phone_number %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the group in which incoming calls will be added to.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="js-messagesGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the organization to which users will be added to when they leave a recording to this voice line.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="profile-organization js-organization"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="token"><%- @T('Endpoint URL') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="token" type="text" value="<%= "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/channels_cdr_voice_webhook/#{@channel.options.token}" %>" class="form-control input js-select" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<div class="page-header">
|
||||
<div class="page-header-title">
|
||||
<h1><%- @T('Voice') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="page-header-meta">
|
||||
<a class="btn btn--success js-new"><%- @T('Add Voice Line') %></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
<% if _.isEmpty(@channels): %>
|
||||
<div class="page-description">
|
||||
<p><%- @T('You have no configured %s right now.', 'voice lines') %></p>
|
||||
</div>
|
||||
<% else: %>
|
||||
|
||||
<% for channel in @channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
|
||||
<div class="action-block action-row">
|
||||
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.phone_number %></h2>
|
||||
</div>
|
||||
<div class="action-flow action-flow--row">
|
||||
<div class="action-block">
|
||||
<h3><%- @T('Group') %></h3>
|
||||
<% if channel.options: %>
|
||||
<%= channel.options.groupName %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="action-block">
|
||||
<h3><%- @T('Endpoint URL') %></h3>
|
||||
<%- @T('Click the edit button to view the endpoint details ') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-controls">
|
||||
<div class="btn btn--danger btn--secondary js-delete"><%- @T('Delete') %></div>
|
||||
<div class="btn btn--danger btn--secondary js-rotate-token"><%- @T('Reset Token') %></div>
|
||||
<% if channel.active is true: %>
|
||||
<div class="btn btn--secondary js-disable"><%- @T('Disable') %></div>
|
||||
<% else: %>
|
||||
<div class="btn btn--secondary js-enable"><%- @T('Enable') %></div>
|
||||
<% end %>
|
||||
<div class="btn js-edit"><%- @T('Edit') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<div class="alert alert--danger hidden" role="alert"></div>
|
||||
<fieldset>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Phone number') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="phone_number" type="text" name="phone_number" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_token" type="text" name="bot_token" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_endpoint" type="text" name="bot_endpoint" value="" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the group in which form submissions will get added to.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="js-messagesGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the organization to which submitters will be added to when they submit via this form.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="profile-organization js-organization"></div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="alert alert--danger hidden" role="alert"></div>
|
||||
|
||||
<fieldset>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Whatsapp Number') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="phone_number" type="text" name="phone_number" value="<%= @channel.options.phone_number %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_token" type="text" name="bot_token" value="<%= @channel.options.bot_token %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="bot_endpoint" type="text" name="bot_endpoint" value="<%= @channel.options.bot_endpoint %>" class="form-control" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the group in which incoming messages will be added to.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="js-messagesGroup"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for=""><%- @T('Choose the organization to which users will be added to when they send a message to this number.') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="profile-organization js-organization"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="token"><%- @T('Endpoint URL') %> <span>*</span></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input id="token" type="text" value="<%= "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/channels_cdr_whatsapp_webhook/#{@channel.options.token}" %>" class="form-control input js-select" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<div class="page-header">
|
||||
<div class="page-header-title">
|
||||
<h1><%- @T('Whatsapp') %></h1>
|
||||
</div>
|
||||
|
||||
<div class="page-header-meta">
|
||||
<a class="btn btn--success js-new"><%- @T('Add Whatsapp bot') %></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
<% if _.isEmpty(@channels): %>
|
||||
<div class="page-description">
|
||||
<p><%- @T('You have no configured %s right now.', 'Whatsapp numbers') %></p>
|
||||
</div>
|
||||
<% else: %>
|
||||
|
||||
<% for channel in @channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
|
||||
<div class="action-block action-row">
|
||||
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.phone_number %></h2>
|
||||
</div>
|
||||
<div class="action-flow action-flow--row">
|
||||
<div class="action-block">
|
||||
<h3><%- @T('Group') %></h3>
|
||||
<% if channel.options: %>
|
||||
<%= channel.options.groupName %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="action-block">
|
||||
<h3><%- @T('Endpoint URL') %></h3>
|
||||
<%- @T('Click the edit button to view the endpoint details ') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-controls">
|
||||
<div class="btn btn--danger btn--secondary js-delete"><%- @T('Delete') %></div>
|
||||
<div class="btn btn--danger btn--secondary js-rotate-token"><%- @T('Reset Token') %></div>
|
||||
<% if channel.active is true: %>
|
||||
<div class="btn btn--secondary js-disable"><%- @T('Disable') %></div>
|
||||
<% else: %>
|
||||
<div class="btn btn--secondary js-enable"><%- @T('Enable') %></div>
|
||||
<% end %>
|
||||
<div class="btn js-edit"><%- @T('Edit') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.icon-cdr-signal {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.icon-cdr-whatsapp {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChannelsCdrSignalController < ApplicationController
|
||||
prepend_before_action -> { authentication_check && authorize! }, except: [:webhook]
|
||||
skip_before_action :verify_csrf_token, only: [:webhook]
|
||||
|
||||
include CreatesTicketArticles
|
||||
|
||||
def index
|
||||
assets = {}
|
||||
channel_ids = []
|
||||
Channel.where(area: 'Signal::Number').order(:id).each do |channel|
|
||||
assets = channel.assets(assets)
|
||||
channel_ids.push channel.id
|
||||
end
|
||||
render json: {
|
||||
assets: assets,
|
||||
channel_ids: channel_ids
|
||||
}
|
||||
end
|
||||
|
||||
def add
|
||||
begin
|
||||
errors = {}
|
||||
errors['group_id'] = 'required' unless params[:group_id].present?
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
channel = Channel.create(
|
||||
area: 'Signal::Number',
|
||||
options: {
|
||||
adapter: 'cdr_signal',
|
||||
phone_number: params[:phone_number],
|
||||
bot_token: params[:bot_token],
|
||||
bot_endpoint: params[:bot_endpoint],
|
||||
token: SecureRandom.urlsafe_base64(48),
|
||||
organization_id: params[:organization_id]
|
||||
},
|
||||
group_id: params[:group_id],
|
||||
active: true
|
||||
)
|
||||
rescue StandardError => e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
end
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def update
|
||||
errors = {}
|
||||
errors['group_id'] = 'required' unless params[:group_id].present?
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
|
||||
begin
|
||||
channel.options[:phone_number] = params[:phone_number]
|
||||
channel.options[:bot_token] = params[:bot_token]
|
||||
channel.options[:bot_endpoint] = params[:bot_endpoint]
|
||||
channel.options[:organization_id] = params[:organization_id]
|
||||
channel.group_id = params[:group_id]
|
||||
channel.save!
|
||||
rescue StandardError => e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
end
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def rotate_token
|
||||
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
|
||||
channel.options[:token] = SecureRandom.urlsafe_base64(48)
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def enable
|
||||
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
|
||||
channel.active = true
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def disable
|
||||
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
|
||||
channel.active = false
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def destroy
|
||||
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
|
||||
channel.destroy
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def channel_for_token(token)
|
||||
return false unless token
|
||||
|
||||
Channel.where(area: 'Signal::Number').each do |channel|
|
||||
return channel if channel.options[:token] == token
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def webhook
|
||||
token = params['token']
|
||||
return render json: {}, status: 401 unless token
|
||||
|
||||
channel = channel_for_token(token)
|
||||
return render json: {}, status: 401 if !channel || !channel.active
|
||||
return render json: {}, status: 401 if channel.options[:token] != token
|
||||
|
||||
channel_id = channel.id
|
||||
|
||||
# validate input
|
||||
errors = {}
|
||||
|
||||
# %i[to
|
||||
# from
|
||||
# message_id
|
||||
# sent_at].each | field |
|
||||
# (errors[field] = 'required' if params[field].blank?)
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
message_id = params[:message_id]
|
||||
|
||||
return if Ticket::Article.exists?(message_id: "cdr_signal.#{message_id}")
|
||||
|
||||
receiver_phone_number = params[:to].strip
|
||||
sender_phone_number = params[:from].strip
|
||||
customer = User.find_by(phone: sender_phone_number)
|
||||
customer ||= User.find_by(mobile: sender_phone_number)
|
||||
unless customer
|
||||
role_ids = Role.signup_role_ids
|
||||
customer = User.create(
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: sender_phone_number,
|
||||
note: 'CDR Signal',
|
||||
active: true,
|
||||
role_ids: role_ids,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
# set current user
|
||||
UserInfo.current_user_id = customer.id
|
||||
current_user_set(customer, 'token_auth')
|
||||
|
||||
group = Group.find_by(id: channel.group_id)
|
||||
if group.blank?
|
||||
Rails.logger.error "Signal channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!"
|
||||
return render json: { error: 'There was an error during Signal submission' }, status: 500
|
||||
end
|
||||
|
||||
organization_id = channel.options['organization_id']
|
||||
if organization_id.present?
|
||||
organization = Organization.find_by(id: organization_id)
|
||||
unless organization.present?
|
||||
Rails.logger.error "Signal channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!"
|
||||
return render json: { error: 'There was an error during Signal submission' }, status: 500
|
||||
end
|
||||
unless customer.organization_id.present?
|
||||
customer.organization_id = organization.id
|
||||
customer.save!
|
||||
end
|
||||
end
|
||||
|
||||
message = params[:message] ||= 'No text content'
|
||||
sent_at = params[:sent_at]
|
||||
attachment_data_base64 = params[:attachment]
|
||||
attachment_filename = params[:filename]
|
||||
attachment_mimetype = params[:mime_type]
|
||||
title = "Message from #{sender_phone_number} at #{sent_at}"
|
||||
body = message
|
||||
|
||||
# find ticket or create one
|
||||
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
|
||||
ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first
|
||||
if ticket
|
||||
# check if title need to be updated
|
||||
ticket.title = title if ticket.title == '-'
|
||||
new_state = Ticket::State.find_by(default_create: true)
|
||||
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
|
||||
else
|
||||
ticket = Ticket.new(
|
||||
group_id: channel.group_id,
|
||||
title: title,
|
||||
customer_id: customer.id,
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
cdr_signal: {
|
||||
bot_token: channel.options[:bot_token], # change to bot id
|
||||
chat_id: sender_phone_number
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
ticket.save!
|
||||
|
||||
article_params = {
|
||||
from: sender_phone_number,
|
||||
to: receiver_phone_number,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
subject: title,
|
||||
body: body,
|
||||
content_type: 'text/plain',
|
||||
message_id: "cdr_signal.#{message_id}",
|
||||
ticket_id: ticket.id,
|
||||
internal: false,
|
||||
preferences: {
|
||||
cdr_signal: {
|
||||
timestamp: sent_at,
|
||||
message_id: message_id,
|
||||
from: sender_phone_number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attachment_data_base64.present?
|
||||
article_params[:attachments] = [
|
||||
# i don't even...
|
||||
# this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb
|
||||
# we need help from the ruby gods
|
||||
{
|
||||
'filename' => attachment_filename,
|
||||
:filename => attachment_filename,
|
||||
:data => attachment_data_base64,
|
||||
'data' => attachment_data_base64,
|
||||
'mime-type' => attachment_mimetype
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
ticket.with_lock do
|
||||
ta = article_create(ticket, article_params)
|
||||
ta.update!(type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id)
|
||||
end
|
||||
|
||||
ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id)
|
||||
|
||||
result = {
|
||||
ticket: {
|
||||
id: ticket.id,
|
||||
number: ticket.number
|
||||
}
|
||||
}
|
||||
|
||||
render json: result, status: :ok
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class ChannelsCdrVoiceController < ApplicationController
|
||||
prepend_before_action -> { authentication_check && authorize! }, except: [:webhook]
|
||||
skip_before_action :verify_csrf_token, only: [:webhook]
|
||||
|
||||
include CreatesTicketArticles
|
||||
|
||||
def index
|
||||
assets = {}
|
||||
channel_ids = []
|
||||
Channel.where(area: 'Voice::Number').order(:id).each do |channel|
|
||||
assets = channel.assets(assets)
|
||||
channel_ids.push channel.id
|
||||
end
|
||||
render json: {
|
||||
assets: assets,
|
||||
channel_ids: channel_ids
|
||||
}
|
||||
end
|
||||
|
||||
def add
|
||||
begin
|
||||
errors = {}
|
||||
errors['group_id'] = 'required' unless params[:group_id].present?
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
channel = Channel.create(
|
||||
area: 'Voice::Number',
|
||||
options: {
|
||||
phone_number: params[:phone_number],
|
||||
token: SecureRandom.urlsafe_base64(48),
|
||||
organization_id: params[:organization_id]
|
||||
},
|
||||
group_id: params[:group_id],
|
||||
active: true
|
||||
)
|
||||
rescue StandardError => e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
end
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def update
|
||||
errors = {}
|
||||
errors['group_id'] = 'required' unless params[:group_id].present?
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
|
||||
begin
|
||||
channel.options[:phone_number] = params[:phone_number]
|
||||
channel.options[:organization_id] = params[:organization_id]
|
||||
channel.group_id = params[:group_id]
|
||||
channel.save!
|
||||
rescue StandardError => e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
end
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def rotate_token
|
||||
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
|
||||
channel.options[:token] = SecureRandom.urlsafe_base64(48)
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def enable
|
||||
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
|
||||
channel.active = true
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def disable
|
||||
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
|
||||
channel.active = false
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def destroy
|
||||
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
|
||||
channel.destroy
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def channel_for_token(token)
|
||||
return false unless token
|
||||
|
||||
Channel.where(area: 'Voice::Number').each do |channel|
|
||||
return channel if channel.options[:token] == token
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def webhook
|
||||
token = params['token']
|
||||
return render json: {}, status: 401 unless token
|
||||
|
||||
channel = channel_for_token(token)
|
||||
return render json: {}, status: 401 if !channel || !channel.active
|
||||
return render json: {}, status: 401 if channel.options[:token] != token
|
||||
|
||||
channel_id = channel.id
|
||||
|
||||
# validate input
|
||||
errors = {}
|
||||
|
||||
%i[to
|
||||
from
|
||||
duration
|
||||
startTime
|
||||
endTime
|
||||
recording
|
||||
mimeType
|
||||
callSid].each do |field|
|
||||
errors[field] = 'required' if params[field].blank?
|
||||
end
|
||||
|
||||
valid_mimetypes = ['audio/mpeg']
|
||||
unless valid_mimetypes.include?(params[:mimeType])
|
||||
errors[:mimeType] = "invalid. must be one of #{valid_mimetypes.join(',')}"
|
||||
end
|
||||
|
||||
receiver_phone_number = params[:to]
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
caller_phone_number = params[:from].strip
|
||||
|
||||
customer = User.find_by(phone: caller_phone_number)
|
||||
customer ||= User.find_by(mobile: caller_phone_number)
|
||||
unless customer
|
||||
role_ids = Role.signup_role_ids
|
||||
customer = User.create(
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: caller_phone_number,
|
||||
active: true,
|
||||
role_ids: role_ids,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
# set current user
|
||||
UserInfo.current_user_id = customer.id
|
||||
current_user_set(customer, 'token_auth')
|
||||
|
||||
group = Group.find_by(id: channel.group_id)
|
||||
unless group.present?
|
||||
Rails.logger.error "Voice channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!"
|
||||
return render json: { error: 'There was an error during voice submission' }, status: 500
|
||||
end
|
||||
|
||||
organization_id = channel.options['organization_id']
|
||||
if organization_id.present?
|
||||
organization = Organization.find_by(id: organization_id)
|
||||
unless organization.present?
|
||||
Rails.logger.error "Voice channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!"
|
||||
return render json: { error: 'There was an error during voice submission' }, status: 500
|
||||
end
|
||||
unless customer.organization_id.present?
|
||||
customer.organization_id = organization.id
|
||||
customer.save!
|
||||
end
|
||||
end
|
||||
|
||||
call_id = params[:calLSid]
|
||||
duration = params[:duration]
|
||||
start_time = params[:startTime]
|
||||
end_time = params[:endTime]
|
||||
recording_data_base64 = params[:recording]
|
||||
recording_filename = "phone-call-#{start_time}-#{call_id}.mp3"
|
||||
recording_mimetype = params[:mimeType]
|
||||
|
||||
title = "Call from #{caller_phone_number} at #{start_time}"
|
||||
body = %(
|
||||
<ul>
|
||||
<li>Caller: #{caller_phone_number}</li>
|
||||
<li>Service Number: #{receiver_phone_number}</li>
|
||||
<li>Call Duration: #{duration} seconds</li>
|
||||
<li>Start Time: #{start_time}</li>
|
||||
<li>End Time: #{end_time}</li>
|
||||
</ul>
|
||||
<p>See the attached recording.</p>
|
||||
)
|
||||
|
||||
ticket_params = {
|
||||
group_id: group.id,
|
||||
customer_id: customer.id,
|
||||
title: title,
|
||||
preferences: {},
|
||||
note: 'This ticket was created from a recorded voice message.'
|
||||
}
|
||||
|
||||
article_params = {
|
||||
sender: 'Customer',
|
||||
subject: title,
|
||||
body: body,
|
||||
content_type: 'text/html',
|
||||
type: 'note',
|
||||
attachments: [
|
||||
# i don't even...
|
||||
# this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb
|
||||
# we need help from the ruby gods
|
||||
{
|
||||
'filename' => recording_filename,
|
||||
:filename => recording_filename,
|
||||
:data => recording_data_base64,
|
||||
'data' => recording_data_base64,
|
||||
'mime-type' => recording_mimetype
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
clean_params = Ticket.param_cleanup(ticket_params, true)
|
||||
ticket = Ticket.new(clean_params)
|
||||
|
||||
ticket.save!
|
||||
ticket.with_lock do
|
||||
article_params[:sender] = 'Customer'
|
||||
article_create(ticket, article_params)
|
||||
end
|
||||
|
||||
result = {
|
||||
ticket: {
|
||||
id: ticket.id,
|
||||
number: ticket.number
|
||||
}
|
||||
}
|
||||
|
||||
render json: result, status: :ok
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChannelsCdrWhatsappController < ApplicationController
|
||||
prepend_before_action -> { authentication_check && authorize! }, except: [:webhook]
|
||||
skip_before_action :verify_csrf_token, only: [:webhook]
|
||||
|
||||
include CreatesTicketArticles
|
||||
|
||||
def index
|
||||
assets = {}
|
||||
channel_ids = []
|
||||
Channel.where(area: 'Whatsapp::Number').order(:id).each do |channel|
|
||||
assets = channel.assets(assets)
|
||||
channel_ids.push channel.id
|
||||
end
|
||||
render json: {
|
||||
assets: assets,
|
||||
channel_ids: channel_ids
|
||||
}
|
||||
end
|
||||
|
||||
def add
|
||||
begin
|
||||
errors = {}
|
||||
errors['group_id'] = 'required' if params[:group_id].blank?
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
channel = Channel.create(
|
||||
area: 'Whatsapp::Number',
|
||||
options: {
|
||||
adapter: 'cdr_whatsapp',
|
||||
phone_number: params[:phone_number],
|
||||
bot_token: params[:bot_token],
|
||||
bot_endpoint: params[:bot_endpoint],
|
||||
token: SecureRandom.urlsafe_base64(48),
|
||||
organization_id: params[:organization_id]
|
||||
},
|
||||
group_id: params[:group_id],
|
||||
active: true
|
||||
)
|
||||
rescue StandardError => e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
end
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def update
|
||||
errors = {}
|
||||
errors['group_id'] = 'required' if params[:group_id].blank?
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
|
||||
begin
|
||||
channel.options[:phone_number] = params[:phone_number]
|
||||
channel.options[:bot_token] = params[:bot_token]
|
||||
channel.options[:bot_endpoint] = params[:bot_endpoint]
|
||||
channel.options[:organization_id] = params[:organization_id]
|
||||
channel.group_id = params[:group_id]
|
||||
channel.save!
|
||||
rescue StandardError => e
|
||||
raise Exceptions::UnprocessableEntity, e.message
|
||||
end
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def rotate_token
|
||||
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
|
||||
channel.options[:token] = SecureRandom.urlsafe_base64(48)
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def enable
|
||||
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
|
||||
channel.active = true
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def disable
|
||||
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
|
||||
channel.active = false
|
||||
channel.save!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def destroy
|
||||
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
|
||||
channel.destroy
|
||||
render json: {}
|
||||
end
|
||||
|
||||
def channel_for_token(token)
|
||||
return false unless token
|
||||
|
||||
Channel.where(area: 'Whatsapp::Number').each do |channel|
|
||||
return channel if channel.options[:token] == token
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def webhook
|
||||
token = params['token']
|
||||
return render json: {}, status: :unauthorized unless token
|
||||
|
||||
channel = channel_for_token(token)
|
||||
return render json: {}, status: :unauthorized if !channel || !channel.active
|
||||
return render json: {}, status: :unauthorized if channel.options[:token] != token
|
||||
|
||||
channel_id = channel.id
|
||||
|
||||
# validate input
|
||||
errors = {}
|
||||
|
||||
%i[to
|
||||
from
|
||||
message_id
|
||||
sent_at].each do |field|
|
||||
errors[field] = 'required' if params[field].blank?
|
||||
end
|
||||
|
||||
if errors.present?
|
||||
render json: {
|
||||
errors: errors
|
||||
}, status: :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
message_id = params[:message_id]
|
||||
|
||||
return if Ticket::Article.exists?(message_id: "cdr_whatsapp.#{message_id}")
|
||||
|
||||
receiver_phone_number = params[:to].strip
|
||||
sender_phone_number = params[:from].strip
|
||||
customer = User.find_by(phone: sender_phone_number)
|
||||
customer ||= User.find_by(mobile: sender_phone_number)
|
||||
unless customer
|
||||
role_ids = Role.signup_role_ids
|
||||
customer = User.create(
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
password: '',
|
||||
phone: sender_phone_number,
|
||||
note: 'CDR Whatsapp',
|
||||
active: true,
|
||||
role_ids: role_ids,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
# set current user
|
||||
UserInfo.current_user_id = customer.id
|
||||
current_user_set(customer, 'token_auth')
|
||||
|
||||
group = Group.find_by(id: channel.group_id)
|
||||
if group.blank?
|
||||
Rails.logger.error "Whatsapp channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!"
|
||||
return render json: { error: 'There was an error during Whatsapp submission' }, status: :internal_server_error
|
||||
end
|
||||
|
||||
organization_id = channel.options['organization_id']
|
||||
if organization_id.present?
|
||||
organization = Organization.find_by(id: organization_id)
|
||||
if organization.blank?
|
||||
Rails.logger.error "Whatsapp channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!"
|
||||
return render json: { error: 'There was an error during Whatsapp submission' }, status: :internal_server_error
|
||||
end
|
||||
if customer.organization_id.blank?
|
||||
customer.organization_id = organization.id
|
||||
customer.save!
|
||||
end
|
||||
end
|
||||
|
||||
message = params[:message] ||= 'No text content'
|
||||
sent_at = params[:sent_at]
|
||||
attachment_data_base64 = params[:attachment]
|
||||
attachment_filename = params[:filename]
|
||||
attachment_mimetype = params[:mime_type]
|
||||
title = "Message from #{sender_phone_number} at #{sent_at}"
|
||||
body = message
|
||||
|
||||
# find ticket or create one
|
||||
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
|
||||
ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first
|
||||
if ticket
|
||||
# check if title need to be updated
|
||||
ticket.title = title if ticket.title == '-'
|
||||
new_state = Ticket::State.find_by(default_create: true)
|
||||
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
|
||||
else
|
||||
ticket = Ticket.new(
|
||||
group_id: channel.group_id,
|
||||
title: title,
|
||||
customer_id: customer.id,
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
cdr_whatsapp: {
|
||||
bot_token: channel.options[:bot_token], # change to bot id
|
||||
chat_id: sender_phone_number
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
ticket.save!
|
||||
|
||||
article_params = {
|
||||
from: sender_phone_number,
|
||||
to: receiver_phone_number,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
subject: title,
|
||||
body: body,
|
||||
content_type: 'text/plain',
|
||||
message_id: "cdr_whatsapp.#{message_id}",
|
||||
ticket_id: ticket.id,
|
||||
internal: false,
|
||||
preferences: {
|
||||
cdr_whatsapp: {
|
||||
timestamp: sent_at,
|
||||
message_id: message_id,
|
||||
from: sender_phone_number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attachment_data_base64.present?
|
||||
article_params[:attachments] = [
|
||||
# i don't even...
|
||||
# this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb
|
||||
# we need help from the ruby gods
|
||||
{
|
||||
'filename' => attachment_filename,
|
||||
:filename => attachment_filename,
|
||||
:data => attachment_data_base64,
|
||||
'data' => attachment_data_base64,
|
||||
'mime-type' => attachment_mimetype
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# setting the article type after saving seems to be the only way to get it to stick
|
||||
ticket.with_lock do
|
||||
ta = article_create(ticket, article_params)
|
||||
ta.update!(type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id)
|
||||
end
|
||||
|
||||
ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id)
|
||||
|
||||
result = {
|
||||
ticket: {
|
||||
id: ticket.id,
|
||||
number: ticket.number
|
||||
}
|
||||
}
|
||||
|
||||
render json: result, status: :ok
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Channel
|
||||
class Driver
|
||||
class CdrSignal
|
||||
def fetchable?(_channel)
|
||||
false
|
||||
end
|
||||
|
||||
def disconnect; end
|
||||
|
||||
#
|
||||
# instance = Channel::Driver::CdrSignal.new
|
||||
# instance.send(
|
||||
# {
|
||||
# adapter: 'cdrsignal',
|
||||
# auth: {
|
||||
# api_key: api_key
|
||||
# },
|
||||
# },
|
||||
# signal_attributes,
|
||||
# notification
|
||||
# )
|
||||
#
|
||||
|
||||
def send(options, article, _notification = false)
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
options = check_external_credential(options)
|
||||
|
||||
Rails.logger.debug { 'signal send started' }
|
||||
Rails.logger.debug { options.inspect }
|
||||
@signal = ::CdrSignal.new(options[:bot_endpoint], options[:bot_token])
|
||||
@signal.from_article(article)
|
||||
end
|
||||
|
||||
def self.streamable?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_external_credential(options)
|
||||
if options[:auth] && options[:auth][:external_credential_id]
|
||||
external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id])
|
||||
raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" unless external_credential
|
||||
|
||||
options[:auth][:api_key] = external_credential.credentials['api_key']
|
||||
end
|
||||
options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Channel
|
||||
class Driver
|
||||
class CdrWhatsapp
|
||||
def fetchable?(_channel)
|
||||
false
|
||||
end
|
||||
|
||||
def disconnect; end
|
||||
|
||||
#
|
||||
# instance = Channel::Driver::CdrWhatsapp.new
|
||||
# instance.send(
|
||||
# {
|
||||
# adapter: 'cdr_whatsapp',
|
||||
# auth: {
|
||||
# api_key: api_key
|
||||
# },
|
||||
# },
|
||||
# whatsapp_attributes,
|
||||
# notification
|
||||
# )
|
||||
#
|
||||
|
||||
def send(options, article, _notification = false)
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
options = check_external_credential(options)
|
||||
|
||||
Rails.logger.debug { 'whatsapp send started' }
|
||||
Rails.logger.debug { options.inspect }
|
||||
@whatsapp = ::CdrWhatsapp.new(options[:bot_endpoint], options[:bot_token])
|
||||
@whatsapp.from_article(article)
|
||||
end
|
||||
|
||||
def self.streamable?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_external_credential(options)
|
||||
if options[:auth] && options[:auth][:external_credential_id]
|
||||
external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id])
|
||||
raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" unless external_credential
|
||||
|
||||
options[:auth][:api_key] = external_credential.credentials['api_key']
|
||||
end
|
||||
options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Observer::Ticket::Article::CommunicateCdrSignal < ActiveRecord::Observer
|
||||
observe 'ticket::_article'
|
||||
|
||||
def after_create(record)
|
||||
# return if we run import mode
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# if sender is customer, do not communicate
|
||||
return true unless record.sender_id
|
||||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return true if sender.nil?
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# only apply on signal messages
|
||||
return true unless record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return true if type.name !~ /\Acdr_signal/i
|
||||
|
||||
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateCdrSignal::BackgroundJob.new(record.id))
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Observer
|
||||
module Ticket
|
||||
module Article
|
||||
class CommunicateCdrSignal
|
||||
class BackgroundJob
|
||||
def initialize(id)
|
||||
@article_id = id
|
||||
end
|
||||
|
||||
def perform
|
||||
article = ::Ticket::Article.find(@article_id)
|
||||
|
||||
# set retry count
|
||||
article.preferences['delivery_retry'] ||= 0
|
||||
article.preferences['delivery_retry'] += 1
|
||||
|
||||
ticket = ::Ticket.lookup(id: article.ticket_id)
|
||||
Rails.logger.debug { 'Signal background job' }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
Rails.logger.debug { article.inspect }
|
||||
unless ticket.preferences
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
unless ticket.preferences['cdr_signal']
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences['cdr_signal'] for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
unless ticket.preferences['cdr_signal']['chat_id']
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences['cdr_signal']['chat_id'] for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
unless ticket.preferences['cdr_signal']['bot_token']
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences['cdr_signal']['bot_token'] for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
|
||||
channel = ::CdrSignal.bot_by_bot_token(ticket.preferences['cdr_signal']['bot_token'])
|
||||
Rails.logger.debug { "signal got channel for #{channel.inspect}" }
|
||||
|
||||
channel ||= ::Channel.lookup(id: ticket.preferences['channel_id'])
|
||||
unless channel
|
||||
log_error(article,
|
||||
"No such channel for bot #{ticket.preferences['cdr_signal']['bot_token']} or channel id #{ticket.preferences['channel_id']}")
|
||||
end
|
||||
if channel.options[:bot_token].blank?
|
||||
log_error(article,
|
||||
"Channel.find(#{channel.id}) has no signal api token!")
|
||||
end
|
||||
|
||||
begin
|
||||
result = channel.deliver(
|
||||
to: ticket.preferences[:cdr_signal][:chat_id],
|
||||
body: article.body
|
||||
)
|
||||
rescue StandardError => e
|
||||
log_error(article, e.message)
|
||||
return
|
||||
end
|
||||
|
||||
Rails.logger.debug { "send result: #{result}" }
|
||||
|
||||
if result.nil? || result[:error].present?
|
||||
log_error(article, 'Delivering signal message failed!')
|
||||
return
|
||||
end
|
||||
|
||||
article.to = result['result']['recipient']
|
||||
article.from = result['result']['source']
|
||||
|
||||
message_id = format('%<source>s@%<timestamp>s', source: result['result']['source'],
|
||||
timestamp: result['result']['timestamp'])
|
||||
article.preferences['cdr_signal'] = {
|
||||
timestamp: result['result']['timestamp'],
|
||||
message_id: message_id,
|
||||
from: result['result']['source'],
|
||||
to: result['result']['recipient']
|
||||
}
|
||||
|
||||
# set delivery status
|
||||
article.preferences['delivery_status_message'] = nil
|
||||
article.preferences['delivery_status'] = 'success'
|
||||
article.preferences['delivery_status_date'] = Time.zone.now
|
||||
|
||||
article.message_id = "cdr_signal.#{message_id}"
|
||||
|
||||
article.save!
|
||||
|
||||
Rails.logger.info "Sent signal message to: '#{article.to}' (from #{article.from})"
|
||||
|
||||
article
|
||||
end
|
||||
|
||||
def log_error(local_record, message)
|
||||
local_record.preferences['delivery_status'] = 'fail'
|
||||
local_record.preferences['delivery_status_message'] =
|
||||
message.encode('UTF-8', 'UTF-8', invalid: :replace, replace: '?')
|
||||
local_record.preferences['delivery_status_date'] = Time.zone.now
|
||||
local_record.save
|
||||
Rails.logger.error message
|
||||
|
||||
if local_record.preferences['delivery_retry'] > 3
|
||||
::Ticket::Article.create(
|
||||
ticket_id: local_record.ticket_id,
|
||||
content_type: 'text/plain',
|
||||
body: "Unable to send signal message: #{message}",
|
||||
internal: true,
|
||||
sender: ::Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: ::Ticket::Article::Type.find_by(name: 'note'),
|
||||
preferences: {
|
||||
delivery_article_id_related: local_record.id,
|
||||
delivery_message: true
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
raise message
|
||||
end
|
||||
|
||||
def max_attempts
|
||||
4
|
||||
end
|
||||
|
||||
def reschedule_at(current_time, attempts)
|
||||
return current_time + attempts * 120.seconds if Rails.env.production?
|
||||
|
||||
current_time + 5.seconds
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Observer::Ticket::Article::CommunicateCdrWhatsapp < ActiveRecord::Observer
|
||||
observe 'ticket::_article'
|
||||
|
||||
def after_create(record)
|
||||
# return if we run import mode
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# if sender is customer, do not communicate
|
||||
return true unless record.sender_id
|
||||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return true if sender.nil?
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# only apply on whatsapp messages
|
||||
return true unless record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return true if type.name !~ /\Acdr_whatsapp/i
|
||||
|
||||
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateCdrWhatsapp::BackgroundJob.new(record.id))
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Observer
|
||||
module Ticket
|
||||
module Article
|
||||
class CommunicateCdrWhatsapp
|
||||
class BackgroundJob
|
||||
def initialize(id)
|
||||
@article_id = id
|
||||
end
|
||||
|
||||
def perform
|
||||
article = ::Ticket::Article.find(@article_id)
|
||||
|
||||
# set retry count
|
||||
article.preferences['delivery_retry'] ||= 0
|
||||
article.preferences['delivery_retry'] += 1
|
||||
|
||||
ticket = ::Ticket.lookup(id: article.ticket_id)
|
||||
Rails.logger.debug { 'Whatsapp background job' }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
Rails.logger.debug { article.inspect }
|
||||
unless ticket.preferences
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
unless ticket.preferences['cdr_whatsapp']
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences['cdr_whatsapp'] for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
unless ticket.preferences['cdr_whatsapp']['chat_id']
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences['cdr_whatsapp']['chat_id'] for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
unless ticket.preferences['cdr_whatsapp']['bot_token']
|
||||
log_error(article,
|
||||
"Can't find ticket.preferences['cdr_whatsapp']['bot_token'] for Ticket.find(#{article.ticket_id})")
|
||||
end
|
||||
|
||||
channel = ::CdrWhatsapp.bot_by_bot_token(ticket.preferences['cdr_whatsapp']['bot_token'])
|
||||
Rails.logger.debug { "whatsapp got channel for #{channel.inspect}" }
|
||||
|
||||
channel ||= ::Channel.lookup(id: ticket.preferences['channel_id'])
|
||||
unless channel
|
||||
log_error(article,
|
||||
"No such channel for bot #{ticket.preferences['cdr_whatsapp']['bot_token']} or channel id #{ticket.preferences['channel_id']}")
|
||||
end
|
||||
if channel.options[:bot_token].blank?
|
||||
log_error(article,
|
||||
"Channel.find(#{channel.id}) has no whatsapp api token!")
|
||||
end
|
||||
|
||||
begin
|
||||
result = channel.deliver(
|
||||
to: ticket.preferences[:cdr_whatsapp][:chat_id],
|
||||
body: article.body
|
||||
)
|
||||
rescue StandardError => e
|
||||
log_error(article, e.message)
|
||||
return
|
||||
end
|
||||
|
||||
Rails.logger.debug { "send result: #{result}" }
|
||||
|
||||
if result.nil? || result[:error].present?
|
||||
log_error(article, 'Delivering whatsapp message failed!')
|
||||
return
|
||||
end
|
||||
|
||||
article.to = result['result']['recipient']
|
||||
article.from = result['result']['source']
|
||||
|
||||
message_id = format('%<source>s@%<timestamp>s', source: result['result']['source'],
|
||||
timestamp: result['result']['timestamp'])
|
||||
article.preferences['cdr_whatsapp'] = {
|
||||
timestamp: result['result']['timestamp'],
|
||||
message_id: message_id,
|
||||
from: result['result']['source'],
|
||||
to: result['result']['recipient']
|
||||
}
|
||||
|
||||
# set delivery status
|
||||
article.preferences['delivery_status_message'] = nil
|
||||
article.preferences['delivery_status'] = 'success'
|
||||
article.preferences['delivery_status_date'] = Time.zone.now
|
||||
|
||||
article.message_id = "cdr_whatsapp.#{message_id}"
|
||||
|
||||
article.save!
|
||||
|
||||
Rails.logger.info "Sent whatsapp message to: '#{article.to}' (from #{article.from})"
|
||||
|
||||
article
|
||||
end
|
||||
|
||||
def log_error(local_record, message)
|
||||
local_record.preferences['delivery_status'] = 'fail'
|
||||
local_record.preferences['delivery_status_message'] =
|
||||
message.encode('UTF-8', 'UTF-8', invalid: :replace, replace: '?')
|
||||
local_record.preferences['delivery_status_date'] = Time.zone.now
|
||||
local_record.save
|
||||
Rails.logger.error message
|
||||
|
||||
if local_record.preferences['delivery_retry'] > 3
|
||||
::Ticket::Article.create(
|
||||
ticket_id: local_record.ticket_id,
|
||||
content_type: 'text/plain',
|
||||
body: "Unable to send whatsapp message: #{message}",
|
||||
internal: true,
|
||||
sender: ::Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: ::Ticket::Article::Type.find_by(name: 'note'),
|
||||
preferences: {
|
||||
delivery_article_id_related: local_record.id,
|
||||
delivery_message: true
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
raise message
|
||||
end
|
||||
|
||||
def max_attempts
|
||||
4
|
||||
end
|
||||
|
||||
def reschedule_at(current_time, attempts)
|
||||
return current_time + attempts * 120.seconds if Rails.env.production?
|
||||
|
||||
current_time + 5.seconds
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Controllers
|
||||
class ChannelsCdrSignalControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
default_permit!('admin.channel_cdr_signal')
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Controllers
|
||||
class ChannelsCdrVoiceControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
default_permit!('admin.channel_cdr_voice')
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Controllers
|
||||
class ChannelsCdrWhatsappControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
default_permit!('admin.channel_cdr_whatsapp')
|
||||
end
|
||||
end
|
||||
15
zammad-addon-metamigo/src/config/initializers/cdr_signal.rb
Normal file
15
zammad-addon-metamigo/src/config/initializers/cdr_signal.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
Ticket::Article.add_observer Observer::Ticket::Article::CommunicateCdrSignal.instance
|
||||
|
||||
icon = File.read('public/assets/images/icons/cdr_signal.svg')
|
||||
doc = File.open('public/assets/images/icons.svg') { |f| Nokogiri::XML(f) }
|
||||
if !doc.at_css('#icon-cdr-signal')
|
||||
doc.at('svg').add_child(icon)
|
||||
Rails.logger.debug 'signal icon added to icon set'
|
||||
else
|
||||
Rails.logger.debug 'signal icon already in icon set'
|
||||
end
|
||||
File.write('public/assets/images/icons.svg', doc.to_xml)
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
Ticket::Article.add_observer Observer::Ticket::Article::CommunicateCdrWhatsapp.instance
|
||||
|
||||
icon = File.read('public/assets/images/icons/cdr_whatsapp.svg')
|
||||
doc = File.open('public/assets/images/icons.svg') { |f| Nokogiri::XML(f) }
|
||||
if !doc.at_css('#icon-cdr-whatsapp')
|
||||
doc.at('svg').add_child(icon)
|
||||
Rails.logger.debug 'whatsapp icon added to icon set'
|
||||
else
|
||||
Rails.logger.debug 'whatsapp icon already in icon set'
|
||||
end
|
||||
File.write('public/assets/images/icons.svg', doc.to_xml)
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#index', via: :get
|
||||
match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#add', via: :post
|
||||
match "#{api_path}/channels_cdr_signal/:id", to: 'channels_cdr_signal#update', via: :put
|
||||
match "#{api_path}/channels_cdr_signal_webhook/:token", to: 'channels_cdr_signal#webhook', via: :post
|
||||
match "#{api_path}/channels_cdr_signal_disable", to: 'channels_cdr_signal#disable', via: :post
|
||||
match "#{api_path}/channels_cdr_signal_enable", to: 'channels_cdr_signal#enable', via: :post
|
||||
match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#destroy', via: :delete
|
||||
match "#{api_path}/channels_cdr_signal_rotate_token", to: 'channels_cdr_signal#rotate_token', via: :post
|
||||
end
|
||||
14
zammad-addon-metamigo/src/config/routes/channel_cdr_voice.rb
Normal file
14
zammad-addon-metamigo/src/config/routes/channel_cdr_voice.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match "#{api_path}/channels_cdr_voice", to: 'channels_cdr_voice#index', via: :get
|
||||
match "#{api_path}/channels_cdr_voice", to: 'channels_cdr_voice#add', via: :post
|
||||
match "#{api_path}/channels_cdr_voice/:id", to: 'channels_cdr_voice#update', via: :put
|
||||
match "#{api_path}/channels_cdr_voice_webhook/:token", to: 'channels_cdr_voice#webhook', via: :post
|
||||
match "#{api_path}/channels_cdr_voice_disable", to: 'channels_cdr_voice#disable', via: :post
|
||||
match "#{api_path}/channels_cdr_voice_enable", to: 'channels_cdr_voice#enable', via: :post
|
||||
match "#{api_path}/channels_cdr_voice", to: 'channels_cdr_voice#destroy', via: :delete
|
||||
match "#{api_path}/channels_cdr_voice_rotate_token", to: 'channels_cdr_voice#rotate_token', via: :post
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match "#{api_path}/channels_cdr_whatsapp", to: 'channels_cdr_whatsapp#index', via: :get
|
||||
match "#{api_path}/channels_cdr_whatsapp", to: 'channels_cdr_whatsapp#add', via: :post
|
||||
match "#{api_path}/channels_cdr_whatsapp/:id", to: 'channels_cdr_whatsapp#update', via: :put
|
||||
match "#{api_path}/channels_cdr_whatsapp_webhook/:token", to: 'channels_cdr_whatsapp#webhook', via: :post
|
||||
match "#{api_path}/channels_cdr_whatsapp_disable", to: 'channels_cdr_whatsapp#disable', via: :post
|
||||
match "#{api_path}/channels_cdr_whatsapp_enable", to: 'channels_cdr_whatsapp#enable', via: :post
|
||||
match "#{api_path}/channels_cdr_whatsapp", to: 'channels_cdr_whatsapp#destroy', via: :delete
|
||||
match "#{api_path}/channels_cdr_whatsapp_rotate_token", to: 'channels_cdr_whatsapp#rotate_token', via: :post
|
||||
end
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CdrSignalChannel < ActiveRecord::Migration[5.2]
|
||||
def self.up
|
||||
Ticket::Article::Type.create_if_not_exists(
|
||||
name: 'cdr_signal',
|
||||
communication: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.channel_cdr_signal',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Channel - Signal']
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def self.down
|
||||
t = Ticket::Article::Type.find_by(name: 'cdr_signal')
|
||||
t&.destroy
|
||||
|
||||
p = Permission.find_by(name: 'admin.channel_cdr_signal')
|
||||
p&.destroy
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CdrVoiceChannel < ActiveRecord::Migration[5.2]
|
||||
def self.up
|
||||
Ticket::Article::Type.create_if_not_exists(
|
||||
name: 'cdr_voice',
|
||||
communication: false,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.channel_cdr_voice',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Channel - Voice']
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def self.down
|
||||
t = Ticket::Article::Type.find_by(name: 'cdr_voice')
|
||||
|
||||
t&.destroy
|
||||
|
||||
p = Permission.find_by(name: 'admin.channel_cdr_voice')
|
||||
p&.destroy
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CdrWhatsappChannel < ActiveRecord::Migration[5.2]
|
||||
def self.up
|
||||
Ticket::Article::Type.create_if_not_exists(
|
||||
name: 'cdr_whatsapp',
|
||||
communication: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.channel_cdr_whatsapp',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Channel - Whatsapp']
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def self.down
|
||||
t = Ticket::Article::Type.find_by(name: 'cdr_whatsapp')
|
||||
t&.destroy
|
||||
|
||||
p = Permission.find_by(name: 'admin.channel_cdr_whatsapp')
|
||||
p&.destroy
|
||||
end
|
||||
end
|
||||
319
zammad-addon-metamigo/src/lib/cdr_signal.rb
Normal file
319
zammad-addon-metamigo/src/lib/cdr_signal.rb
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'cdr_signal_api'
|
||||
|
||||
class CdrSignal
|
||||
attr_accessor :client
|
||||
|
||||
#
|
||||
# check token and return bot attributes of token
|
||||
#
|
||||
# bot = CdrSignal.check_token('token')
|
||||
#
|
||||
|
||||
def self.check_token(api_url, token)
|
||||
api = CdrSignalAPI.new(api_url, token)
|
||||
begin
|
||||
bot = api.fetch_self
|
||||
rescue StandardError => e
|
||||
raise "invalid api token: #{e.message}"
|
||||
end
|
||||
bot
|
||||
end
|
||||
|
||||
#
|
||||
# create or update channel, store bot attributes and verify token
|
||||
#
|
||||
# channel = CdrSignal.create_or_update_channel('token', params)
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# channel # instance of Channel
|
||||
#
|
||||
|
||||
def self.create_or_update_channel(api_url, token, params, channel = nil)
|
||||
# verify token
|
||||
bot = CdrSignal.check_token(api_url, token)
|
||||
|
||||
raise 'Bot already exists!' unless channel && CdrSignal.bot_duplicate?(bot['id'])
|
||||
|
||||
raise 'Group needed!' if params[:group_id].blank?
|
||||
|
||||
group = Group.find_by(id: params[:group_id])
|
||||
raise 'Group invalid!' unless group
|
||||
|
||||
unless channel
|
||||
channel = CdrSignal.bot_by_bot_id(bot['id'])
|
||||
channel ||= Channel.new
|
||||
end
|
||||
channel.area = 'Signal::Account'
|
||||
channel.options = {
|
||||
adapter: 'cdr_signal',
|
||||
bot: {
|
||||
id: bot['id'],
|
||||
number: bot['number']
|
||||
},
|
||||
api_token: token,
|
||||
api_url: api_url,
|
||||
welcome: params[:welcome]
|
||||
}
|
||||
channel.group_id = group.id
|
||||
channel.active = true
|
||||
channel.save!
|
||||
channel
|
||||
end
|
||||
|
||||
#
|
||||
# check if bot already exists as channel
|
||||
#
|
||||
# success = CdrSignal.bot_duplicate?(bot_id)
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# channel # instance of Channel
|
||||
#
|
||||
|
||||
def self.bot_duplicate?(bot_id, channel_id = nil)
|
||||
Channel.where(area: 'Signal::Account').each do |channel|
|
||||
next unless channel.options
|
||||
next unless channel.options[:bot]
|
||||
next unless channel.options[:bot][:id]
|
||||
next if channel.options[:bot][:id] != bot_id
|
||||
next if channel.id.to_s == channel_id.to_s
|
||||
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
#
|
||||
# get channel by bot_id
|
||||
#
|
||||
# channel = CdrSignal.bot_by_bot_id(bot_id)
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# true|false
|
||||
#
|
||||
|
||||
def self.bot_by_bot_token(bot_token)
|
||||
Channel.where(area: 'Signal::Account').each do |channel|
|
||||
next unless channel.options
|
||||
next unless channel.options[:bot_token]
|
||||
return channel if channel.options[:bot_token].to_s == bot_token.to_s
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
#
|
||||
# date = CdrSignal.timestamp_to_date('1543414973285')
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# 2018-11-28T14:22:53.285Z
|
||||
#
|
||||
|
||||
def self.timestamp_to_date(timestamp_str)
|
||||
Time.at(timestamp_str.to_i).utc.to_datetime
|
||||
end
|
||||
|
||||
def self.message_id(message_raw)
|
||||
format('%<source>s@%<timestamp>s', source: message_raw['source'], timestamp: message_raw['timestamp'])
|
||||
end
|
||||
|
||||
#
|
||||
# client = CdrSignal.new('token')
|
||||
#
|
||||
|
||||
def initialize(api_url, token)
|
||||
@token = token
|
||||
@api_url = api_url
|
||||
@api = CdrSignalAPI.new(api_url, token)
|
||||
end
|
||||
|
||||
#
|
||||
# client.send_message(chat_id, 'some message')
|
||||
#
|
||||
|
||||
def send_message(recipient, message)
|
||||
return if Rails.env.test?
|
||||
|
||||
@api.send_message(recipient, message)
|
||||
end
|
||||
|
||||
def user(number)
|
||||
{
|
||||
# id: params[:message][:from][:id],
|
||||
id: number,
|
||||
username: number
|
||||
# first_name: params[:message][:from][:first_name],
|
||||
# last_name: params[:message][:from][:last_name]
|
||||
}
|
||||
end
|
||||
|
||||
def to_user(message)
|
||||
Rails.logger.debug { 'Create user from message...' }
|
||||
Rails.logger.debug { message.inspect }
|
||||
|
||||
# do message_user lookup
|
||||
message_user = user(message[:source])
|
||||
|
||||
# create or update user
|
||||
login = message_user[:username] || message_user[:id]
|
||||
|
||||
auth = Authorization.find_by(uid: message[:source], provider: 'cdr_signal')
|
||||
|
||||
user_data = {
|
||||
login: login,
|
||||
mobile: message[:source]
|
||||
}
|
||||
|
||||
user = if auth
|
||||
User.find(auth.user_id)
|
||||
else
|
||||
User.where(mobile: message[:source]).order(:updated_at).first
|
||||
end
|
||||
if user
|
||||
user.update!(user_data)
|
||||
else
|
||||
user = User.create!(
|
||||
firstname: message[:source],
|
||||
mobile: message[:source],
|
||||
note: "Signal #{message_user[:username]}",
|
||||
active: true,
|
||||
role_ids: Role.signup_role_ids
|
||||
)
|
||||
end
|
||||
|
||||
# create or update authorization
|
||||
auth_data = {
|
||||
uid: message_user[:id],
|
||||
username: login,
|
||||
user_id: user.id,
|
||||
provider: 'cdr_signal'
|
||||
}
|
||||
if auth
|
||||
auth.update!(auth_data)
|
||||
else
|
||||
Authorization.create(auth_data)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def to_ticket(message, user, group_id, channel)
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
Rails.logger.debug { 'Create ticket from message...' }
|
||||
Rails.logger.debug { message.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { group_id.inspect }
|
||||
|
||||
# prepare title
|
||||
title = '-'
|
||||
title = message[:message][:body] unless message[:message][:body].nil?
|
||||
title = "#{title[0, 60]}..." if title.length > 60
|
||||
|
||||
# find ticket or create one
|
||||
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
|
||||
ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first
|
||||
if ticket
|
||||
|
||||
# check if title need to be updated
|
||||
ticket.title = title if ticket.title == '-'
|
||||
new_state = Ticket::State.find_by(default_create: true)
|
||||
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
|
||||
ticket.save!
|
||||
return ticket
|
||||
end
|
||||
|
||||
ticket = Ticket.new(
|
||||
group_id: group_id,
|
||||
title: title,
|
||||
state_id: Ticket::State.find_by(default_create: true).id,
|
||||
priority_id: Ticket::Priority.find_by(default_create: true).id,
|
||||
customer_id: user.id,
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
cdr_signal: {
|
||||
bot_token: channel.options[:bot_token],
|
||||
chat_id: message[:source]
|
||||
}
|
||||
}
|
||||
)
|
||||
ticket.save!
|
||||
ticket
|
||||
end
|
||||
|
||||
def to_article(message, user, ticket, channel)
|
||||
Rails.logger.debug { 'Create article from message...' }
|
||||
Rails.logger.debug { message.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
article = Ticket::Article.new(
|
||||
from: message[:source],
|
||||
to: channel[:options][:bot][:number],
|
||||
body: message[:message][:body],
|
||||
content_type: 'text/plain',
|
||||
message_id: "cdr_signal.#{message[:id]}",
|
||||
ticket_id: ticket.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
internal: false,
|
||||
preferences: {
|
||||
cdr_signal: {
|
||||
timestamp: message[:timestamp],
|
||||
message_id: message[:id],
|
||||
from: message[:source]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# TODO: attachments
|
||||
# TODO voice
|
||||
# TODO emojis
|
||||
#
|
||||
if message[:message][:body]
|
||||
Rails.logger.debug { article.inspect }
|
||||
article.save!
|
||||
|
||||
Store.remove(
|
||||
object: 'Ticket::Article',
|
||||
o_id: article.id
|
||||
)
|
||||
|
||||
return article
|
||||
end
|
||||
raise 'invalid action'
|
||||
end
|
||||
|
||||
def to_group(message, group_id, channel)
|
||||
# begin import
|
||||
Rails.logger.debug { 'signal import message' }
|
||||
|
||||
# TODO: handle messages in group chats
|
||||
|
||||
return if Ticket::Article.find_by(message_id: message[:id])
|
||||
|
||||
ticket = nil
|
||||
# use transaction
|
||||
Transaction.execute(reset_user_id: true) do
|
||||
user = to_user(message)
|
||||
ticket = to_ticket(message, user, group_id, channel)
|
||||
to_article(message, user, ticket, channel)
|
||||
end
|
||||
|
||||
ticket
|
||||
end
|
||||
|
||||
def from_article(article)
|
||||
# sends a message from a zammad article
|
||||
|
||||
Rails.logger.debug { "Create signal message from article to '#{article[:to]}'..." }
|
||||
|
||||
@api.send_message(article[:to], article[:body])
|
||||
end
|
||||
end
|
||||
41
zammad-addon-metamigo/src/lib/cdr_signal_api.rb
Normal file
41
zammad-addon-metamigo/src/lib/cdr_signal_api.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
require 'net/http'
|
||||
require 'net/https'
|
||||
require 'uri'
|
||||
require 'rest-client'
|
||||
|
||||
class CdrSignalAPI
|
||||
def initialize(api_url, token)
|
||||
@token = token
|
||||
@last_update = 0
|
||||
@api = api_url
|
||||
end
|
||||
|
||||
def parse_hash(hash)
|
||||
ret = {}
|
||||
hash.map do |k, v|
|
||||
ret[k] = CGI.encode(v.to_s.gsub('\\\'', '\''))
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def get(api)
|
||||
url = "#{@api}/bots/#{@token}/#{api}"
|
||||
JSON.parse(RestClient.get(url, { accept: :json }).body)
|
||||
end
|
||||
|
||||
def post(api, params = {})
|
||||
url = "#{@api}/bots/#{@token}/#{api}"
|
||||
JSON.parse(RestClient.post(url, params, { accept: :json }).body)
|
||||
end
|
||||
|
||||
def fetch_self
|
||||
get('')
|
||||
end
|
||||
|
||||
def send_message(recipient, text, options = {})
|
||||
post('send', { phoneNumber: recipient.to_s, message: text }.merge(parse_hash(options)))
|
||||
end
|
||||
end
|
||||
319
zammad-addon-metamigo/src/lib/cdr_whatsapp.rb
Normal file
319
zammad-addon-metamigo/src/lib/cdr_whatsapp.rb
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'cdr_whatsapp_api'
|
||||
|
||||
class CdrWhatsapp
|
||||
attr_accessor :client
|
||||
|
||||
#
|
||||
# check token and return bot attributes of token
|
||||
#
|
||||
# bot = CdrWhatsapp.check_token('token')
|
||||
#
|
||||
|
||||
def self.check_token(api_url, token)
|
||||
api = CdrWhatsappAPI.new(api_url, token)
|
||||
begin
|
||||
bot = api.fetch_self
|
||||
rescue StandardError => e
|
||||
raise "invalid api token: #{e.message}"
|
||||
end
|
||||
bot
|
||||
end
|
||||
|
||||
#
|
||||
# create or update channel, store bot attributes and verify token
|
||||
#
|
||||
# channel = CdrWhatsapp.create_or_update_channel('token', params)
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# channel # instance of Channel
|
||||
#
|
||||
|
||||
def self.create_or_update_channel(api_url, token, params, channel = nil)
|
||||
# verify token
|
||||
bot = CdrWhatsapp.check_token(api_url, token)
|
||||
|
||||
raise 'Bot already exists!' unless channel && CdrWhatsapp.bot_duplicate?(bot['id'])
|
||||
|
||||
raise 'Group needed!' if params[:group_id].blank?
|
||||
|
||||
group = Group.find_by(id: params[:group_id])
|
||||
raise 'Group invalid!' unless group
|
||||
|
||||
unless channel
|
||||
channel = CdrWhatsapp.bot_by_bot_id(bot['id'])
|
||||
channel ||= Channel.new
|
||||
end
|
||||
channel.area = 'Whatsapp::Account'
|
||||
channel.options = {
|
||||
adapter: 'cdr_whatsapp',
|
||||
bot: {
|
||||
id: bot['id'],
|
||||
number: bot['number']
|
||||
},
|
||||
api_token: token,
|
||||
api_url: api_url,
|
||||
welcome: params[:welcome]
|
||||
}
|
||||
channel.group_id = group.id
|
||||
channel.active = true
|
||||
channel.save!
|
||||
channel
|
||||
end
|
||||
|
||||
#
|
||||
# check if bot already exists as channel
|
||||
#
|
||||
# success = CdrWhatsapp.bot_duplicate?(bot_id)
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# channel # instance of Channel
|
||||
#
|
||||
|
||||
def self.bot_duplicate?(bot_id, channel_id = nil)
|
||||
Channel.where(area: 'Whatsapp::Account').each do |channel|
|
||||
next unless channel.options
|
||||
next unless channel.options[:bot]
|
||||
next unless channel.options[:bot][:id]
|
||||
next if channel.options[:bot][:id] != bot_id
|
||||
next if channel.id.to_s == channel_id.to_s
|
||||
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
#
|
||||
# get channel by bot_id
|
||||
#
|
||||
# channel = CdrWhatsapp.bot_by_bot_id(bot_id)
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# true|false
|
||||
#
|
||||
|
||||
def self.bot_by_bot_token(bot_token)
|
||||
Channel.where(area: 'Whatsapp::Account').each do |channel|
|
||||
next unless channel.options
|
||||
next unless channel.options[:bot_token]
|
||||
return channel if channel.options[:bot_token].to_s == bot_token.to_s
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
#
|
||||
# date = CdrWhatsapp.timestamp_to_date('1543414973285')
|
||||
#
|
||||
# returns
|
||||
#
|
||||
# 2018-11-28T14:22:53.285Z
|
||||
#
|
||||
|
||||
def self.timestamp_to_date(timestamp_str)
|
||||
Time.at(timestamp_str.to_i).utc.to_datetime
|
||||
end
|
||||
|
||||
def self.message_id(message_raw)
|
||||
format('%<source>s@%<timestamp>s', source: message_raw['source'], timestamp: message_raw['timestamp'])
|
||||
end
|
||||
|
||||
#
|
||||
# client = CdrWhatsapp.new('token')
|
||||
#
|
||||
|
||||
def initialize(api_url, token)
|
||||
@token = token
|
||||
@api_url = api_url
|
||||
@api = CdrWhatsappAPI.new(api_url, token)
|
||||
end
|
||||
|
||||
#
|
||||
# client.send_message(chat_id, 'some message')
|
||||
#
|
||||
|
||||
def send_message(recipient, message)
|
||||
return if Rails.env.test?
|
||||
|
||||
@api.send_message(recipient, message)
|
||||
end
|
||||
|
||||
def user(number)
|
||||
{
|
||||
# id: params[:message][:from][:id],
|
||||
id: number,
|
||||
username: number
|
||||
# first_name: params[:message][:from][:first_name],
|
||||
# last_name: params[:message][:from][:last_name]
|
||||
}
|
||||
end
|
||||
|
||||
def to_user(message)
|
||||
Rails.logger.debug { 'Create user from message...' }
|
||||
Rails.logger.debug { message.inspect }
|
||||
|
||||
# do message_user lookup
|
||||
message_user = user(message[:source])
|
||||
|
||||
# create or update user
|
||||
login = message_user[:username] || message_user[:id]
|
||||
|
||||
auth = Authorization.find_by(uid: message[:source], provider: 'whatsapp')
|
||||
|
||||
user_data = {
|
||||
login: login,
|
||||
mobile: message[:source]
|
||||
}
|
||||
|
||||
user = if auth
|
||||
User.find(auth.user_id)
|
||||
else
|
||||
User.where(mobile: message[:source]).order(:updated_at).first
|
||||
end
|
||||
if user
|
||||
user.update!(user_data)
|
||||
else
|
||||
user = User.create!(
|
||||
firstname: message[:source],
|
||||
mobile: message[:source],
|
||||
note: "Whatsapp #{message_user[:username]}",
|
||||
active: true,
|
||||
role_ids: Role.signup_role_ids
|
||||
)
|
||||
end
|
||||
|
||||
# create or update authorization
|
||||
auth_data = {
|
||||
uid: message_user[:id],
|
||||
username: login,
|
||||
user_id: user.id,
|
||||
provider: 'cdr_whatsapp'
|
||||
}
|
||||
if auth
|
||||
auth.update!(auth_data)
|
||||
else
|
||||
Authorization.create(auth_data)
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
def to_ticket(message, user, group_id, channel)
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
Rails.logger.debug { 'Create ticket from message...' }
|
||||
Rails.logger.debug { message.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { group_id.inspect }
|
||||
|
||||
# prepare title
|
||||
title = '-'
|
||||
title = message[:message][:body] unless message[:message][:body].nil?
|
||||
title = "#{title[0, 60]}..." if title.length > 60
|
||||
|
||||
# find ticket or create one
|
||||
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
|
||||
ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first
|
||||
if ticket
|
||||
|
||||
# check if title need to be updated
|
||||
ticket.title = title if ticket.title == '-'
|
||||
new_state = Ticket::State.find_by(default_create: true)
|
||||
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
|
||||
ticket.save!
|
||||
return ticket
|
||||
end
|
||||
|
||||
ticket = Ticket.new(
|
||||
group_id: group_id,
|
||||
title: title,
|
||||
state_id: Ticket::State.find_by(default_create: true).id,
|
||||
priority_id: Ticket::Priority.find_by(default_create: true).id,
|
||||
customer_id: user.id,
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
cdr_whatsapp: {
|
||||
bot_id: channel.options[:bot][:id],
|
||||
chat_id: message[:source]
|
||||
}
|
||||
}
|
||||
)
|
||||
ticket.save!
|
||||
ticket
|
||||
end
|
||||
|
||||
def to_article(message, user, ticket, channel)
|
||||
Rails.logger.debug { 'Create article from message...' }
|
||||
Rails.logger.debug { message.inspect }
|
||||
Rails.logger.debug { user.inspect }
|
||||
Rails.logger.debug { ticket.inspect }
|
||||
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
article = Ticket::Article.new(
|
||||
from: message[:source],
|
||||
to: channel[:options][:bot][:number],
|
||||
body: message[:message][:body],
|
||||
content_type: 'text/plain',
|
||||
message_id: "cdr_whatsapp.#{message[:id]}",
|
||||
ticket_id: ticket.id,
|
||||
type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id,
|
||||
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
|
||||
internal: false,
|
||||
preferences: {
|
||||
cdr_whatsapp: {
|
||||
timestamp: message[:timestamp],
|
||||
message_id: message[:id],
|
||||
from: message[:source]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# TODO: attachments
|
||||
# TODO voice
|
||||
# TODO emojis
|
||||
#
|
||||
if message[:message][:body]
|
||||
Rails.logger.debug { article.inspect }
|
||||
article.save!
|
||||
|
||||
Store.remove(
|
||||
object: 'Ticket::Article',
|
||||
o_id: article.id
|
||||
)
|
||||
|
||||
return article
|
||||
end
|
||||
raise 'invalid action'
|
||||
end
|
||||
|
||||
def to_group(message, group_id, channel)
|
||||
# begin import
|
||||
Rails.logger.debug { 'whatsapp import message' }
|
||||
|
||||
# TODO: handle messages in group chats
|
||||
|
||||
return if Ticket::Article.find_by(message_id: message[:id])
|
||||
|
||||
ticket = nil
|
||||
# use transaction
|
||||
Transaction.execute(reset_user_id: true) do
|
||||
user = to_user(message)
|
||||
ticket = to_ticket(message, user, group_id, channel)
|
||||
to_article(message, user, ticket, channel)
|
||||
end
|
||||
|
||||
ticket
|
||||
end
|
||||
|
||||
def from_article(article)
|
||||
# sends a message from a zammad article
|
||||
|
||||
Rails.logger.debug { "Create whatsapp message from article to '#{article[:to]}'..." }
|
||||
|
||||
@api.send_message(article[:to], article[:body])
|
||||
end
|
||||
end
|
||||
41
zammad-addon-metamigo/src/lib/cdr_whatsapp_api.rb
Normal file
41
zammad-addon-metamigo/src/lib/cdr_whatsapp_api.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
require 'net/http'
|
||||
require 'net/https'
|
||||
require 'uri'
|
||||
require 'rest-client'
|
||||
|
||||
class CdrWhatsappAPI
|
||||
def initialize(api_url, token)
|
||||
@token = token
|
||||
@last_update = 0
|
||||
@api_url = api_url
|
||||
end
|
||||
|
||||
def parse_hash(hash)
|
||||
ret = {}
|
||||
hash.map do |k, v|
|
||||
ret[k] = CGI.encode(v.to_s.gsub('\\\'', '\''))
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def get(api)
|
||||
url = "#{@api_url}/bots/#{@token}/#{api}"
|
||||
JSON.parse(RestClient.get(url, { accept: :json }).body)
|
||||
end
|
||||
|
||||
def post(api, params = {})
|
||||
url = "#{@api_url}/bots/#{@token}/#{api}"
|
||||
JSON.parse(RestClient.post(url, params, { accept: :json }).body)
|
||||
end
|
||||
|
||||
def fetch_self
|
||||
get('')
|
||||
end
|
||||
|
||||
def send_message(recipient, text, options = {})
|
||||
post('send', { phoneNumber: recipient.to_s, message: text }.merge(parse_hash(options)))
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<symbol id="icon-cdr-signal" viewBox="0 0 17 17"><title>signal</title>
|
||||
<defs><path id="a" d="M1 .41h.925v2.167H1z"/><path id="c" d="M.356 1h2.179v.745H.355z"/><path id="e" d="M.935.432h.921v2.167h-.92z"/><path id="g" d="M1 .202h.838V2.37H1z"/><path id="i" d="M1 .95h.856v2.165H.999z"/><path id="k" d="M.09.605H1.62V2H.09z"/></defs><g fill="none" fill-rule="evenodd"><path d="M15.255 7.872c0-3.422-2.988-6.196-6.675-6.196-3.686 0-6.717 2.774-6.717 6.196 0 1.925.59 3.806 2.18 4.913l.133 1.986a.327.327 0 0 0 .497.258c.703-.427 1.966-1.19 2.005-1.18 5.168 1.121 8.577-2.555 8.577-5.977" fill="#C6C7C8"/><g transform="translate(-1 5)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M1.842.555l-.4-.145a7.466 7.466 0 0 0-.434 2.145l.426.022c.036-.69.174-1.37.408-2.022" fill="#C6C7C8" mask="url(#b)"/></g><path d="M6.791.59L6.708.17A8.732 8.732 0 0 0 4.64.858l.183.385A8.358 8.358 0 0 1 6.792.59M4.272 1.533l-.213-.37a8.43 8.43 0 0 0-1.736 1.322l.3.302a8.05 8.05 0 0 1 1.649-1.254M2.199 3.243l-.324-.278A7.886 7.886 0 0 0 .69 4.801l.388.18a7.477 7.477 0 0 1 1.12-1.738" fill="#C6C7C8"/><g transform="translate(7 -1)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M2.484 1.489l.05-.424a9.026 9.026 0 0 0-2.178.002l.052.423a8.634 8.634 0 0 1 2.076-.001" fill="#C6C7C8" mask="url(#d)"/></g><path d="M16.18 4.822a7.854 7.854 0 0 0-1.171-1.845l-.325.276a7.45 7.45 0 0 1 1.107 1.745l.389-.176z" fill="#C6C7C8"/><g transform="translate(15 5)"><mask id="f" fill="#fff"><use xlink:href="#e"/></mask><path d="M1.42 2.599l.427-.021A7.568 7.568 0 0 0 1.425.432l-.402.143c.23.652.364 1.333.397 2.024" fill="#C6C7C8" mask="url(#f)"/></g><path d="M14.564 2.493a8.426 8.426 0 0 0-1.731-1.33l-.214.37c.604.35 1.157.774 1.643 1.262l.302-.302zM12.252.857a8.653 8.653 0 0 0-2.07-.688L10.1.587a8.23 8.23 0 0 1 1.966.654l.186-.384zM12.98 14.214l.229.361a7.813 7.813 0 0 0 1.668-1.41l-.318-.284a7.376 7.376 0 0 1-1.578 1.333" fill="#C6C7C8"/><g transform="translate(-1 8)"><mask id="h" fill="#fff"><use xlink:href="#g"/></mask><path d="M1.426.202L1 .21c.016.773.104 1.499.261 2.16l.415-.1a9.917 9.917 0 0 1-.25-2.068" fill="#C6C7C8" mask="url(#h)"/></g><path d="M10.495 15.233l.095.415a8.604 8.604 0 0 0 2.049-.746l-.198-.378c-.6.312-1.257.55-1.947.709M8.314 15.47h-.001c-.17 0-.343-.004-.516-.012l-.02.426a10.49 10.49 0 0 0 2.169-.112l-.066-.422c-.503.08-1.03.12-1.566.12" fill="#C6C7C8"/><g transform="translate(15 7)"><mask id="j" fill="#fff"><use xlink:href="#i"/></mask><path d="M1.43.95c0 .692-.098 1.38-.288 2.048l.41.117A7.892 7.892 0 0 0 1.856.95H1.43z" fill="#C6C7C8" mask="url(#j)"/></g><path d="M14.953 12.4l.34.255a7.809 7.809 0 0 0 1.053-1.916l-.4-.152a7.374 7.374 0 0 1-.993 1.813M.44 11.002a6.393 6.393 0 0 0 1.016 1.946l.337-.26a5.979 5.979 0 0 1-.948-1.819l-.405.133zM6.653 15.353c-.058 0-.082 0-1.604.624l.163.395a48.799 48.799 0 0 1 1.46-.588c.152.021.306.038.457.053l.042-.424a13.84 13.84 0 0 1-.49-.058c-.008-.002-.018-.002-.028-.002M2.94 13.828a.215.215 0 0 0-.078-.109 5.416 5.416 0 0 1-.665-.57l-.304.3c.204.206.426.4.66.573l.346 1.1.407-.129-.366-1.165z" fill="#C6C7C8"/><g transform="translate(3 15)"><mask id="l" fill="#fff"><use xlink:href="#k"/></mask><path d="M.779 1.5L.498.606.09.732.44 1.85A.219.219 0 0 0 .645 2a.203.203 0 0 0 .08-.017l.894-.368-.163-.395-.677.28" fill="#C6C7C8" mask="url(#l)"/></g></g>
|
||||
</symbol>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<symbol id="icon-cdr-whatsapp" viewBox="0 0 17 17"><title>whatsapp</title>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g><g transform="translate(0.000000,462.000000) scale(0.100000,-0.100000)"><path d="M4472.9,4492.4C2377.1,4206.7,746.1,2682.3,347.4,637.7c-198.3-1025.5-51.2-2070.2,428.5-3036l172.7-343.2L520.1-4005.9c-236.7-692.9-424.3-1266.4-420-1270.7c4.3-4.3,601.2,179.1,1324,409.4l1317.6,420l251.6-119.4c1029.8-481.8,2172.6-599.1,3259.9-330.5c1669.4,413.6,3012.6,1709.9,3475.3,3355.8c194,695,226,1522.3,83.2,2228c-328.3,1618.2-1483.9,2963.6-3048.8,3545.6c-236.7,89.6-622.6,189.8-914.7,240.9C5568.8,4520.1,4760.7,4532.9,4472.9,4492.4z M5666.9,3722.8c1571.3-213.2,2906-1383.7,3326-2918.8c179.1-654.6,187.6-1385.8,23.4-2034c-336.9-1326.1-1341-2402.8-2645.9-2835.6c-1115.1-371-2351.7-243-3343.1,347.5c-91.7,53.3-174.8,98.1-183.4,98.1c-6.4,0-356.1-108.8-776.1-243.1c-417.9-132.2-761.1-234.5-761.1-223.9c0,8.5,110.9,343.3,247.3,742l247.3,724.9l-110.9,166.3c-138.6,208.9-362.5,663.1-445.6,904C594.7,334.9,1453.9,2420.1,3257.6,3332.6C4001.7,3707.8,4809.8,3837.9,5666.9,3722.8z"/><path d="M3272.6,1878.5c-168.4-83.1-400.8-407.2-486.1-678c-17.1-53.3-38.4-194-44.8-311.3c-14.9-272.9,38.4-509.6,185.5-816.6c108.7-226,360.3-605.5,609.8-916.8c437.1-547.9,976.5-995.7,1488.2-1240.9c398.7-189.8,1012.7-392.3,1272.8-420c356.1-36.3,897.6,200.4,1087.4,477.6c123.7,179.1,191.9,618.3,108.7,695c-42.6,38.4-850.7,432.8-1010.6,494.6c-55.4,21.3-123.7,32-151.4,23.4c-32-6.4-115.1-91.7-209-208.9C5892.9-1313.1,5779.9-1424,5718-1424c-83.1,0-496.8,200.4-729.1,353.9c-296.4,196.2-678,579.9-884.8,889.1c-85.3,125.8-153.5,253.7-153.5,281.4c0,29.9,55.4,113,149.2,221.7c136.4,159.9,255.8,349.6,255.8,409.4c0,12.8-98.1,260.1-217.5,550.1c-140.7,336.9-238.8,545.8-275,584.2c-55.4,55.4-66.1,57.6-279.3,57.6C3402.6,1923.3,3347.2,1914.8,3272.6,1878.5z"/></g></g>
|
||||
</svg>
|
||||
</symbol>
|
||||
Loading…
Add table
Add a link
Reference in a new issue