Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fca2bb65f | ||
|
|
e15ef5d007 | ||
| 885a043e2b | |||
|
|
dc9128d712 | ||
|
|
89b477888a | ||
|
|
c12374f86c | ||
|
|
f683496318 | ||
|
|
18821dd951 | ||
|
|
3c767bb14b | ||
|
|
d0c3f29ca6 | ||
|
|
8ef2d9d07d | ||
|
|
dc8fd06c2f | ||
|
|
b725c8bca3 | ||
|
|
54ed4cf307 | ||
|
|
b50099feb6 | ||
|
|
2e9378500c | ||
|
|
4ee78e69e1 | ||
|
|
c7bde09ad4 | ||
|
|
05f521d78b | ||
|
|
95767ee0d5 | ||
|
|
45323b2d3d | ||
|
|
58f5e13eb3 | ||
|
|
a6fda095d8 | ||
|
|
372f644995 | ||
|
|
c75b6249c1 | ||
|
|
f9911f6cf3 | ||
|
|
92e2ea2337 | ||
|
|
438e674eff | ||
|
|
5b74cf2dc5 | ||
|
|
4f5d937265 | ||
|
|
544dc770cd | ||
|
|
5fbe257625 | ||
|
|
e60c5819e2 | ||
|
|
04765868db | ||
|
|
cb50d4b8f8 | ||
|
|
f2e6c12af4 | ||
|
|
308db818da | ||
|
|
541f11af6d | ||
|
|
6ff00625a3 | ||
|
|
26b11ff555 | ||
|
|
243f0d7a8a | ||
|
|
a335622fc0 | ||
|
|
b3ed8e91e9 | ||
|
|
7256d5678c | ||
|
|
2d5268bb44 | ||
|
|
1d11e71fad | ||
|
|
5e63030326 | ||
|
|
00dac17306 | ||
|
|
c2d8a916e5 | ||
|
|
12d86da784 | ||
|
|
1be77b71e8 | ||
|
|
b67007bc13 | ||
|
|
ef2c979fba | ||
|
|
30fc6ddaae | ||
|
|
c7cf436740 | ||
|
|
9cb575760d | ||
|
|
f2e5ed5aeb | ||
|
|
df99000b6c | ||
|
|
fe417b50f9 | ||
|
|
c8b27bea81 | ||
|
|
c7f6a7f984 | ||
|
|
1cdde02248 | ||
|
|
1c177010e2 | ||
|
|
55c85c74b2 | ||
|
|
6fec6acb73 | ||
|
|
74ac4c4a8a | ||
|
|
95ddb1f9e9 | ||
|
|
2d4376bc25 | ||
|
|
bf148ef154 | ||
|
|
70a85dde88 | ||
|
|
88a7abba5d | ||
|
|
fe95e77a68 | ||
|
|
5d55e16d93 | ||
|
|
a012dd66a9 | ||
|
|
5f65fb1a9d | ||
|
|
326caf7249 | ||
|
|
892af180c2 | ||
|
|
baf10252d5 | ||
|
|
10357ea4a7 | ||
|
|
5bd8e55953 |
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
env
|
||||||
|
src/plugins/
|
||||||
|
.wwebjs_auth
|
||||||
|
.wwebjs_cache
|
||||||
|
.claude
|
||||||
|
downloads
|
||||||
|
src/node_modules/
|
||||||
|
node_modules/
|
||||||
|
cookies.txt
|
||||||
|
bin/
|
||||||
|
mychats.txt
|
||||||
|
manybot.conf
|
||||||
|
update.log
|
||||||
|
logs/audio-error.log
|
||||||
|
logs/video-error.log
|
||||||
|
registry.json
|
||||||
|
|
||||||
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Install dependencies for Puppeteer/Chrome
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
chromium \
|
||||||
|
chromium-sandbox \
|
||||||
|
fonts-ipafont-gothic \
|
||||||
|
fonts-wqy-zenhei \
|
||||||
|
fonts-thai-tlwg \
|
||||||
|
fonts-kacst \
|
||||||
|
fonts-freefont-ttf \
|
||||||
|
libxss1 \
|
||||||
|
--no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set environment variables for Puppeteer
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||||
|
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||||
|
|
||||||
|
# Set terminal to UTF-8 to suport QR Code characters
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
ENV LC_ALL=C.UTF-8
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p session logs
|
||||||
|
|
||||||
|
CMD ["node", "src/main.js"]
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
194
README.md
Normal file
194
README.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Bot para WhatsApp 100% local, sem API oficial</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="#-recursos">Recursos</a> .
|
||||||
|
<a href="#-instalação-rápida">Instalação</a> .
|
||||||
|
<a href="#-uso">Uso</a> .
|
||||||
|
<a href="#-plugins">Plugins</a> .
|
||||||
|
<a href="#-documentação">Documentação</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
🇧🇷 Português · <a href="README_EN.md">🇺🇸 English</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white" alt="Node.js 18+">
|
||||||
|
<img src="https://img.shields.io/badge/npm-9+-CB3837?logo=npm&logoColor=white" alt="npm 9+">
|
||||||
|
<img src="https://img.shields.io/badge/License-GPL--v3-blue.svg" alt="License: GPL v3">
|
||||||
|
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20Windows-lightgrey" alt="Platform">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/whatsapp--web.js-%2325D366?logo=whatsapp&logoColor=white" alt="whatsapp-web.js">
|
||||||
|
<img src="https://img.shields.io/badge/headless-Automated-green" alt="Headless">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
> **Versão Oficial Online**
|
||||||
|
> Quer usar o ManyBot sem instalar? Adicione o bot oficial:
|
||||||
|
>
|
||||||
|
> **+55 (16) 99459-1903**
|
||||||
|
>
|
||||||
|
> Online 24h (quando possível) - Disponibilidade não garantida
|
||||||
|
>
|
||||||
|
> Ao adicionar, você concorda com os [Termos de Uso](TERMOS_pt-br.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recursos
|
||||||
|
|
||||||
|
- **100% Local** - Sem depender da API oficial do WhatsApp
|
||||||
|
- **Multi-chat** - Suporte a múltiplos chats em uma única sessão
|
||||||
|
- **Sistema de Plugins** - Adicione, remova ou crie funcionalidades sem mexer no núcleo
|
||||||
|
- **Headless** - Funciona em segundo plano sem interface gráfica
|
||||||
|
- **Fácil Configuração** - Arquivo de config simples e intuitivo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Instalação Rápida
|
||||||
|
|
||||||
|
### Opção 1: Usar o Bot Oficial (Sem instalar)
|
||||||
|
|
||||||
|
Adicione o número **+55 (16) 99459-1903** aos seus contatos e envie `!many` para ver os comandos disponíveis.
|
||||||
|
|
||||||
|
**Status:** 🟢 Online (24h quando possível, mas sem garantia)
|
||||||
|
|
||||||
|
> ⚠️ **Importante:** Ao usar o bot oficial, você concorda com os [Termos de Uso](TERMOS_pt-br.md). Leia antes de adicionar!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Opção 2: Instalar sua Própria Versão
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone o repositório
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
|
||||||
|
# 2. Crie o arquivo de configuração
|
||||||
|
cp manybot.conf.example manybot.conf
|
||||||
|
|
||||||
|
# 3. Configure conforme sua necessidade (veja a documentação)
|
||||||
|
nano manybot.conf
|
||||||
|
|
||||||
|
# 4. Execute o script de instalação
|
||||||
|
bash ./setup
|
||||||
|
|
||||||
|
# 5. Rode o bot
|
||||||
|
node ./src/main.js
|
||||||
|
```
|
||||||
|
|
||||||
|
📱 **Escaneie o QR Code** no WhatsApp: Menu → Dispositivos conectados → Conectar um dispositivo
|
||||||
|
|
||||||
|
> **⚡ Pronto!** Veja a [documentação completa](docs/INSTALACAO.md) para mais detalhes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Uso
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Iniciar o bot
|
||||||
|
node ./src/main.js
|
||||||
|
|
||||||
|
# Atualizar para a versão mais recente
|
||||||
|
bash ./update
|
||||||
|
|
||||||
|
# Descobrir IDs de chats
|
||||||
|
node src/utils/get_id.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Plugins
|
||||||
|
|
||||||
|
O ManyBot é construído em torno de um sistema de plugins. O kernel apenas conecta ao WhatsApp e distribui as mensagens — os plugins decidem o que fazer.
|
||||||
|
|
||||||
|
### Gerenciando Plugins com ManyPlug
|
||||||
|
|
||||||
|
Instale e gerencie plugins usando o **ManyPlug CLI**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalar o gerenciador
|
||||||
|
npm install -g @freakk.dev/manyplug
|
||||||
|
|
||||||
|
# Criar um novo plugin
|
||||||
|
cd src/plugins
|
||||||
|
manyplug init meu-plugin --category utility
|
||||||
|
|
||||||
|
# Instalar de outro diretório
|
||||||
|
manyplug install --local ../outro-plugin
|
||||||
|
|
||||||
|
# Listar plugins instalados
|
||||||
|
manyplug list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Criar um Plugin
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/meu-plugin/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "oi")) return;
|
||||||
|
await msg.reply("Olá! 👋");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Veja mais na [documentação de plugins](docs/PLUGINS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentação
|
||||||
|
|
||||||
|
- [📥 Instalação Completa](docs/INSTALACAO.md) — Linux, Windows, Termux
|
||||||
|
- [⚙️ Configuração](docs/CONFIGURACAO.md) — Todas as opções do `manybot.conf`
|
||||||
|
- [🔌 Criando Plugins](docs/PLUGINS.md) — Guia completo de desenvolvimento
|
||||||
|
- [🛠️ API de Plugins](docs/API.md) — Referência de objetos `msg` e `api`
|
||||||
|
|
||||||
|
## 🌍 Internacionalização
|
||||||
|
|
||||||
|
O ManyBot suporta múltiplos idiomas. Configure no `manybot.conf`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LANGUAGE=pt # Português
|
||||||
|
LANGUAGE=en # English
|
||||||
|
LANGUAGE=es # Español
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Padrão:** Inglês (`en`)
|
||||||
|
- **Fallback:** Se o idioma selecionado não existir, o bot usa inglês
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Requisitos
|
||||||
|
|
||||||
|
- **Node.js** 18+
|
||||||
|
- **NPM** 9+
|
||||||
|
- **Linux** ou **Windows** (via Git Bash)
|
||||||
|
|
||||||
|
> ⚠️ Android/iOS e Termux têm suporte experimental sem garantias.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Licença
|
||||||
|
|
||||||
|
Distribuído sob a licença **GPLv3**. Veja [LICENSE](LICENSE) para mais detalhes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**[⬆ Voltar ao topo](#)**
|
||||||
|
|
||||||
|
</div>
|
||||||
194
README_EN.md
Normal file
194
README_EN.md
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>100% Local WhatsApp Bot, no official API</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="#-features">Features</a> •
|
||||||
|
<a href="#-quick-install">Install</a> •
|
||||||
|
<a href="#-usage">Usage</a> •
|
||||||
|
<a href="#-plugins">Plugins</a> •
|
||||||
|
<a href="#-documentation">Documentation</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="README.md">🇧🇷 Português</a> · <a href="README_EN.md">🇺🇸 English</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white" alt="Node.js 18+">
|
||||||
|
<img src="https://img.shields.io/badge/npm-9+-CB3837?logo=npm&logoColor=white" alt="npm 9+">
|
||||||
|
<img src="https://img.shields.io/badge/License-GPL--v3-blue.svg" alt="License: GPL v3">
|
||||||
|
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20Windows-lightgrey" alt="Platform">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/whatsapp--web.js-%2325D366?logo=whatsapp&logoColor=white" alt="whatsapp-web.js">
|
||||||
|
<img src="https://img.shields.io/badge/headless-Automated-green" alt="Headless">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
> 🟢 **Official Instance Online**
|
||||||
|
> Want to use ManyBot without installing? Add the official bot:
|
||||||
|
>
|
||||||
|
> **+55 (16) 99459-1903**
|
||||||
|
>
|
||||||
|
> Online 24h (when possible) · Availability not guaranteed
|
||||||
|
>
|
||||||
|
> By adding, you agree to the [Terms of Use](TERMS_en-us.md)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **100% Local** — No dependency on the official WhatsApp API
|
||||||
|
- **Multi-chat** — Support for multiple chats in a single session
|
||||||
|
- **Plugin System** — Add, remove, or create features without touching the core
|
||||||
|
- **Headless** — Runs in the background without a GUI
|
||||||
|
- **Easy Configuration** — Simple and intuitive config file
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Install
|
||||||
|
|
||||||
|
### Option 1: Use the Official Bot (No install)
|
||||||
|
|
||||||
|
Add the number **+55 (16) 99459-1903** to your contacts and send `!many` to see available commands.
|
||||||
|
|
||||||
|
**Status:** 🟢 Online (24h when possible, but no guarantee)
|
||||||
|
|
||||||
|
> ⚠️ **Important:** By using the official bot, you agree to the [Terms of Use](TERMS_en-us.md). Read before adding!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Install Your Own Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repository
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
|
||||||
|
# 2. Create the config file
|
||||||
|
cp manybot.conf.example manybot.conf
|
||||||
|
|
||||||
|
# 3. Configure as needed (see documentation)
|
||||||
|
nano manybot.conf
|
||||||
|
|
||||||
|
# 4. Run the install script
|
||||||
|
bash ./setup
|
||||||
|
|
||||||
|
# 5. Run the bot
|
||||||
|
node ./src/main.js
|
||||||
|
```
|
||||||
|
|
||||||
|
📱 **Scan the QR Code** on WhatsApp: Menu → Linked Devices → Link a Device
|
||||||
|
|
||||||
|
> **⚡ Done!** See the [full documentation](docs/INSTALLATION.md) for more details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the bot
|
||||||
|
node ./src/main.js
|
||||||
|
|
||||||
|
# Update to the latest version
|
||||||
|
bash ./update
|
||||||
|
|
||||||
|
# Discover chat IDs
|
||||||
|
node src/utils/get_id.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Plugins
|
||||||
|
|
||||||
|
ManyBot is built around a plugin system. The kernel only connects to WhatsApp and distributes messages — plugins decide what to do.
|
||||||
|
|
||||||
|
### Managing Plugins with ManyPlug
|
||||||
|
|
||||||
|
Install and manage plugins using the **ManyPlug CLI**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the plugin manager
|
||||||
|
npm install -g @freakk.dev/manyplug
|
||||||
|
|
||||||
|
# Create a new plugin
|
||||||
|
cd src/plugins
|
||||||
|
manyplug init my-plugin --category utility
|
||||||
|
|
||||||
|
# Install from another directory
|
||||||
|
manyplug install --local ../another-plugin
|
||||||
|
|
||||||
|
# List installed plugins
|
||||||
|
manyplug list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a Plugin
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/my-plugin/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "hello")) return;
|
||||||
|
await msg.reply("Hello! 👋");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See more in the [plugin documentation](docs/PLUGINS_EN.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
- [📥 Full Installation](docs/INSTALLATION.md) — Linux, Windows, Termux
|
||||||
|
- [⚙️ Configuration](docs/CONFIGURATION.md) — All `manybot.conf` options
|
||||||
|
- [🔌 Creating Plugins](docs/PLUGINS.md) — Complete development guide
|
||||||
|
- [🛠️ Plugin API](docs/API.md) — Reference for `msg` and `api` objects
|
||||||
|
|
||||||
|
## 🌍 Internationalization
|
||||||
|
|
||||||
|
ManyBot supports multiple languages. Configure in `manybot.conf`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LANGUAGE=pt # Português
|
||||||
|
LANGUAGE=en # English
|
||||||
|
LANGUAGE=es # Español
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Default:** English (`en`)
|
||||||
|
- **Fallback:** If selected language doesn't exist, bot falls back to English
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
- **Node.js** 18+
|
||||||
|
- **NPM** 9+
|
||||||
|
- **Linux** or **Windows** (via Git Bash)
|
||||||
|
|
||||||
|
> ⚠️ Android/iOS and Termux have experimental support with no guarantees.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
Distributed under the **GPLv3** license. See [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**[⬆ Back to top](#)**
|
||||||
|
|
||||||
|
</div>
|
||||||
219
TERMOS_pt-br.md
Normal file
219
TERMOS_pt-br.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
## Termos de Uso
|
||||||
|
|
||||||
|
> Este arquivo poderá ser movido no futuro, assim que o site oficial for lançado.
|
||||||
|
|
||||||
|
### 1. Natureza do Software
|
||||||
|
|
||||||
|
Este projeto é uma ferramenta de automação de código aberto para plataformas de mensagens.
|
||||||
|
|
||||||
|
Por padrão, o sistema principal não gera mídia nem conteúdo. No entanto, ele pode suportar plug-ins, extensões ou modificações que adicionem tais funcionalidades (incluindo a geração de figurinhas ou mídia).
|
||||||
|
|
||||||
|
Ele pode ser usado:
|
||||||
|
|
||||||
|
* Como uma instância auto-hospedada (local)
|
||||||
|
* Por meio de uma instância hospedada oficialmente operada pelo desenvolvedor
|
||||||
|
|
||||||
|
O desenvolvedor fornece este software estritamente como uma ferramenta técnica e não cria, controla ou endossa conteúdo gerado pelo usuário.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Âmbito de Responsabilidade
|
||||||
|
|
||||||
|
#### 2.1 Instância Hospedada Oficialmente
|
||||||
|
|
||||||
|
Ao usar a instância oficial:
|
||||||
|
|
||||||
|
* Mensagens e mídia podem passar pela infraestrutura do desenvolvedor
|
||||||
|
* O conteúdo é processado automaticamente e não é monitorado ativamente
|
||||||
|
* Os dados são processados e, em seguida, descartados permanentemente
|
||||||
|
|
||||||
|
O desenvolvedor não assume responsabilidade por qualquer conteúdo processado pelo sistema.
|
||||||
|
|
||||||
|
#### 2.2 Versões auto-hospedadas / modificadas
|
||||||
|
|
||||||
|
Se você executar, modificar ou distribuir sua própria versão:
|
||||||
|
|
||||||
|
* Você é totalmente responsável pela sua instância
|
||||||
|
* O desenvolvedor não tem controle sobre sua implantação
|
||||||
|
* Quaisquer plug-ins, modificações ou extensões são de sua responsabilidade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Responsabilidade do usuário
|
||||||
|
|
||||||
|
Ao utilizar este software, você concorda que:
|
||||||
|
|
||||||
|
* Você é o único responsável por todo o conteúdo que gerar, processar ou compartilhar
|
||||||
|
* Você cumprirá todas as leis e regulamentos aplicáveis
|
||||||
|
* Você não utilizará este software para fins ilegais, prejudiciais ou abusivos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Usos proibidos
|
||||||
|
|
||||||
|
Os seguintes usos são estritamente proibidos:
|
||||||
|
|
||||||
|
* Atividades ilegais de qualquer tipo
|
||||||
|
* Conteúdo que viole as leis aplicáveis
|
||||||
|
* Conteúdo abusivo, prejudicial ou explorador
|
||||||
|
* Uso indevido da plataforma para prejudicar terceiros
|
||||||
|
|
||||||
|
O desenvolvedor não tolera tal uso, mesmo que não seja possível evitá-lo totalmente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Isenção de Responsabilidade
|
||||||
|
|
||||||
|
O desenvolvedor não se responsabiliza por:
|
||||||
|
|
||||||
|
* Qualquer conteúdo gerado pelos usuários
|
||||||
|
* Qualquer uso indevido do software
|
||||||
|
* Quaisquer danos ou consequências legais resultantes do uso
|
||||||
|
|
||||||
|
Isso inclui, mas não se limita a:
|
||||||
|
|
||||||
|
* Conteúdo ofensivo ou inadequado
|
||||||
|
* Conteúdo ilegal criado ou distribuído pelos usuários
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Ausência de monitoramento ativo
|
||||||
|
|
||||||
|
O sistema opera automaticamente e não é monitorado ativamente o tempo todo.
|
||||||
|
|
||||||
|
O desenvolvedor não é obrigado a revisar todas as atividades, mas pode tomar medidas quando for relatado ou detectado uso indevido.
|
||||||
|
|
||||||
|
O tempo de resposta não é garantido.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Denúncias e aplicação de medidas
|
||||||
|
|
||||||
|
Os usuários podem denunciar o uso indevido por meio do canal de contato fornecido (por exemplo, e-mail).
|
||||||
|
|
||||||
|
Ao receber denúncias válidas, o desenvolvedor pode, a seu exclusivo critério:
|
||||||
|
|
||||||
|
* Bloquear usuários
|
||||||
|
* Remover o bot de grupos
|
||||||
|
* Restringir o acesso ao serviço
|
||||||
|
|
||||||
|
Com exceção de instâncias auto-hospedadas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Tratamento de dados e privacidade
|
||||||
|
|
||||||
|
* Mensagens e arquivos de mídia podem ser processados temporariamente
|
||||||
|
* O conteúdo não é armazenado permanentemente pelo sistema principal
|
||||||
|
* Os registros podem incluir metadados e/ou conteúdo para fins operacionais
|
||||||
|
* Alguns plug-ins ou modificações de terceiros podem armazenar dados de forma independente
|
||||||
|
|
||||||
|
O desenvolvedor não se responsabiliza pelo tratamento de dados realizado por plug-ins de terceiros ou versões modificadas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Serviços de terceiros
|
||||||
|
|
||||||
|
Este software depende de plataformas e bibliotecas de terceiros (por exemplo, ferramentas de automação do WhatsApp Web).
|
||||||
|
|
||||||
|
O uso deste software pode violar os termos de serviço dessas plataformas.
|
||||||
|
|
||||||
|
O desenvolvedor não se responsabiliza por:
|
||||||
|
|
||||||
|
* Banimentos de conta
|
||||||
|
* Restrições de serviço
|
||||||
|
* Quaisquer consequências impostas por plataformas de terceiros
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Plug-ins e modificações
|
||||||
|
|
||||||
|
Este software suporta plug-ins e permite forks do código-fonte.
|
||||||
|
|
||||||
|
Esses plug-ins podem introduzir recursos, incluindo, mas não se limitando a, geração de mídia ou figurinhas.
|
||||||
|
|
||||||
|
O desenvolvedor:
|
||||||
|
|
||||||
|
* Analisa o código-fonte de todos os plug-ins no repositório oficial
|
||||||
|
* Não analisa bifurcações, versões de terceiros e repositórios
|
||||||
|
* Aprova apenas plug-ins que concordem com os termos
|
||||||
|
|
||||||
|
O uso de plug-ins e forks de terceiros é de sua responsabilidade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Uso comercial
|
||||||
|
|
||||||
|
Este software é fornecido como freeware (software gratuito).
|
||||||
|
|
||||||
|
O uso comercial não é permitido, a menos que o software seja significativamente modificado e renomeado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Marca e Identidade
|
||||||
|
|
||||||
|
O nome do projeto, a identidade e os elementos associados são de uso exclusivo.
|
||||||
|
|
||||||
|
É proibido o uso não autorizado da identidade, da marca ou da representação do projeto como uma instância oficial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Disponibilidade do Serviço oficial
|
||||||
|
|
||||||
|
O serviço hospedado oficial pode:
|
||||||
|
|
||||||
|
* Estar indisponível a qualquer momento
|
||||||
|
* Conter bugs ou erros
|
||||||
|
* Ser modificado ou descontinuado sem aviso prévio
|
||||||
|
|
||||||
|
Não são fornecidas garantias de tempo de atividade ou confiabilidade.
|
||||||
|
|
||||||
|
Alertas de alterações e manutenção são enviados para os chats onde o bot está instalado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. Cessação do Acesso
|
||||||
|
|
||||||
|
O desenvolvedor reserva-se o direito de:
|
||||||
|
|
||||||
|
* Bloquear usuários
|
||||||
|
* Negar acesso
|
||||||
|
* Desativar o serviço a qualquer momento
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. Ausência de Garantia
|
||||||
|
|
||||||
|
Este software é fornecido “no estado em que se encontra”, sem qualquer tipo de garantia.
|
||||||
|
|
||||||
|
Não são oferecidas garantias quanto a:
|
||||||
|
|
||||||
|
* Funcionalidade
|
||||||
|
* Disponibilidade
|
||||||
|
* Segurança
|
||||||
|
* Adequação a qualquer finalidade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Aceitação dos Termos
|
||||||
|
|
||||||
|
Ao utilizar este software ou qualquer instância oficial, você concorda com estes Termos de Uso.
|
||||||
|
|
||||||
|
Caso não concorde, você não deve utilizar o software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. Alterações aos Termos
|
||||||
|
|
||||||
|
Estes termos podem ser atualizados a qualquer momento sem aviso prévio.
|
||||||
|
|
||||||
|
O uso continuado do software implica a aceitação dos termos atualizados, mesmo em versões antigas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. Jurisdição
|
||||||
|
|
||||||
|
Estes termos serão regidos e interpretados de acordo com as leis do Brasil.
|
||||||
|
|
||||||
|
Quaisquer disputas decorrentes do uso deste software estarão sujeitas à jurisdição aplicável no Brasil.
|
||||||
|
|
||||||
218
TERMS_en-us.md
Normal file
218
TERMS_en-us.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
## Terms of Use
|
||||||
|
|
||||||
|
> This is file may be moved in the future as the official webpage is launched.
|
||||||
|
|
||||||
|
### 1. Nature of the Software
|
||||||
|
|
||||||
|
This project is an open-source automation tool for messaging platforms.
|
||||||
|
|
||||||
|
By default, the core system does not generate media or content. However, it may support plugins, extensions, or modifications that add such functionality (including sticker or media generation).
|
||||||
|
|
||||||
|
It may be used:
|
||||||
|
|
||||||
|
* As a self-hosted (local) instance
|
||||||
|
* Through an official hosted instance operated by the developer
|
||||||
|
|
||||||
|
The developer provides this software strictly as a technical tool and does not create, control, or endorse user-generated content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Scope of Responsibility
|
||||||
|
|
||||||
|
#### 2.1 Official Hosted Instance
|
||||||
|
|
||||||
|
When using the official hosted bot:
|
||||||
|
|
||||||
|
* Messages and media may pass through the developer's infrastructure
|
||||||
|
* Content is processed automatically and is not actively monitored
|
||||||
|
* Data is processed and then permanently discarded
|
||||||
|
|
||||||
|
The developer does not assume responsibility for any content processed by the system.
|
||||||
|
|
||||||
|
#### 2.2 Self-Hosted / Modified Versions
|
||||||
|
|
||||||
|
If you run, modify, or distribute your own version:
|
||||||
|
|
||||||
|
* You are fully responsible for your instance
|
||||||
|
* The developer has no control over your deployment
|
||||||
|
* Any plugins, modifications, or extensions are your responsibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. User Responsibility
|
||||||
|
|
||||||
|
By using this software, you agree that:
|
||||||
|
|
||||||
|
* You are solely responsible for all content you generate, process, or share
|
||||||
|
* You will comply with all applicable laws and regulations
|
||||||
|
* You will not use this software for illegal, harmful, or abusive purposes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Prohibited Use
|
||||||
|
|
||||||
|
The following uses are strictly prohibited:
|
||||||
|
|
||||||
|
* Illegal activities of any kind
|
||||||
|
* Content that violates applicable laws
|
||||||
|
* Abusive, harmful, or exploitative content
|
||||||
|
* Misuse of the platform to harm others
|
||||||
|
|
||||||
|
The developer does not tolerate such use, even if it cannot be fully prevented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. No Liability
|
||||||
|
|
||||||
|
The developer shall not be held liable for:
|
||||||
|
|
||||||
|
* Any content generated by users
|
||||||
|
* Any misuse of the software
|
||||||
|
* Any damages or legal consequences resulting from usage
|
||||||
|
|
||||||
|
This includes, but is not limited to:
|
||||||
|
|
||||||
|
* Offensive or inappropriate content
|
||||||
|
* Illegal content created or distributed by users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. No Active Monitoring
|
||||||
|
|
||||||
|
The system operates automatically and is not actively monitored at all times.
|
||||||
|
|
||||||
|
The developer is not required to review all activity but may take action when misuse is reported or detected.
|
||||||
|
|
||||||
|
Response time is not guaranteed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Reporting and Enforcement
|
||||||
|
|
||||||
|
Users may report misuse through the provided contact channel (e.g., email).
|
||||||
|
|
||||||
|
Upon receiving valid reports, the developer may, at their sole discretion:
|
||||||
|
|
||||||
|
* Block users
|
||||||
|
* Remove the bot from groups
|
||||||
|
* Restrict access to the service
|
||||||
|
|
||||||
|
Except for self-hosted instances.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Data Handling and Privacy
|
||||||
|
|
||||||
|
* Messages and media may be temporarily processed
|
||||||
|
* Content is not permanently stored by the core system
|
||||||
|
* Logs may include metadata and/or content for operational purposes
|
||||||
|
* Some plugins or third-party modifications may store data independently
|
||||||
|
|
||||||
|
The developer is not responsible for data handling performed by third-party plugins or modified versions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Third-Party Services
|
||||||
|
|
||||||
|
This software depends on third-party platforms and libraries (e.g., WhatsApp Web automation tools).
|
||||||
|
|
||||||
|
Use of this software may violate the terms of service of such platforms.
|
||||||
|
|
||||||
|
The developer is not responsible for:
|
||||||
|
|
||||||
|
* Account bans
|
||||||
|
* Service restrictions
|
||||||
|
* Any consequences imposed by third-party platforms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Plugins and Modifications
|
||||||
|
|
||||||
|
This software supports plugins and allows forks fo the source code.
|
||||||
|
|
||||||
|
Such plugins may introduce features including, but not limited to, media or sticker generation.
|
||||||
|
|
||||||
|
The developer:
|
||||||
|
|
||||||
|
* Reviews the source code of all plugins on the official repository
|
||||||
|
* Does not reviews forks and third-party versions and repositories
|
||||||
|
* Approves only plugins who agree with the terms
|
||||||
|
|
||||||
|
The use of plugins and third-party forks is your responsibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Commercial Use
|
||||||
|
|
||||||
|
This software is provided as freeware.
|
||||||
|
|
||||||
|
Commercial use is not permitted unless the software is significantly modified and rebranded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Branding and Identity
|
||||||
|
|
||||||
|
The project name, identity, and associated elements are reserved.
|
||||||
|
|
||||||
|
Unauthorized use of the project's identity, branding, or representation as an official instance is prohibited.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Service Availability
|
||||||
|
|
||||||
|
The official hosted service may:
|
||||||
|
|
||||||
|
* Be unavailable at any time
|
||||||
|
* Contain bugs or errors
|
||||||
|
* Be modified or discontinued
|
||||||
|
|
||||||
|
No uptime or reliability guarantees are provided.
|
||||||
|
|
||||||
|
Change and maintenance alerts are sent to the chats where the bot is installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. Termination of Access
|
||||||
|
|
||||||
|
The developer reserves the right to:
|
||||||
|
|
||||||
|
* Block users
|
||||||
|
* Deny access
|
||||||
|
* Shut down the service at any time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. No Warranty
|
||||||
|
|
||||||
|
This software is provided "as is", without warranty of any kind.
|
||||||
|
|
||||||
|
No guarantees are made regarding:
|
||||||
|
|
||||||
|
* Functionality
|
||||||
|
* Availability
|
||||||
|
* Security
|
||||||
|
* Fitness for any purpose
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Acceptance of Terms
|
||||||
|
|
||||||
|
By using this software or any official instance, you agree to these Terms of Use.
|
||||||
|
|
||||||
|
If you do not agree, you must not use the software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 17. Changes to Terms
|
||||||
|
|
||||||
|
These terms may be updated at any time without prior notice.
|
||||||
|
|
||||||
|
Continued use of the software implies acceptance of the updated terms, even in old versions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. Jurisdiction
|
||||||
|
|
||||||
|
These terms shall be governed by and interpreted in accordance with the laws of Brazil.
|
||||||
|
|
||||||
|
Any disputes arising from the use of this software shall be subject to the applicable jurisdiction within Brazil.
|
||||||
348
deploy.sh
Executable file
348
deploy.sh
Executable file
@@ -0,0 +1,348 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# deploy — script de release com gates de qualidade e versionamento semântico
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# ./deploy → commit automático + push (branch atual)
|
||||||
|
# ./deploy --release patch → bump patch + tag + push para master
|
||||||
|
# ./deploy --release minor → bump minor + tag + push para master
|
||||||
|
# ./deploy --release major → bump major + tag + push para master
|
||||||
|
# ./deploy --dry-run → simula tudo sem executar nada
|
||||||
|
# ./deploy --help → mostra esta ajuda
|
||||||
|
#
|
||||||
|
# Conventional Commits esperados:
|
||||||
|
# feat: nova funcionalidade → candidate a minor bump
|
||||||
|
# fix: correção de bug → candidate a patch bump
|
||||||
|
# docs: só documentação → sem bump de versão
|
||||||
|
# chore: manutenção → sem bump de versão
|
||||||
|
# BREAKING CHANGE: no rodapé → candidate a major bump
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# Cores e utilitários
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${BLUE}▸${RESET} $*"; }
|
||||||
|
success() { echo -e "${GREEN}✔${RESET} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}⚠${RESET} $*"; }
|
||||||
|
error() { echo -e "${RED}✖${RESET} $*" >&2; }
|
||||||
|
die() { error "$*"; exit 1; }
|
||||||
|
step() { echo -e "\n${BOLD}$*${RESET}"; }
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# Argumentos
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
RELEASE_TYPE=""
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
sed -n '/^# Uso:/,/^# =/p' "$0" | grep '^#' | sed 's/^# \?//'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--release)
|
||||||
|
[[ -z "${2-}" ]] && die "--release requer: patch | minor | major"
|
||||||
|
RELEASE_TYPE="$2"
|
||||||
|
[[ "$RELEASE_TYPE" =~ ^(patch|minor|major)$ ]] || die "Tipo inválido: '$RELEASE_TYPE'. Use patch, minor ou major."
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--help|-h) show_help ;;
|
||||||
|
*) die "Argumento desconhecido: '$1'. Use --help." ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# Wrapper para dry-run
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
run() {
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${YELLOW}[dry-run]${RESET} $*"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GATE 1 — Ambiente Git
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
step "[ 1/6 ] Verificando ambiente Git..."
|
||||||
|
|
||||||
|
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||||
|
|| die "Não é um repositório Git."
|
||||||
|
|
||||||
|
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) \
|
||||||
|
|| die "Não foi possível detectar a branch atual (HEAD detached?)."
|
||||||
|
|
||||||
|
# Releases só saem da master/main
|
||||||
|
if [[ -n "$RELEASE_TYPE" ]]; then
|
||||||
|
[[ "$BRANCH" =~ ^(master|main)$ ]] \
|
||||||
|
|| die "--release só pode ser executado na branch master/main (atual: $BRANCH)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Branch: ${BOLD}$BRANCH${RESET}"
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GATE 2 — Working tree limpa
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
function worktree() {
|
||||||
|
# Em modo release, working tree tem de estar absolutamente limpa
|
||||||
|
if [[ -n "$RELEASE_TYPE" ]]; then
|
||||||
|
warn "Arquivos não commitados:"
|
||||||
|
[[ -n "$STAGED" ]] && echo -e " ${GREEN}Staged:${RESET}\n$(echo "$STAGED" | sed 's/^/ /')"
|
||||||
|
[[ -n "$MODIFIED" ]] && echo -e " ${YELLOW}Modificados:${RESET}\n$(echo "$MODIFIED" | sed 's/^/ /')"
|
||||||
|
[[ -n "$UNTRACKED" ]] && echo -e " ${RED}Não rastreados:${RESET}\n$(echo "$UNTRACKED" | sed 's/^/ /')"
|
||||||
|
die "Working tree suja. Faça commit ou stash antes de um release."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Seletor interativo de arquivos ──────────────────────────────────────
|
||||||
|
# Monta lista indexada: staged (S), modificados (M), não rastreados (?)
|
||||||
|
declare -a ALL_FILES
|
||||||
|
declare -a ALL_LABELS
|
||||||
|
while IFS= read -r f; do [[ -n "$f" ]] && ALL_FILES+=("$f") && ALL_LABELS+=("${GREEN}staged${RESET}"); done <<< "$STAGED"
|
||||||
|
while IFS= read -r f; do [[ -n "$f" ]] && ALL_FILES+=("$f") && ALL_LABELS+=("${YELLOW}modificado${RESET}"); done <<< "$MODIFIED"
|
||||||
|
while IFS= read -r f; do [[ -n "$f" ]] && ALL_FILES+=("$f") && ALL_LABELS+=("${RED}novo${RESET}"); done <<< "$UNTRACKED"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}Mudanças detectadas:${RESET}"
|
||||||
|
for i in "${!ALL_FILES[@]}"; do
|
||||||
|
printf " %2d) %b %s\n" "$((i+1))" "${ALL_LABELS[$i]}" "${ALL_FILES[$i]}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
declare -a SELECTED_FILES
|
||||||
|
|
||||||
|
echo -e "Digite os números para adicionar ao commit, ${BOLD}all${RESET} para todos, ou ${BOLD}done${RESET} para encerrar:"
|
||||||
|
while true; do
|
||||||
|
echo -ne "${BLUE}▸${RESET} "
|
||||||
|
read -r INPUT
|
||||||
|
|
||||||
|
case "$INPUT" in
|
||||||
|
done|"")
|
||||||
|
[[ ${#SELECTED_FILES[@]} -eq 0 ]] && die "Nenhum arquivo selecionado. Operação cancelada."
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
SELECTED_FILES=("${ALL_FILES[@]}")
|
||||||
|
echo -e " ${GREEN}✔${RESET} Todos os arquivos adicionados."
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Aceita múltiplos números na mesma linha (ex: "1 3 5")
|
||||||
|
for TOKEN in $INPUT; do
|
||||||
|
if [[ "$TOKEN" =~ ^[0-9]+$ ]] && (( TOKEN >= 1 && TOKEN <= ${#ALL_FILES[@]} )); then
|
||||||
|
FILE="${ALL_FILES[$((TOKEN-1))]}"
|
||||||
|
# Evita duplicatas
|
||||||
|
if printf '%s\n' "${SELECTED_FILES[@]+"${SELECTED_FILES[@]}"}" | grep -qxF "$FILE"; then
|
||||||
|
warn "$FILE já está na lista."
|
||||||
|
else
|
||||||
|
SELECTED_FILES+=("$FILE")
|
||||||
|
echo -e " ${GREEN}✔${RESET} adicionado: ${BOLD}$FILE${RESET}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Entrada inválida: '$TOKEN' — use um número entre 1 e ${#ALL_FILES[@]}, 'all' ou 'done'."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Arquivos no commit:"
|
||||||
|
for f in "${SELECTED_FILES[@]}"; do echo " $f"; done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Mensagem de commit ──────────────────────────────────────────────────
|
||||||
|
echo "Tipos de commit (Conventional Commits):"
|
||||||
|
echo " feat → nova funcionalidade"
|
||||||
|
echo " fix → correção de bug"
|
||||||
|
echo " docs → só documentação"
|
||||||
|
echo " refactor → refatoração sem mudança de comportamento"
|
||||||
|
echo " test → adição/correção de testes"
|
||||||
|
echo " chore → manutenção, dependências, CI"
|
||||||
|
echo " style → formatação, whitespace"
|
||||||
|
echo ""
|
||||||
|
echo -n "Tipo: "
|
||||||
|
read -r COMMIT_TYPE
|
||||||
|
[[ "$COMMIT_TYPE" =~ ^(feat|fix|docs|refactor|test|chore|style)$ ]] \
|
||||||
|
|| die "Tipo inválido. Use um dos listados acima."
|
||||||
|
|
||||||
|
echo -n "Escopo (opcional, ex: auth, api, ui — Enter para pular): "
|
||||||
|
read -r COMMIT_SCOPE
|
||||||
|
|
||||||
|
echo -n "Descrição curta: "
|
||||||
|
read -r COMMIT_DESC
|
||||||
|
[[ -z "$COMMIT_DESC" ]] && die "Descrição não pode ser vazia."
|
||||||
|
|
||||||
|
echo -n "Há breaking changes? [s/N]: "
|
||||||
|
read -r BREAKING
|
||||||
|
BREAKING_FOOTER=""
|
||||||
|
if [[ "$BREAKING" =~ ^[sS]$ ]]; then
|
||||||
|
echo -n "Descreva o breaking change: "
|
||||||
|
read -r BREAKING_MSG
|
||||||
|
BREAKING_FOOTER=$'\n\nBREAKING CHANGE: '"$BREAKING_MSG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$COMMIT_SCOPE" ]]; then
|
||||||
|
COMMIT_MSG="${COMMIT_TYPE}(${COMMIT_SCOPE}): ${COMMIT_DESC}${BREAKING_FOOTER}"
|
||||||
|
else
|
||||||
|
COMMIT_MSG="${COMMIT_TYPE}: ${COMMIT_DESC}${BREAKING_FOOTER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Mensagem: ${BOLD}${COMMIT_MSG}${RESET}"
|
||||||
|
|
||||||
|
for f in "${SELECTED_FILES[@]}"; do run git add -- "$f"; done
|
||||||
|
run git commit -m "$COMMIT_MSG"
|
||||||
|
success "Commit criado."
|
||||||
|
}
|
||||||
|
|
||||||
|
step "[ 2/6 ] Verificando working tree..."
|
||||||
|
|
||||||
|
function thereschanges(){
|
||||||
|
UNTRACKED=$(git ls-files --others --exclude-standard)
|
||||||
|
MODIFIED=$(git diff --name-only)
|
||||||
|
STAGED=$(git diff --cached --name-only)
|
||||||
|
if [[ -n "$UNTRACKED" || -n "$MODIFIED" || -n "$STAGED" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if thereschanges; then
|
||||||
|
worktree
|
||||||
|
if thereschanges; then
|
||||||
|
echo -n "Deseja criar outro commit? [s/N]: "
|
||||||
|
read -r OTHERCOMMIT
|
||||||
|
[[ "$OTHERCOMMIT" =~ ^[sS]$ ]] && worktree
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
success "Working tree limpa."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GATE 3 — Testes e qualidade
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
step "[ 3/6 ] Executando gates de qualidade..."
|
||||||
|
|
||||||
|
run_if_exists() {
|
||||||
|
local label="$1"
|
||||||
|
local cmd="$2"
|
||||||
|
if eval "$cmd" >/dev/null 2>&1; then
|
||||||
|
info "$label encontrado. Executando..."
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo -e " ${YELLOW}[dry-run]${RESET} $cmd"
|
||||||
|
else
|
||||||
|
eval "$cmd" || die "$label falhou. Corrija antes de continuar."
|
||||||
|
fi
|
||||||
|
success "$label passou."
|
||||||
|
else
|
||||||
|
warn "$label não encontrado — pulando."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
if [[ -f "package.json" ]]; then
|
||||||
|
if node -e "require('./package.json').scripts?.lint" >/dev/null 2>&1; then
|
||||||
|
run_if_exists "Lint (npm)" "npm run lint --if-present"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[[ -f ".flake8" || -f "setup.cfg" || -f "pyproject.toml" ]] && \
|
||||||
|
run_if_exists "Lint (flake8)" "command -v flake8 && flake8 ."
|
||||||
|
|
||||||
|
# Testes
|
||||||
|
run_if_exists "Testes (npm)" "node -e \"require('./package.json').scripts?.test\" && npm test --if-present"
|
||||||
|
|
||||||
|
# Build (só bloqueia em release)
|
||||||
|
if [[ -n "$RELEASE_TYPE" ]]; then
|
||||||
|
run_if_exists "Build (npm)" "node -e \"require('./package.json').scripts?.build\" && npm run build"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "Gates de qualidade concluídos."
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GATE 4 — Sincronia com remoto
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
step "[ 4/6 ] Verificando sincronia com remoto..."
|
||||||
|
|
||||||
|
if git remote get-url origin >/dev/null 2>&1; then
|
||||||
|
run git fetch origin --quiet
|
||||||
|
|
||||||
|
LOCAL=$(git rev-parse HEAD)
|
||||||
|
REMOTE=$(git rev-parse "origin/$BRANCH" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$REMOTE" && "$LOCAL" != "$REMOTE" ]]; then
|
||||||
|
BEHIND=$(git rev-list --count HEAD.."origin/$BRANCH")
|
||||||
|
AHEAD=$(git rev-list --count "origin/$BRANCH"..HEAD)
|
||||||
|
|
||||||
|
[[ "$BEHIND" -gt 0 ]] && die "Branch está $BEHIND commit(s) atrás do remoto. Faça pull antes."
|
||||||
|
[[ "$AHEAD" -gt 0 ]] && info "Branch está $AHEAD commit(s) à frente do remoto — ok, será enviado."
|
||||||
|
else
|
||||||
|
success "Branch sincronizada com o remoto."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Nenhum remoto 'origin' configurado — pulando verificação."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GATE 5 — Versionamento (somente --release)
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
if [[ -n "$RELEASE_TYPE" ]]; then
|
||||||
|
step "[ 5/6 ] Versionando release ($RELEASE_TYPE)..."
|
||||||
|
|
||||||
|
if [[ -f "package.json" ]]; then
|
||||||
|
CURRENT_VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "desconhecida")
|
||||||
|
info "Versão atual: ${BOLD}$CURRENT_VERSION${RESET}"
|
||||||
|
run npm version "$RELEASE_TYPE" -m "chore(release): %s"
|
||||||
|
NEW_VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "nova")
|
||||||
|
success "Nova versão: ${BOLD}$NEW_VERSION${RESET}"
|
||||||
|
else
|
||||||
|
# Fallback: tag Git manual
|
||||||
|
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
|
info "Última tag: $LAST_TAG"
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "${LAST_TAG#v}"
|
||||||
|
case "$RELEASE_TYPE" in
|
||||||
|
major) MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0 ;;
|
||||||
|
minor) MINOR=$((MINOR+1)); PATCH=0 ;;
|
||||||
|
patch) PATCH=$((PATCH+1)) ;;
|
||||||
|
esac
|
||||||
|
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
run git tag -a "$NEW_TAG" -m "chore(release): $NEW_TAG"
|
||||||
|
success "Tag criada: ${BOLD}$NEW_TAG${RESET}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
step "[ 5/6 ] Versionamento..."
|
||||||
|
info "Modo push simples (sem --release). Nenhuma tag será criada."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# GATE 6 — Push
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
step "[ 6/6 ] Enviando para o remoto..."
|
||||||
|
|
||||||
|
run git push origin "$BRANCH" --follow-tags
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
# Resumo final
|
||||||
|
# ──────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}${BOLD}═════════════════════════════════${RESET}"
|
||||||
|
echo -e "${GREEN}${BOLD} Deploy concluído com sucesso!${RESET}"
|
||||||
|
echo -e "${GREEN}${BOLD}═════════════════════════════════${RESET}"
|
||||||
|
echo -e " Branch : ${BOLD}$BRANCH${RESET}"
|
||||||
|
[[ -n "$RELEASE_TYPE" ]] && \
|
||||||
|
echo -e " Release: ${BOLD}$RELEASE_TYPE${RESET} → ${BOLD}${NEW_VERSION:-$NEW_TAG}${RESET}"
|
||||||
|
$DRY_RUN && echo -e "\n ${YELLOW}(dry-run: nenhuma alteração foi feita de fato)${RESET}"
|
||||||
|
echo ""
|
||||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
manybot:
|
||||||
|
build: .
|
||||||
|
container_name: manybot
|
||||||
|
volumes:
|
||||||
|
- ./session:/app/session
|
||||||
|
- ./manybot.conf:/app/manybot.conf:ro
|
||||||
|
- ./src/plugins:/app/src/plugins
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
277
docs/API (en).md
Normal file
277
docs/API (en).md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# 🛠️ API Reference
|
||||||
|
|
||||||
|
Complete documentation of objects available in plugins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The `msg` Object
|
||||||
|
|
||||||
|
Contains information about the received message.
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `msg.body` | `string` | Full text of the message |
|
||||||
|
| `msg.args` | `string[]` | Message tokens. E.g.: `"!video url"` → `["!video", "url"]` |
|
||||||
|
| `msg.type` | `string` | Message type: `chat`, `image`, `video`, `audio`, `sticker`, `ptt` (voice), `document`, `location` |
|
||||||
|
| `msg.sender` | `string` | Sender ID (format: `NUMBER@c.us` or `NUMBER@g.us`) |
|
||||||
|
| `msg.senderName` | `string` | Display name of the sender |
|
||||||
|
| `msg.fromMe` | `boolean` | `true` if the bot itself sent the message |
|
||||||
|
| `msg.hasMedia` | `boolean` | `true` if the message contains media |
|
||||||
|
| `msg.hasReply` | `boolean` | `true` if the message is a reply to another |
|
||||||
|
| `msg.isGif` | `boolean` | `true` if the media is a GIF |
|
||||||
|
| `msg.timestamp` | `number` | Unix timestamp of the message |
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
#### `msg.is(cmd)`
|
||||||
|
|
||||||
|
Checks whether the message starts with the specified command.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (msg.is(CMD_PREFIX + "video")) {
|
||||||
|
// Execute command
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `boolean`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `msg.reply(text)`
|
||||||
|
|
||||||
|
Replies to the current message with a quote (citation).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await msg.reply("Reply with citation!");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `text` (string): Text of the reply
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `msg.downloadMedia()`
|
||||||
|
|
||||||
|
Downloads the media from the message.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
// Returns: { mimetype: "image/jpeg", data: "base64string..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `Promise<{ mimetype: string, data: string } | null>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `msg.getReply()`
|
||||||
|
|
||||||
|
Returns the message that was quoted.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const quotedMsg = msg.getReply();
|
||||||
|
if (quotedMsg) {
|
||||||
|
console.log(quotedMsg.body);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `msg object | null`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The `api` Object
|
||||||
|
|
||||||
|
Contains methods for interacting with WhatsApp and other plugins.
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
|
||||||
|
#### `api.chat`
|
||||||
|
|
||||||
|
Information about the current chat.
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `api.chat.id` | `string` | Chat ID |
|
||||||
|
| `api.chat.name` | `string` | Chat name |
|
||||||
|
| `api.chat.isGroup` | `boolean` | `true` if it is a group |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Send Methods
|
||||||
|
|
||||||
|
#### `api.send(text)`
|
||||||
|
|
||||||
|
Sends a text message.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.send("Message sent!");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `text` (string): Text to send
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendVideo(filePath)`
|
||||||
|
|
||||||
|
Sends a video from the file system.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.sendVideo("/path/to/video.mp4");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `filePath` (string): Path to the file
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendAudio(filePath)`
|
||||||
|
|
||||||
|
Sends audio as a voice message (PTT).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.sendAudio("/path/to/audio.mp3");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `filePath` (string): Path to the file
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendImage(filePath, caption?)`
|
||||||
|
|
||||||
|
Sends an image.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.sendImage("/path/to/image.jpg", "Optional caption");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `filePath` (string): Path to the file
|
||||||
|
- `caption` (string, optional): Image caption
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendSticker(bufferOrPath)`
|
||||||
|
|
||||||
|
Sends a sticker. Accepts a `Buffer` or a file path.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// With Buffer
|
||||||
|
const buffer = fs.readFileSync("image.png");
|
||||||
|
await api.sendSticker(buffer);
|
||||||
|
|
||||||
|
// With path
|
||||||
|
await api.sendSticker("/path/to/image.png");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `bufferOrPath` (`Buffer` | `string`): Image data or file path
|
||||||
|
|
||||||
|
**Returns:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Plugin Methods
|
||||||
|
|
||||||
|
#### `api.getPlugin(name)`
|
||||||
|
|
||||||
|
Accesses the public API of another plugin.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const utils = api.getPlugin("utilities");
|
||||||
|
const data = utils.formatDate(new Date());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `name` (string): Plugin name
|
||||||
|
|
||||||
|
**Returns:** `object | undefined`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Log Methods
|
||||||
|
|
||||||
|
#### `api.log.info(...args)`
|
||||||
|
|
||||||
|
Informational log.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
api.log.info("Message received:", msg.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api.log.warn(...args)`
|
||||||
|
|
||||||
|
Warning log.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
api.log.warn("Missing config, using default");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api.log.error(...args)`
|
||||||
|
|
||||||
|
Error log.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
api.log.error("Failed to process:", error);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Object
|
||||||
|
|
||||||
|
Import settings from `manybot.conf`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CMD_PREFIX, CLIENT_ID, CHATS, PLUGINS } from "../../config.js";
|
||||||
|
|
||||||
|
// Custom configurations also work
|
||||||
|
import { MY_PREFIX } from "../../config.js";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
// Ignore messages from the bot itself
|
||||||
|
if (msg.fromMe) return;
|
||||||
|
|
||||||
|
// Command: !echo
|
||||||
|
if (!msg.is(CMD_PREFIX + "echo")) return;
|
||||||
|
|
||||||
|
api.log.info("Echo command received from:", msg.senderName);
|
||||||
|
|
||||||
|
// If it has media, download and resend
|
||||||
|
if (msg.hasMedia) {
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
await api.sendSticker(media.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a reply, echo the quoted message
|
||||||
|
if (msg.hasReply) {
|
||||||
|
const quoted = msg.getReply();
|
||||||
|
await msg.reply(`You quoted: "${quoted.body}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default response
|
||||||
|
await msg.reply("Send media or reply to a message!");
|
||||||
|
}
|
||||||
|
```
|
||||||
277
docs/API.md
Normal file
277
docs/API.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# 🛠️ Referência da API
|
||||||
|
|
||||||
|
Documentação completa dos objetos disponíveis nos plugins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objeto `msg`
|
||||||
|
|
||||||
|
Contém informações da mensagem recebida.
|
||||||
|
|
||||||
|
### Propriedades
|
||||||
|
|
||||||
|
| Propriedade | Tipo | Descrição |
|
||||||
|
|-------------|------|-----------|
|
||||||
|
| `msg.body` | `string` | Texto completo da mensagem |
|
||||||
|
| `msg.args` | `string[]` | Tokens da mensagem. Ex: `"!video url"` → `["!video", "url"]` |
|
||||||
|
| `msg.type` | `string` | Tipo da mensagem: `chat`, `image`, `video`, `audio`, `sticker`, `ptt` (voz), `document`, `location` |
|
||||||
|
| `msg.sender` | `string` | ID do remetente (formato: `NUMERO@c.us` ou `NUMERO@g.us`) |
|
||||||
|
| `msg.senderName` | `string` | Nome de exibição do remetente |
|
||||||
|
| `msg.fromMe` | `boolean` | `true` se o próprio bot enviou a mensagem |
|
||||||
|
| `msg.hasMedia` | `boolean` | `true` se a mensagem contém mídia |
|
||||||
|
| `msg.hasReply` | `boolean` | `true` se é uma resposta a outra mensagem |
|
||||||
|
| `msg.isGif` | `boolean` | `true` se a mídia é um GIF |
|
||||||
|
| `msg.timestamp` | `number` | Timestamp Unix da mensagem |
|
||||||
|
|
||||||
|
### Métodos
|
||||||
|
|
||||||
|
#### `msg.is(cmd)`
|
||||||
|
|
||||||
|
Verifica se a mensagem começa com o comando especificado.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
if (msg.is(CMD_PREFIX + "video")) {
|
||||||
|
// Executa comando
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retorno:** `boolean`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `msg.reply(text)`
|
||||||
|
|
||||||
|
Responde à mensagem atual com quote (citação).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await msg.reply("Resposta com citação!");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `text` (string): Texto da resposta
|
||||||
|
|
||||||
|
**Retorno:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `msg.downloadMedia()`
|
||||||
|
|
||||||
|
Baixa a mídia da mensagem.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
// Retorna: { mimetype: "image/jpeg", data: "base64string..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retorno:** `Promise<{ mimetype: string, data: string } | null>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `msg.getReply()`
|
||||||
|
|
||||||
|
Retorna a mensagem que foi citada.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const mensagemCitada = msg.getReply();
|
||||||
|
if (mensagemCitada) {
|
||||||
|
console.log(mensagemCitada.body);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retorno:** `msg object | null`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objeto `api`
|
||||||
|
|
||||||
|
Contém métodos para interagir com o WhatsApp e outros plugins.
|
||||||
|
|
||||||
|
### Propriedades
|
||||||
|
|
||||||
|
#### `api.chat`
|
||||||
|
|
||||||
|
Informações do chat atual.
|
||||||
|
|
||||||
|
| Propriedade | Tipo | Descrição |
|
||||||
|
|-------------|------|-----------|
|
||||||
|
| `api.chat.id` | `string` | ID do chat |
|
||||||
|
| `api.chat.name` | `string` | Nome do chat |
|
||||||
|
| `api.chat.isGroup` | `boolean` | `true` se é grupo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Métodos de Envio
|
||||||
|
|
||||||
|
#### `api.send(text)`
|
||||||
|
|
||||||
|
Envia uma mensagem de texto.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.send("Mensagem enviada!");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `text` (string): Texto a enviar
|
||||||
|
|
||||||
|
**Retorno:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendVideo(filePath)`
|
||||||
|
|
||||||
|
Envia um vídeo do sistema de arquivos.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.sendVideo("/caminho/para/video.mp4");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `filePath` (string): Caminho para o arquivo
|
||||||
|
|
||||||
|
**Retorno:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendAudio(filePath)`
|
||||||
|
|
||||||
|
Envia um áudio como mensagem de voz (PTT).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.sendAudio("/caminho/para/audio.mp3");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `filePath` (string): Caminho para o arquivo
|
||||||
|
|
||||||
|
**Retorno:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendImage(filePath, caption?)`
|
||||||
|
|
||||||
|
Envia uma imagem.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await api.sendImage("/caminho/para/imagem.jpg", "Legenda opcional");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `filePath` (string): Caminho para o arquivo
|
||||||
|
- `caption` (string, opcional): Legenda da imagem
|
||||||
|
|
||||||
|
**Retorno:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `api.sendSticker(bufferOuPath)`
|
||||||
|
|
||||||
|
Envia uma figurinha. Aceita `Buffer` ou caminho para arquivo.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Com Buffer
|
||||||
|
const buffer = fs.readFileSync("imagem.png");
|
||||||
|
await api.sendSticker(buffer);
|
||||||
|
|
||||||
|
// Com caminho
|
||||||
|
await api.sendSticker("/caminho/para/imagem.png");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `bufferOuPath` (`Buffer` | `string`): Dados da imagem ou caminho
|
||||||
|
|
||||||
|
**Retorno:** `Promise<void>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Métodos de Plugin
|
||||||
|
|
||||||
|
#### `api.getPlugin(name)`
|
||||||
|
|
||||||
|
Acessa a API pública de outro plugin.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const utils = api.getPlugin("utilidades");
|
||||||
|
const data = utils.formatarData(new Date());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
- `name` (string): Nome do plugin
|
||||||
|
|
||||||
|
**Retorno:** `object | undefined`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Métodos de Log
|
||||||
|
|
||||||
|
#### `api.log.info(...args)`
|
||||||
|
|
||||||
|
Log informativo.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
api.log.info("Mensagem recebida:", msg.body);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api.log.warn(...args)`
|
||||||
|
|
||||||
|
Log de aviso.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
api.log.warn("Configuração ausente, usando padrão");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `api.log.error(...args)`
|
||||||
|
|
||||||
|
Log de erro.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
api.log.error("Falha ao processar:", erro);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objeto de Configuração
|
||||||
|
|
||||||
|
Importe configurações do `manybot.conf`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CMD_PREFIX, CLIENT_ID, CHATS, PLUGINS } from "../../config.js";
|
||||||
|
|
||||||
|
// Configurações personalizadas também funcionam
|
||||||
|
import { MEU_PREFIXO } from "../../config.js";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemplo Completo
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
// Ignora mensagens do próprio bot
|
||||||
|
if (msg.fromMe) return;
|
||||||
|
|
||||||
|
// Comando: !eco
|
||||||
|
if (!msg.is(CMD_PREFIX + "eco")) return;
|
||||||
|
|
||||||
|
api.log.info("Comando eco recebido de:", msg.senderName);
|
||||||
|
|
||||||
|
// Se tem mídia, baixa e reenvia
|
||||||
|
if (msg.hasMedia) {
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
await api.sendSticker(media.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se é resposta, ecoa a mensagem citada
|
||||||
|
if (msg.hasReply) {
|
||||||
|
const citada = msg.getReply();
|
||||||
|
await msg.reply(`Você citou: "${citada.body}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resposta padrão
|
||||||
|
await msg.reply("Envie uma mídia ou responda uma mensagem!");
|
||||||
|
}
|
||||||
|
```
|
||||||
146
docs/CONFIGURACAO.md
Normal file
146
docs/CONFIGURACAO.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# ⚙️ Configuração
|
||||||
|
|
||||||
|
Guia completo do arquivo `manybot.conf`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura Básica
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comentários começam com '#'
|
||||||
|
|
||||||
|
CLIENT_ID=bot_permanente
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=pt
|
||||||
|
CHATS=[]
|
||||||
|
PLUGINS=[]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opções
|
||||||
|
|
||||||
|
### CLIENT_ID
|
||||||
|
|
||||||
|
Identificador único da sessão do bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLIENT_ID=bot_permanente
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Padrão:** `bot_permanente`
|
||||||
|
- **Uso:** Cria uma pasta `session/` com esse nome para armazenar dados de autenticação
|
||||||
|
|
||||||
|
### CMD_PREFIX
|
||||||
|
|
||||||
|
Caractere que indica o início de um comando.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CMD_PREFIX=!
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Padrão:** `!`
|
||||||
|
- **Exemplo:** Com prefixo `!`, o comando é `!figurinha`. Com `.`, seria `.figurinha`.
|
||||||
|
|
||||||
|
### LANGUAGE
|
||||||
|
|
||||||
|
Idioma das mensagens do bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LANGUAGE=pt
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Padrão:** `en` (inglês)
|
||||||
|
- **Opções:** `en` (inglês), `pt` (português), `es` (espanhol)
|
||||||
|
- **Nota:** Se o idioma selecionado não existir, o bot usará inglês como fallback
|
||||||
|
|
||||||
|
### CHATS
|
||||||
|
|
||||||
|
Lista de IDs de chats onde o bot responderá.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CHATS=[
|
||||||
|
123456789@c.us, # Chat privado
|
||||||
|
123456789@g.us # Grupo
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Padrão:** `[]` (vazio = responde em todos)
|
||||||
|
- **Formato:**
|
||||||
|
- Privado: `NUMERO@c.us`
|
||||||
|
- Grupo: `NUMERO@g.us`
|
||||||
|
|
||||||
|
#### Como descobrir o ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node src/utils/get_id.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Escaneie o QR Code e mande uma mensagem no chat. O ID aparecerá no terminal.
|
||||||
|
|
||||||
|
> Nota: O utilitário usa um `CLIENT_ID` separado para não conflitar com a sessão principal.
|
||||||
|
|
||||||
|
### PLUGINS
|
||||||
|
|
||||||
|
Lista de plugins a serem carregados.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PLUGINS=[
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
figurinha,
|
||||||
|
adivinhacao,
|
||||||
|
forca,
|
||||||
|
many,
|
||||||
|
obrigado
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Padrão:** `[]` (nenhum)
|
||||||
|
- Cada nome corresponde a uma pasta em `src/plugins/`
|
||||||
|
- Remova ou comente para desativar sem apagar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configurações Personalizadas
|
||||||
|
|
||||||
|
Você pode adicionar suas próprias variáveis para plugins:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# manybot.conf
|
||||||
|
MEU_PREFIXO=>
|
||||||
|
API_KEY=minha_chave
|
||||||
|
```
|
||||||
|
|
||||||
|
E acessar no código do plugin:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { MEU_PREFIXO, API_KEY } from "../../config.js";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exemplo Completo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ManyBot Configuration
|
||||||
|
|
||||||
|
CLIENT_ID=meu_bot_prod
|
||||||
|
CMD_PREFIX=/
|
||||||
|
LANGUAGE=pt
|
||||||
|
|
||||||
|
CHATS=[
|
||||||
|
5511999999999@c.us,
|
||||||
|
5511888888888-123456789@g.us
|
||||||
|
]
|
||||||
|
|
||||||
|
PLUGINS=[
|
||||||
|
figurinha,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
many
|
||||||
|
]
|
||||||
|
|
||||||
|
# Configurações extras
|
||||||
|
ADMIN_NUMBER=5511999999999@c.us
|
||||||
|
```
|
||||||
146
docs/CONFIGURATION.md
Normal file
146
docs/CONFIGURATION.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
Complete guide for the `manybot.conf` file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comments start with '#'
|
||||||
|
|
||||||
|
CLIENT_ID=bot_permanente
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=en
|
||||||
|
CHATS=[]
|
||||||
|
PLUGINS=[]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
### CLIENT_ID
|
||||||
|
|
||||||
|
Unique identifier for the bot session.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLIENT_ID=my_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Default:** `bot_permanente`
|
||||||
|
- **Usage:** Creates a `session/` folder with this name to store authentication data
|
||||||
|
|
||||||
|
### CMD_PREFIX
|
||||||
|
|
||||||
|
Character that indicates the start of a command.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CMD_PREFIX=!
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Default:** `!`
|
||||||
|
- **Example:** With prefix `!`, the command is `!sticker`. With `.`, it would be `.sticker`.
|
||||||
|
|
||||||
|
### LANGUAGE
|
||||||
|
|
||||||
|
Bot message language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LANGUAGE=en
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Default:** `en` (English)
|
||||||
|
- **Options:** `en` (English), `pt` (Portuguese), `es` (Spanish)
|
||||||
|
- **Note:** If the selected language doesn't exist, the bot will fall back to English
|
||||||
|
|
||||||
|
### CHATS
|
||||||
|
|
||||||
|
List of chat IDs where the bot will respond.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CHATS=[
|
||||||
|
123456789@c.us, # Private chat
|
||||||
|
123456789@g.us # Group
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Default:** `[]` (empty = responds to all)
|
||||||
|
- **Format:**
|
||||||
|
- Private: `NUMBER@c.us`
|
||||||
|
- Group: `NUMBER@g.us`
|
||||||
|
|
||||||
|
#### How to discover the ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node src/utils/get_id.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Scan the QR Code and send a message in the chat. The ID will appear in the terminal.
|
||||||
|
|
||||||
|
> Note: The utility uses a separate `CLIENT_ID` to avoid conflicting with the main session.
|
||||||
|
|
||||||
|
### PLUGINS
|
||||||
|
|
||||||
|
List of plugins to be loaded.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PLUGINS=[
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
sticker,
|
||||||
|
guess,
|
||||||
|
hangman,
|
||||||
|
many,
|
||||||
|
thanks
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Default:** `[]` (none)
|
||||||
|
- Each name corresponds to a folder in `src/plugins/`
|
||||||
|
- Remove or comment to disable without deleting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom Settings
|
||||||
|
|
||||||
|
You can add your own variables for plugins:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# manybot.conf
|
||||||
|
MY_PREFIX=>
|
||||||
|
API_KEY=my_key
|
||||||
|
```
|
||||||
|
|
||||||
|
And access in the plugin code:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { MY_PREFIX, API_KEY } from "../../config.js";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ManyBot Configuration
|
||||||
|
|
||||||
|
CLIENT_ID=my_bot_prod
|
||||||
|
CMD_PREFIX=/
|
||||||
|
LANGUAGE=en
|
||||||
|
|
||||||
|
CHATS=[
|
||||||
|
5511999999999@c.us,
|
||||||
|
5511888888888-123456789@g.us
|
||||||
|
]
|
||||||
|
|
||||||
|
PLUGINS=[
|
||||||
|
sticker,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
many
|
||||||
|
]
|
||||||
|
|
||||||
|
# Extra settings
|
||||||
|
ADMIN_NUMBER=5511999999999@c.us
|
||||||
|
```
|
||||||
190
docs/INSTALACAO.md
Normal file
190
docs/INSTALACAO.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# 📥 Instalação
|
||||||
|
|
||||||
|
Guia completo de instalação do ManyBot em diferentes plataformas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Índice
|
||||||
|
|
||||||
|
- [Docker](#docker) (Recomendado)
|
||||||
|
- [Linux](#linux)
|
||||||
|
- [Windows](#windows)
|
||||||
|
- [Termux (Android)](#termux-android)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
A maneira mais fácil e recomendada de rodar o ManyBot.
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
|
||||||
|
- [Docker](https://docs.docker.com/get-docker/)
|
||||||
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
|
### Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone o repositório
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
|
||||||
|
# 2. Crie o arquivo de configuração
|
||||||
|
cp manybot.conf.example manybot.conf
|
||||||
|
nano manybot.conf
|
||||||
|
|
||||||
|
# 3. Inicie com Docker
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 4. Veja os logs para escanear o QR Code
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Escaneie o QR Code** que aparecerá nos logs.
|
||||||
|
|
||||||
|
### Comandos úteis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ver logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Parar o bot
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Atualizar
|
||||||
|
git pull
|
||||||
|
docker-compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### 1. Clone o repositório
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure o bot
|
||||||
|
|
||||||
|
Crie o arquivo de configuração:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch manybot.conf
|
||||||
|
nano manybot.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplo de configuração:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comentários com '#'
|
||||||
|
|
||||||
|
CLIENT_ID=bot_permanente
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=pt
|
||||||
|
CHATS=[
|
||||||
|
123456789@c.us,
|
||||||
|
123456789@g.us
|
||||||
|
]
|
||||||
|
PLUGINS=[
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
figurinha,
|
||||||
|
adivinhacao
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detalhes:**
|
||||||
|
- `CLIENT_ID`: ID da sessão (padrão: `bot_permanente`)
|
||||||
|
- `CMD_PREFIX`: Prefixo dos comandos (padrão: `!`)
|
||||||
|
- `LANGUAGE`: Idioma do bot - `pt`, `en` ou `es` (padrão: `en`)
|
||||||
|
- `CHATS`: IDs dos chats permitidos (deixe vazio para todos)
|
||||||
|
- `PLUGINS`: Lista de plugins ativos
|
||||||
|
|
||||||
|
### 3. Execute a instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ./setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Primeira execução
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ./src/main.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Escaneie o QR Code no WhatsApp:
|
||||||
|
|
||||||
|
**Menu → Dispositivos conectados → Conectar um dispositivo**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
O ManyBot foi pensado para Linux, mas funciona no Windows via **Git Bash**.
|
||||||
|
|
||||||
|
### Pré-requisitos
|
||||||
|
|
||||||
|
1. **Git Bash**: https://git-scm.com/download/win
|
||||||
|
2. **Node.js**: https://nodejs.org (escolha "Instalador Windows (.msi)")
|
||||||
|
|
||||||
|
### Instalação
|
||||||
|
|
||||||
|
Após instalar ambos, abra o **Git Bash** e siga os mesmos passos da [instalação Linux](#linux).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Termux (Android)
|
||||||
|
|
||||||
|
> ⚠️ **Aviso:** Suporte experimental. Não há garantia de funcionamento.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instale o Termux pela F-Droid (não use Play Store)
|
||||||
|
# https://f-droid.org/packages/com.termux/
|
||||||
|
|
||||||
|
# Atualize pacotes
|
||||||
|
pkg update && pkg upgrade
|
||||||
|
|
||||||
|
# Instale dependências
|
||||||
|
pkg install nodejs git
|
||||||
|
|
||||||
|
# Clone e instale
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
```
|
||||||
|
|
||||||
|
Siga os passos de configuração Linux a partir do passo 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Resolução de Problemas
|
||||||
|
|
||||||
|
### Erro ao escanear QR Code
|
||||||
|
|
||||||
|
- Limpe os dados do Chrome/Chromium do Termux
|
||||||
|
- Delete a pasta `session/` e tente novamente
|
||||||
|
|
||||||
|
### Bot não responde comandos
|
||||||
|
|
||||||
|
- Verifique o `CMD_PREFIX` no `manybot.conf`
|
||||||
|
- Confira se o plugin está na lista `PLUGINS`
|
||||||
|
|
||||||
|
### Erros de instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Limpe a cache do npm
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# Reinstale dependências
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Próximos Passos
|
||||||
|
|
||||||
|
- [Configuração avançada](./CONFIGURACAO.md)
|
||||||
|
- [Criando plugins](./PLUGINS.md)
|
||||||
190
docs/INSTALLATION.md
Normal file
190
docs/INSTALLATION.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
Complete installation guide for ManyBot on different platforms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [Docker](#docker) (Recommended)
|
||||||
|
- [Linux](#linux)
|
||||||
|
- [Windows](#windows)
|
||||||
|
- [Termux (Android)](#termux-android)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
The easiest and recommended way to run ManyBot.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Docker](https://docs.docker.com/get-docker/)
|
||||||
|
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repository
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
|
||||||
|
# 2. Create the configuration file
|
||||||
|
cp manybot.conf.example manybot.conf
|
||||||
|
nano manybot.conf
|
||||||
|
|
||||||
|
# 3. Start with Docker
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 4. Watch logs to scan QR Code
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scan the QR Code** that appears in the logs.
|
||||||
|
|
||||||
|
### Useful Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop the bot
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Update
|
||||||
|
git pull
|
||||||
|
docker-compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### 1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure the bot
|
||||||
|
|
||||||
|
Create the configuration file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch manybot.conf
|
||||||
|
nano manybot.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comments with '#'
|
||||||
|
|
||||||
|
CLIENT_ID=bot_permanente
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=en
|
||||||
|
CHATS=[
|
||||||
|
123456789@c.us,
|
||||||
|
123456789@g.us
|
||||||
|
]
|
||||||
|
PLUGINS=[
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
sticker,
|
||||||
|
guess
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Details:**
|
||||||
|
- `CLIENT_ID`: Session ID (default: `bot_permanente`)
|
||||||
|
- `CMD_PREFIX`: Command prefix (default: `!`)
|
||||||
|
- `LANGUAGE`: Bot language - `pt`, `en`, or `es` (default: `en`)
|
||||||
|
- `CHATS`: Allowed chat IDs (leave empty for all)
|
||||||
|
- `PLUGINS`: List of active plugins
|
||||||
|
|
||||||
|
### 3. Run installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ./setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. First run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ./src/main.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Scan the QR Code in WhatsApp:
|
||||||
|
|
||||||
|
**Menu → Linked Devices → Link a Device**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
ManyBot was designed for Linux, but works on Windows via **Git Bash**.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Git Bash**: https://git-scm.com/download/win
|
||||||
|
2. **Node.js**: https://nodejs.org (choose "Windows Installer (.msi)")
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
After installing both, open **Git Bash** and follow the same steps as the [Linux installation](#linux).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Termux (Android)
|
||||||
|
|
||||||
|
> ⚠️ **Warning:** Experimental support. No guarantee of functionality.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Termux from F-Droid (don't use Play Store)
|
||||||
|
# https://f-droid.org/packages/com.termux/
|
||||||
|
|
||||||
|
# Update packages
|
||||||
|
pkg update && pkg upgrade
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pkg install nodejs git
|
||||||
|
|
||||||
|
# Clone and install
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the Linux configuration steps from step 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### QR Code scanning error
|
||||||
|
|
||||||
|
- Clear Chrome/Chromium data from Termux
|
||||||
|
- Delete the `session/` folder and try again
|
||||||
|
|
||||||
|
### Bot not responding to commands
|
||||||
|
|
||||||
|
- Check `CMD_PREFIX` in `manybot.conf`
|
||||||
|
- Make sure the plugin is in the `PLUGINS` list
|
||||||
|
|
||||||
|
### Installation errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clean npm cache
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# Reinstall dependencies
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Advanced configuration](./CONFIGURATION.md)
|
||||||
|
- [Creating plugins](./PLUGINS_EN.md)
|
||||||
317
docs/PLUGINS (en).md
Normal file
317
docs/PLUGINS (en).md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# 🔌 Creating Plugins
|
||||||
|
|
||||||
|
Complete guide to creating plugins in ManyBot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Index
|
||||||
|
|
||||||
|
- [Basic Structure](#basic-structure)
|
||||||
|
- [Plugin Manifest](#plugin-manifest-manyplug-json)
|
||||||
|
- [Creating Your First Plugin](#creating-your-first-plugin)
|
||||||
|
- [Object API](#object-api)
|
||||||
|
- [Exposing API](#exposing-api-to-other-plugins)
|
||||||
|
- [Translating Your Plugin](#translating-your-plugin)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/plugins/
|
||||||
|
└── my-plugin/
|
||||||
|
├── index.js
|
||||||
|
├── manyplug.json
|
||||||
|
└── locale/ (optional)
|
||||||
|
├── en.json
|
||||||
|
├── pt.json
|
||||||
|
└── es.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`index.js` must export a `default` function that receives `{ msg, api }`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
// Your logic here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Manifest (manyplug.json)
|
||||||
|
|
||||||
|
Every plugin should have a `manyplug.json` at its root. It describes the plugin and declares any extra npm dependencies it needs.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "utility",
|
||||||
|
"service": false,
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `name` | `string` | Plugin identifier, must match the folder name |
|
||||||
|
| `version` | `string` | Semantic version (e.g., `"1.0.0"`) |
|
||||||
|
| `category` | `string` | Plugin category: `utility`, `media`, `game`, `humor`, `info` |
|
||||||
|
| `service` | `boolean` | `true` if the plugin runs in the background (scheduler, listener). `false` if triggered by a command or event |
|
||||||
|
| `dependencies` | `object` | Extra npm packages required by the plugin, same format as `package.json` |
|
||||||
|
|
||||||
|
### Example with dependencies
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "weather",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "utility",
|
||||||
|
"service": false,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After adding dependencies, run `npm install` at the project root to install them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating Your First Plugin
|
||||||
|
|
||||||
|
### Example 1: Simple command
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/greeting/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
// Only responds if the message starts with "!hi"
|
||||||
|
if (!msg.is(CMD_PREFIX + "hi")) return;
|
||||||
|
|
||||||
|
await msg.reply("Hello! 👋");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Command with arguments
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/calculate/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "calculate")) return;
|
||||||
|
|
||||||
|
// msg.args = ["!calculate", "5", "+", "3"]
|
||||||
|
const [, a, operator, b] = msg.args;
|
||||||
|
|
||||||
|
let result;
|
||||||
|
switch (operator) {
|
||||||
|
case "+": result = Number(a) + Number(b); break;
|
||||||
|
case "-": result = Number(a) - Number(b); break;
|
||||||
|
case "*": result = Number(a) * Number(b); break;
|
||||||
|
case "/": result = Number(a) / Number(b); break;
|
||||||
|
default: return msg.reply("Invalid operator!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await msg.reply(`Result: ${result}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Processing media
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/echo-media/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "echo")) return;
|
||||||
|
|
||||||
|
// Checks if the message has media
|
||||||
|
if (!msg.hasMedia) {
|
||||||
|
return msg.reply("Send a media file with the command!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloads the media
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
|
||||||
|
// Resends in the chat
|
||||||
|
await api.sendSticker(media.data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Object API
|
||||||
|
|
||||||
|
### `msg` Object
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
|----------|------|-------------|
|
||||||
|
| `msg.body` | `string` | Message text |
|
||||||
|
| `msg.args` | `string[]` | Message tokens |
|
||||||
|
| `msg.type` | `string` | Type: `chat`, `image`, `video`, `audio`, `sticker` |
|
||||||
|
| `msg.sender` | `string` | Sender ID |
|
||||||
|
| `msg.senderName` | `string` | Sender name |
|
||||||
|
| `msg.fromMe` | `boolean` | Whether the bot sent it |
|
||||||
|
| `msg.hasMedia` | `boolean` | Whether it has media |
|
||||||
|
| `msg.hasReply` | `boolean` | Whether it is a reply |
|
||||||
|
| `msg.isGif` | `boolean` | Whether it is a GIF |
|
||||||
|
| `msg.is(cmd)` | `function` | Checks if starts with command |
|
||||||
|
| `msg.reply(text)` | `function` | Replies with quote |
|
||||||
|
| `msg.downloadMedia()` | `function` | Returns `{ mimetype, data }` |
|
||||||
|
| `msg.getReply()` | `function` | Returns quoted message |
|
||||||
|
|
||||||
|
### `api` Object
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `api.send(text)` | Sends text |
|
||||||
|
| `api.sendVideo(path)` | Sends video |
|
||||||
|
| `api.sendAudio(path)` | Sends audio (voice) |
|
||||||
|
| `api.sendImage(path, caption?)` | Sends image |
|
||||||
|
| `api.sendSticker(bufferOrPath)` | Sends sticker |
|
||||||
|
| `api.getPlugin(name)` | Accesses another plugin |
|
||||||
|
| `api.chat.id` | Chat ID |
|
||||||
|
| `api.chat.name` | Chat name |
|
||||||
|
| `api.chat.isGroup` | Whether it is a group |
|
||||||
|
| `api.log.info(...)` | Info log |
|
||||||
|
| `api.log.warn(...)` | Warning log |
|
||||||
|
| `api.log.error(...)` | Error log |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exposing API to Other Plugins
|
||||||
|
|
||||||
|
A plugin can export functions for others to use:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/utilities/index.js
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
export const api = {
|
||||||
|
formatDate: (date) => date.toLocaleDateString("en-US"),
|
||||||
|
formatCurrency: (value) => `$${value.toFixed(2)}`,
|
||||||
|
wait: (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normal plugin logic
|
||||||
|
export default async function ({ msg }) {
|
||||||
|
if (msg.is("!ping")) {
|
||||||
|
await msg.reply("pong!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Another plugin using it:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/other/index.js
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
const utils = api.getPlugin("utilities");
|
||||||
|
|
||||||
|
const date = utils.formatDate(new Date());
|
||||||
|
await msg.reply(`Today is ${date}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Translating Your Plugin
|
||||||
|
|
||||||
|
Each plugin can have its own translations, completely independent from the bot core. The bot locale (set in `manybot.conf`) is used automatically.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/plugins/
|
||||||
|
└── my-plugin/
|
||||||
|
├── index.js
|
||||||
|
└── locale/
|
||||||
|
├── en.json
|
||||||
|
├── pt.json
|
||||||
|
└── es.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### locale/en.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hello": "Hello, {{name}}! 👋",
|
||||||
|
"error": {
|
||||||
|
"notFound": "Item not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### index.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
import { createPluginI18n } from "../../utils/pluginI18n.js";
|
||||||
|
|
||||||
|
const { t } = createPluginI18n(import.meta.url);
|
||||||
|
|
||||||
|
export default async function ({ msg }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "hi")) return;
|
||||||
|
|
||||||
|
// Simple key
|
||||||
|
await msg.reply(t("hello", { name: msg.senderName }));
|
||||||
|
|
||||||
|
// Nested key
|
||||||
|
await msg.reply(t("error.notFound"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- If the configured locale has no translation file, falls back to `en.json`.
|
||||||
|
- If the key doesn't exist in any file, the key itself is returned as-is.
|
||||||
|
- Use `{{variable}}` syntax for interpolation.
|
||||||
|
- Each plugin manages its own translations — never import `t` from the bot core.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If a plugin throws an error, the kernel automatically disables it:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
try {
|
||||||
|
// Code that might fail
|
||||||
|
const result = await somethingRisky();
|
||||||
|
await msg.reply(result);
|
||||||
|
} catch (error) {
|
||||||
|
// Logs the error and notifies
|
||||||
|
api.log.error("Plugin error:", error);
|
||||||
|
await msg.reply("Oops! Something went wrong.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enabling the Plugin
|
||||||
|
|
||||||
|
After creating it, add to `manybot.conf`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PLUGINS=[
|
||||||
|
# ... other plugins
|
||||||
|
my-plugin
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the bot to load it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [API Reference](./API.md)
|
||||||
|
- [Plugin examples](../src/plugins/)
|
||||||
360
docs/PLUGINS.md
Normal file
360
docs/PLUGINS.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# 🔌 Criando Plugins
|
||||||
|
|
||||||
|
Guia completo para criar plugins no ManyBot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ManyPlug CLI
|
||||||
|
|
||||||
|
**ManyPlug** é a ferramenta oficial para gerenciar plugins do ManyBot. Com ela você pode criar, instalar e validar plugins facilmente.
|
||||||
|
|
||||||
|
### Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @freakk.dev/manyplug
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou para desenvolvimento:
|
||||||
|
```bash
|
||||||
|
git clone https://git.maneos.net/synt-xerror/manyplug
|
||||||
|
cd manyplug
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comandos
|
||||||
|
|
||||||
|
| Comando | Descrição |
|
||||||
|
|---------|-----------|
|
||||||
|
| `manyplug init <nome>` | Cria estrutura de um novo plugin |
|
||||||
|
| `manyplug install [nome]` | Instala do registro ou `--local <caminho>` |
|
||||||
|
| `manyplug list` | Lista plugins instalados |
|
||||||
|
| `manyplug validate [caminho]` | Valida o manyplug.json |
|
||||||
|
|
||||||
|
### Exemplos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Criar novo plugin
|
||||||
|
cd src/plugins
|
||||||
|
manyplug init meu-plugin --category utility
|
||||||
|
|
||||||
|
# Instalar de outro diretório
|
||||||
|
manyplug install --local ../outro-plugin
|
||||||
|
|
||||||
|
# Validar manifesto
|
||||||
|
manyplug validate ./meu-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Índice
|
||||||
|
|
||||||
|
- [ManyPlug CLI](#manyplug-cli)
|
||||||
|
- [Estrutura Básica](#estrutura-básica)
|
||||||
|
- [Manifesto do Plugin](#manifesto-do-plugin-manyplugjson)
|
||||||
|
- [Criando Seu Primeiro Plugin](#criando-seu-primeiro-plugin)
|
||||||
|
- [API de Objetos](#api-de-objetos)
|
||||||
|
- [Expondo API](#expondo-api-para-outros-plugins)
|
||||||
|
- [Traduzindo Seu Plugin](#traduzindo-seu-plugin)
|
||||||
|
- [Tratamento de Erros](#tratamento-de-erros)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura Básica
|
||||||
|
|
||||||
|
```
|
||||||
|
src/plugins/
|
||||||
|
└── meu-plugin/
|
||||||
|
├── index.js
|
||||||
|
├── manyplug.json
|
||||||
|
└── locale/ (opcional)
|
||||||
|
├── en.json
|
||||||
|
├── pt.json
|
||||||
|
└── es.json
|
||||||
|
```
|
||||||
|
|
||||||
|
O `index.js` deve exportar uma função `default` que recebe `{ msg, api }`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
// Sua lógica aqui
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manifesto do Plugin (manyplug.json)
|
||||||
|
|
||||||
|
Todo plugin deve ter um `manyplug.json` na raiz. Ele descreve o plugin e declara dependências npm extras que ele precisar.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "meu-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "utility",
|
||||||
|
"service": false,
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Campos
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| `name` | `string` | Identificador do plugin, deve ser igual ao nome da pasta |
|
||||||
|
| `version` | `string` | Versão semântica (ex: `"1.0.0"`) |
|
||||||
|
| `category` | `string` | Categoria do plugin: `utility`, `media`, `game`, `humor`, `info` |
|
||||||
|
| `service` | `boolean` | `true` se o plugin roda em segundo plano (agendador, listener). `false` se acionado por comando ou evento |
|
||||||
|
| `dependencies` | `object` | Pacotes npm extras necessários, mesmo formato do `package.json` |
|
||||||
|
|
||||||
|
### Exemplo com dependências
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "clima",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "utility",
|
||||||
|
"service": false,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Após adicionar dependências, rode `npm install` na raiz do projeto para instalá-las.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criando Seu Primeiro Plugin
|
||||||
|
|
||||||
|
### Exemplo 1: Comando simples
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/saudacao/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
// Só responde se a mensagem começar com "!oi"
|
||||||
|
if (!msg.is(CMD_PREFIX + "oi")) return;
|
||||||
|
|
||||||
|
await msg.reply("Olá! 👋");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemplo 2: Comando com argumentos
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/calcular/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "calcular")) return;
|
||||||
|
|
||||||
|
// msg.args = ["!calcular", "5", "+", "3"]
|
||||||
|
const [, a, operador, b] = msg.args;
|
||||||
|
|
||||||
|
let resultado;
|
||||||
|
switch (operador) {
|
||||||
|
case "+": resultado = Number(a) + Number(b); break;
|
||||||
|
case "-": resultado = Number(a) - Number(b); break;
|
||||||
|
case "*": resultado = Number(a) * Number(b); break;
|
||||||
|
case "/": resultado = Number(a) / Number(b); break;
|
||||||
|
default: return msg.reply("Operador inválido!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await msg.reply(`Resultado: ${resultado}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exemplo 3: Processando mídia
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/echo-media/index.js
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "echo")) return;
|
||||||
|
|
||||||
|
// Verifica se a mensagem tem mídia
|
||||||
|
if (!msg.hasMedia) {
|
||||||
|
return msg.reply("Envie uma mídia com o comando!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baixa a mídia
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
|
||||||
|
// Reenvia no chat
|
||||||
|
await api.sendSticker(media.data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API de Objetos
|
||||||
|
|
||||||
|
### Objeto `msg`
|
||||||
|
|
||||||
|
| Propriedade | Tipo | Descrição |
|
||||||
|
|-------------|------|-----------|
|
||||||
|
| `msg.body` | `string` | Texto da mensagem |
|
||||||
|
| `msg.args` | `string[]` | Tokens da mensagem |
|
||||||
|
| `msg.type` | `string` | Tipo: `chat`, `image`, `video`, `audio`, `sticker` |
|
||||||
|
| `msg.sender` | `string` | ID do remetente |
|
||||||
|
| `msg.senderName` | `string` | Nome do remetente |
|
||||||
|
| `msg.fromMe` | `boolean` | Se o bot enviou |
|
||||||
|
| `msg.hasMedia` | `boolean` | Se tem mídia |
|
||||||
|
| `msg.hasReply` | `boolean` | Se é resposta |
|
||||||
|
| `msg.isGif` | `boolean` | Se é GIF |
|
||||||
|
| `msg.is(cmd)` | `function` | Verifica se começa com comando |
|
||||||
|
| `msg.reply(text)` | `function` | Responde com quote |
|
||||||
|
| `msg.downloadMedia()` | `function` | Retorna `{ mimetype, data }` |
|
||||||
|
| `msg.getReply()` | `function` | Retorna mensagem citada |
|
||||||
|
|
||||||
|
### Objeto `api`
|
||||||
|
|
||||||
|
| Método | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `api.send(text)` | Envia texto |
|
||||||
|
| `api.sendVideo(path)` | Envia vídeo |
|
||||||
|
| `api.sendAudio(path)` | Envia áudio (voz) |
|
||||||
|
| `api.sendImage(path, caption?)` | Envia imagem |
|
||||||
|
| `api.sendSticker(bufferOrPath)` | Envia figurinha |
|
||||||
|
| `api.getPlugin(name)` | Acessa outro plugin |
|
||||||
|
| `api.chat.id` | ID do chat |
|
||||||
|
| `api.chat.name` | Nome do chat |
|
||||||
|
| `api.chat.isGroup` | Se é grupo |
|
||||||
|
| `api.log.info(...)` | Log informativo |
|
||||||
|
| `api.log.warn(...)` | Log de aviso |
|
||||||
|
| `api.log.error(...)` | Log de erro |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expondo API para Outros Plugins
|
||||||
|
|
||||||
|
Um plugin pode exportar funções para outros usarem:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/utilidades/index.js
|
||||||
|
|
||||||
|
// API pública
|
||||||
|
export const api = {
|
||||||
|
formatarData: (date) => date.toLocaleDateString("pt-BR"),
|
||||||
|
formatarMoeda: (valor) => `R$ ${valor.toFixed(2).replace(".", ",")}`,
|
||||||
|
esperar: (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lógica normal do plugin
|
||||||
|
export default async function ({ msg }) {
|
||||||
|
if (msg.is("!ping")) {
|
||||||
|
await msg.reply("pong!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Outro plugin usando:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/outro/index.js
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
const utils = api.getPlugin("utilidades");
|
||||||
|
|
||||||
|
const data = utils.formatarData(new Date());
|
||||||
|
await msg.reply(`Hoje é ${data}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Traduzindo Seu Plugin
|
||||||
|
|
||||||
|
Cada plugin pode ter suas próprias traduções, completamente independentes do core do bot. O locale do bot (definido no `manybot.conf`) é usado automaticamente.
|
||||||
|
|
||||||
|
### Estrutura
|
||||||
|
|
||||||
|
```
|
||||||
|
src/plugins/
|
||||||
|
└── meu-plugin/
|
||||||
|
├── index.js
|
||||||
|
└── locale/
|
||||||
|
├── en.json
|
||||||
|
├── pt.json
|
||||||
|
└── es.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### locale/pt.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ola": "Olá, {{nome}}! 👋",
|
||||||
|
"erro": {
|
||||||
|
"naoEncontrado": "Item não encontrado."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### index.js
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
import { createPluginI18n } from "../../utils/pluginI18n.js";
|
||||||
|
|
||||||
|
const { t } = createPluginI18n(import.meta.url);
|
||||||
|
|
||||||
|
export default async function ({ msg }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "oi")) return;
|
||||||
|
|
||||||
|
// Chave simples
|
||||||
|
await msg.reply(t("ola", { nome: msg.senderName }));
|
||||||
|
|
||||||
|
// Chave aninhada
|
||||||
|
await msg.reply(t("erro.naoEncontrado"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observações
|
||||||
|
|
||||||
|
- Se o locale configurado não tiver arquivo de tradução, cai automaticamente para `en.json`.
|
||||||
|
- Se a chave não existir em nenhum arquivo, a própria chave é retornada.
|
||||||
|
- Use a sintaxe `{{variavel}}` para interpolação.
|
||||||
|
- Cada plugin gerencia suas próprias traduções — nunca importe `t` do core do bot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tratamento de Erros
|
||||||
|
|
||||||
|
Se um plugin lançar erro, o kernel o desativa automaticamente:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
try {
|
||||||
|
// Código que pode falhar
|
||||||
|
const resultado = await algoPerigoso();
|
||||||
|
await msg.reply(resultado);
|
||||||
|
} catch (erro) {
|
||||||
|
// Loga o erro e notifica
|
||||||
|
api.log.error("Erro no plugin:", erro);
|
||||||
|
await msg.reply("Ops! Algo deu errado.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ativando o Plugin
|
||||||
|
|
||||||
|
Depois de criar, adicione ao `manybot.conf`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PLUGINS=[
|
||||||
|
# ... outros plugins
|
||||||
|
meu-plugin
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o bot para carregar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Veja Também
|
||||||
|
|
||||||
|
- [Referência da API](./API.md)
|
||||||
|
- [Exemplos de plugins](../src/plugins/)
|
||||||
BIN
examples/figurinha.gif
Normal file
BIN
examples/figurinha.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 620 KiB |
137
main.py
137
main.py
@@ -1,137 +0,0 @@
|
|||||||
from selenium import webdriver
|
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from selenium.webdriver.chrome.service import Service
|
|
||||||
from selenium.webdriver.chrome.options import Options
|
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
|
||||||
from selenium.webdriver.common.keys import Keys
|
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
|
||||||
import time
|
|
||||||
import pyperclip
|
|
||||||
import random
|
|
||||||
|
|
||||||
GRUPO = "𝑩𝒂𝒕𝒆 𝒑𝒂𝒑𝒐"
|
|
||||||
BOT_PREFIX = "🤖 *ManyBot:* "
|
|
||||||
PROFILE_DIR = "/home/syntax/whatsapp-profile"
|
|
||||||
CHECK_INTERVAL = 0.5
|
|
||||||
|
|
||||||
def iniciar_driver():
|
|
||||||
print("[DRIVER] Iniciando Chrome...")
|
|
||||||
opts = Options()
|
|
||||||
opts.add_argument(f"--user-data-dir={PROFILE_DIR}")
|
|
||||||
opts.add_argument("--profile-directory=Default")
|
|
||||||
opts.add_argument("--no-sandbox")
|
|
||||||
opts.add_argument("--disable-dev-shm-usage")
|
|
||||||
opts.add_argument("--disable-extensions")
|
|
||||||
opts.add_argument("--disable-gpu")
|
|
||||||
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)
|
|
||||||
driver.get("https://web.whatsapp.com")
|
|
||||||
wait = WebDriverWait(driver, 120)
|
|
||||||
print("[DRIVER] Aguardando QR Code ou login...")
|
|
||||||
wait.until(EC.presence_of_element_located((By.ID, "pane-side")))
|
|
||||||
print("[DRIVER] WhatsApp Web carregado.")
|
|
||||||
return driver, wait
|
|
||||||
|
|
||||||
def abrir_grupo(driver, wait):
|
|
||||||
print(f"[GRUPO] Procurando '{GRUPO}'...")
|
|
||||||
grupo_box = wait.until(EC.presence_of_element_located((By.XPATH, f'//span[@title="{GRUPO}"]')))
|
|
||||||
grupo_box.click()
|
|
||||||
print(f"[GRUPO] Aberto.")
|
|
||||||
|
|
||||||
def pegar_ultima_mensagem(driver):
|
|
||||||
msgs = driver.find_elements(By.CSS_SELECTOR, "[data-testid='selectable-text'] span")
|
|
||||||
if not msgs:
|
|
||||||
return None
|
|
||||||
return msgs[-1].text
|
|
||||||
|
|
||||||
def enviar_mensagem(driver, wait, texto):
|
|
||||||
print(f"[ENVIO] Enviando: '{texto}'")
|
|
||||||
caixa = wait.until(EC.element_to_be_clickable((
|
|
||||||
By.CSS_SELECTOR, "footer div[contenteditable='true'][role='textbox']"
|
|
||||||
)))
|
|
||||||
caixa.click()
|
|
||||||
|
|
||||||
pyperclip.copy(texto)
|
|
||||||
caixa.send_keys(Keys.CONTROL, 'v')
|
|
||||||
|
|
||||||
time.sleep(0.3)
|
|
||||||
caixa.send_keys(Keys.ENTER)
|
|
||||||
print("[ENVIO] Mensagem enviada.")
|
|
||||||
|
|
||||||
def bot_msg(texto):
|
|
||||||
return f"{BOT_PREFIX}\n{texto}"
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
driver, wait = iniciar_driver()
|
|
||||||
abrir_grupo(driver, wait)
|
|
||||||
ultima_mensagem = None
|
|
||||||
|
|
||||||
def jogo():
|
|
||||||
n = random.randint(1, 100)
|
|
||||||
print(f"[JOGO] Jogo iniciado! Número escolhido: {n}")
|
|
||||||
enviar_mensagem(driver, wait, bot_msg("Hora do jogo! Tentem adivinhar qual número de 1 a 100 eu estou pensando!"))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
tentativa = pegar_ultima_mensagem(driver)
|
|
||||||
if not tentativa or tentativa == ultima_mensagem:
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"[JOGO] Nova tentativa: '{tentativa}'")
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
|
|
||||||
if tentativa.isdigit():
|
|
||||||
num = int(tentativa)
|
|
||||||
|
|
||||||
if num == n:
|
|
||||||
enviar_mensagem(driver, wait, bot_msg(f"Parabéns! Você acertou!! O número era: {n}"))
|
|
||||||
break
|
|
||||||
elif num > n:
|
|
||||||
enviar_mensagem(driver, wait, bot_msg(f"Quase! Um pouco menor. Sua resposta: {num}"))
|
|
||||||
elif num < n:
|
|
||||||
enviar_mensagem(driver, wait, bot_msg(f"Quase! Um pouco maior. Sua resposta: {num}"))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERRO] {type(e).__name__}: {e}")
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def processar_comando(texto):
|
|
||||||
tokens = texto.split()
|
|
||||||
|
|
||||||
if tokens[0] == "!many":
|
|
||||||
if len(tokens) == 1: # se só tiver "!many"
|
|
||||||
return bot_msg(
|
|
||||||
"E aí?! Aqui está a lista de todos os meus comandos:\n"
|
|
||||||
"- `!many ping` -> testa se estou funcionando\n"
|
|
||||||
"- `!many jogo` -> jogo de adivinhação\n"
|
|
||||||
"E ai, vai querer qual? 😄"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif tokens[1] == "ping":
|
|
||||||
return bot_msg("pong 🏓")
|
|
||||||
|
|
||||||
elif tokens[1] == "jogo":
|
|
||||||
jogo()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
texto = pegar_ultima_mensagem(driver)
|
|
||||||
if not texto or texto == ultima_mensagem:
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"[MSG] Nova mensagem: '{texto}'")
|
|
||||||
ultima_mensagem = texto
|
|
||||||
time.sleep(CHECK_INTERVAL)
|
|
||||||
|
|
||||||
resposta = processar_comando(texto)
|
|
||||||
if resposta:
|
|
||||||
enviar_mensagem(driver, wait, resposta)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERRO] {type(e).__name__}: {e}")
|
|
||||||
time.sleep(1)
|
|
||||||
267
man/man1/manybot-plugin.1
Normal file
267
man/man1/manybot-plugin.1
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
.TH MANYBOT-PLUGIN 1 "April 2026" "ManyBot 2.4.3" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
manybot-plugin \- ManyBot plugin development guide
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B manyplug.json
|
||||||
|
.I manifest file
|
||||||
|
.br
|
||||||
|
.I src/plugins/
|
||||||
|
.B plugin directory
|
||||||
|
.SH DESCRIPTION
|
||||||
|
ManyBot plugins extend the bot's functionality without modifying the core
|
||||||
|
kernel. The kernel connects to WhatsApp and distributes messages to plugins,
|
||||||
|
which decide how to respond.
|
||||||
|
.PP
|
||||||
|
Each plugin lives in its own folder under
|
||||||
|
.I src/plugins/
|
||||||
|
with a
|
||||||
|
.B manyplug.json
|
||||||
|
manifest file and an
|
||||||
|
.B index.js
|
||||||
|
entry point.
|
||||||
|
.SH PLUGIN STRUCTURE
|
||||||
|
.nf
|
||||||
|
src/plugins/
|
||||||
|
\(do__ my-plugin/
|
||||||
|
\(bu__ manyplug.json # Plugin manifest
|
||||||
|
\(bu__ index.js # Main entry point
|
||||||
|
\(bu__ locale/ # Translations (optional)
|
||||||
|
\(bu__ en.json
|
||||||
|
\(bu__ pt.json
|
||||||
|
\(bu__ es.json
|
||||||
|
.fi
|
||||||
|
.SH MANIFEST (manyplug.json)
|
||||||
|
Every plugin must include a manifest describing its metadata:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
{
|
||||||
|
"name": "my-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "utility",
|
||||||
|
"service": false,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.TP
|
||||||
|
.B name
|
||||||
|
Plugin identifier. Must match the folder name.
|
||||||
|
.TP
|
||||||
|
.B version
|
||||||
|
Semantic version (e.g., "1.0.0").
|
||||||
|
.TP
|
||||||
|
.B category
|
||||||
|
Plugin type: \fButility\fR, \fBmedia\fR, \fBgame\fR, \fBhumor\fR, or \fBinfo\fR.
|
||||||
|
.TP
|
||||||
|
.B service
|
||||||
|
\fBtrue\fR for background plugins (schedulers, listeners).
|
||||||
|
\fBfalse\fR for command/event-triggered plugins.
|
||||||
|
.TP
|
||||||
|
.B dependencies
|
||||||
|
Extra npm packages required. Install with \fBnpm install\fR from project root.
|
||||||
|
.SH PLUGIN ENTRY POINT
|
||||||
|
The
|
||||||
|
.I index.js
|
||||||
|
file must export a default async function:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.B "export default async function ({ msg, api }) {"
|
||||||
|
// Plugin logic here
|
||||||
|
.B "}"
|
||||||
|
.fi
|
||||||
|
.SS Parameters
|
||||||
|
.TP
|
||||||
|
.B msg
|
||||||
|
Message object containing:
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBbody\fR - Full message text
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBargs\fR - Array of message tokens
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBtype\fR - Message type: chat, image, video, audio, sticker, ptt, document, location
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsender\fR - Sender ID (NUMBER@c.us or NUMBER@g.us)
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsenderName\fR - Display name
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBfromMe\fR - True if bot sent the message
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBhasMedia\fR - True if contains media
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBhasReply\fR - True if it's a reply
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBisGif\fR - True if media is GIF
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBtimestamp\fR - Unix timestamp
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBis(cmd)\fR - Check if message starts with command
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBreply(text)\fR - Reply with quote
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBdownloadMedia()\fR - Download media (returns {mimetype, data})
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBgetReply()\fR - Get quoted message object
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B api
|
||||||
|
API object containing:
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsend(text)\fR - Send text message
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsendVideo(path)\fR - Send video file
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsendAudio(path)\fR - Send audio as voice message
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsendImage(path, caption?)\fR - Send image with optional caption
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBsendSticker(bufferOrPath)\fR - Send sticker from buffer or file
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBgetPlugin(name)\fR - Access another plugin's public API
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBchat\fR - Chat info: \fBid\fR, \fBname\fR, \fBisGroup\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
\fBlog.info(...), log.warn(...), log.error(...)\fR - Logging methods
|
||||||
|
.RE
|
||||||
|
.SH EXAMPLES
|
||||||
|
.SS Simple Command
|
||||||
|
.nf
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "hi")) return;
|
||||||
|
await msg.reply("Hello! 👋");
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.SS Command with Arguments
|
||||||
|
.nf
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "calc")) return;
|
||||||
|
|
||||||
|
const [, a, op, b] = msg.args;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case "+": result = Number(a) + Number(b); break;
|
||||||
|
case "-": result = Number(a) - Number(b); break;
|
||||||
|
default: return msg.reply("Invalid operator!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await msg.reply(`Result: ${result}`);
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.SS Processing Media
|
||||||
|
.nf
|
||||||
|
import { CMD_PREFIX } from "../../config.js";
|
||||||
|
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "echo")) return;
|
||||||
|
|
||||||
|
if (!msg.hasMedia) {
|
||||||
|
return msg.reply("Send media with the command!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
await api.sendSticker(media.data);
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.SS Exposing API to Other Plugins
|
||||||
|
.nf
|
||||||
|
// Public API for other plugins
|
||||||
|
export const api = {
|
||||||
|
formatDate: (d) => d.toLocaleDateString("en-US"),
|
||||||
|
wait: (ms) => new Promise(r => setTimeout(r, ms))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normal plugin logic
|
||||||
|
export default async function ({ msg }) {
|
||||||
|
if (msg.is("!ping")) await msg.reply("pong!");
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Used by another plugin:
|
||||||
|
.nf
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
const utils = api.getPlugin("utilities");
|
||||||
|
const date = utils.formatDate(new Date());
|
||||||
|
await msg.reply(`Today is ${date}`);
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.SH TRANSLATIONS
|
||||||
|
Plugins can include their own translations:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
import { createPluginI18n } from "../../utils/pluginI18n.js";
|
||||||
|
|
||||||
|
const { t } = createPluginI18n(import.meta.url);
|
||||||
|
|
||||||
|
export default async function ({ msg }) {
|
||||||
|
if (!msg.is(CMD_PREFIX + "hello")) return;
|
||||||
|
await msg.reply(t("greeting", { name: msg.senderName }));
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Locale file (\fIlocale/en.json\fR):
|
||||||
|
.nf
|
||||||
|
{
|
||||||
|
"greeting": "Hello, {{name}}! 👋"
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
If the configured locale has no translation file, falls back to \fBen.json\fR.
|
||||||
|
Use \fB{{variable}}\fR syntax for interpolation.
|
||||||
|
.SH ENABLING A PLUGIN
|
||||||
|
Add the plugin folder name to
|
||||||
|
.I manybot.confR:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
PLUGINS=[
|
||||||
|
many,
|
||||||
|
figurinha,
|
||||||
|
my-plugin
|
||||||
|
]
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Restart ManyBot to load the plugin.
|
||||||
|
.SH ERROR HANDLING
|
||||||
|
If a plugin throws an error, the kernel automatically disables it.
|
||||||
|
Use try/catch for graceful error handling:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
export default async function ({ msg, api }) {
|
||||||
|
try {
|
||||||
|
const result = await riskyOperation();
|
||||||
|
await msg.reply(result);
|
||||||
|
} catch (error) {
|
||||||
|
api.log.error("Plugin error:", error);
|
||||||
|
await msg.reply("Oops! Something went wrong.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
.SH CONFIGURATION ACCESS
|
||||||
|
Import settings from the main config:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
import { CMD_PREFIX, CLIENT_ID, CHATS, PLUGINS } from "../../config.js";
|
||||||
|
|
||||||
|
// Custom config values also work
|
||||||
|
import { MY_API_KEY } from "../../config.js";
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Add custom values to
|
||||||
|
.I manybot.confR:
|
||||||
|
.nf
|
||||||
|
MY_API_KEY=secret_key_here
|
||||||
|
.fi
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR manybot (1),
|
||||||
|
.BR manyplug (1),
|
||||||
|
.BR manybot.conf (5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Written by synt-xerror.
|
||||||
|
.SH REPORTING BUGS
|
||||||
|
Report bugs at: https://github.com/synt-xerror/manybot/issues
|
||||||
193
man/man1/manybot.1
Normal file
193
man/man1/manybot.1
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
.TH MANYBOT 1 "April 2026" "ManyBot 2.4.3" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
manybot \- local WhatsApp bot with plugin system
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B node
|
||||||
|
.I ./src/main.js
|
||||||
|
.br
|
||||||
|
.B node
|
||||||
|
.I src/utils/get_id.js
|
||||||
|
.br
|
||||||
|
.B bash
|
||||||
|
.I ./setup
|
||||||
|
.br
|
||||||
|
.B bash
|
||||||
|
.I ./update
|
||||||
|
.SH DESCRIPTION
|
||||||
|
ManyBot is a 100%% local WhatsApp bot that operates without relying on the
|
||||||
|
official WhatsApp API. It uses whatsapp-web.js to connect as a regular
|
||||||
|
WhatsApp client and provides a modular plugin system for extending functionality.
|
||||||
|
.PP
|
||||||
|
The bot supports multiple chats in a single session, runs headless (without
|
||||||
|
a GUI), and can be configured through a simple configuration file.
|
||||||
|
.SH COMMANDS
|
||||||
|
.TP
|
||||||
|
.B !many
|
||||||
|
List all available commands from loaded plugins.
|
||||||
|
.TP
|
||||||
|
.B !figurinha
|
||||||
|
Convert images, GIFs, and videos to stickers.
|
||||||
|
.TP
|
||||||
|
.B !video \fIURL\fR
|
||||||
|
Download videos from supported platforms.
|
||||||
|
.TP
|
||||||
|
.B !audio \fIURL\fR
|
||||||
|
Download audio from videos and send as voice message.
|
||||||
|
.TP
|
||||||
|
.B !adivinhacao comecar
|
||||||
|
Start a guessing game (1-100).
|
||||||
|
.TP
|
||||||
|
.B !forca comecar
|
||||||
|
Start a hangman game.
|
||||||
|
.TP
|
||||||
|
.B !obrigado
|
||||||
|
Bot responds with a thank-you message.
|
||||||
|
.PP
|
||||||
|
Command prefix can be configured via
|
||||||
|
.B CMD_PREFIX
|
||||||
|
in
|
||||||
|
.IR manybot.conf .
|
||||||
|
Default is
|
||||||
|
.BR ! .
|
||||||
|
.SH CONFIGURATION
|
||||||
|
ManyBot uses a configuration file
|
||||||
|
.I manybot.conf
|
||||||
|
in the project root. Key options:
|
||||||
|
.TP
|
||||||
|
.B CLIENT_ID=\fIname\fR
|
||||||
|
Unique identifier for the bot session. Creates a session/\fIname\fR folder
|
||||||
|
for authentication data. Default: \fIbot_permanente\fR.
|
||||||
|
.TP
|
||||||
|
.B CMD_PREFIX=\fIchar\fR
|
||||||
|
Character prefixing all commands. Default: \fI!\fR.
|
||||||
|
.TP
|
||||||
|
.B LANGUAGE=\fIcode\fR
|
||||||
|
Bot interface language: \fBen\fR (English), \fBpt\fR (Portuguese), or \fBes\fR (Spanish).
|
||||||
|
Default: \fBen\fR.
|
||||||
|
.TP
|
||||||
|
.B CHATS=[\fIid1\fR, \fIid2\fR, ...]
|
||||||
|
List of allowed chat IDs. Empty array allows all chats.
|
||||||
|
Format: \fBnumber@c.us\fR for private chats, \fBnumber@g.us\fR for groups.
|
||||||
|
.TP
|
||||||
|
.B PLUGINS=[\fIname1\fR, \fIname2\fR, ...]
|
||||||
|
List of plugins to load at startup. Each name corresponds to a folder in
|
||||||
|
.IR src/plugins/ .
|
||||||
|
.PP
|
||||||
|
See
|
||||||
|
.BR manybot.conf (5)
|
||||||
|
for complete configuration reference.
|
||||||
|
.SH PLUGINS
|
||||||
|
Plugins extend ManyBot functionality without modifying the kernel. Each plugin
|
||||||
|
is a folder under
|
||||||
|
.I src/plugins/
|
||||||
|
containing:
|
||||||
|
.TP
|
||||||
|
.I index.js
|
||||||
|
Main plugin file exporting a default async function receiving \fB{ msg, api }\fR.
|
||||||
|
.TP
|
||||||
|
.I manyplug.json
|
||||||
|
Plugin manifest describing name, version, category, service status, and dependencies.
|
||||||
|
.TP
|
||||||
|
.I locale/
|
||||||
|
Optional translation files (\fBen.json\fR, \fBpt.json\fR, \fBes.json\fR).
|
||||||
|
.PP
|
||||||
|
Plugins receive two objects:
|
||||||
|
.TP
|
||||||
|
.B msg
|
||||||
|
Message information including \fBbody\fR, \fBargs\fR, \fBtype\fR, \fBsender\fR,
|
||||||
|
\fBhasMedia\fR, methods \fBis()\fR, \fBreply()\fR, \fBdownloadMedia()\fR.
|
||||||
|
.TP
|
||||||
|
.B api
|
||||||
|
Interaction methods including \fBsend()\fR, \fBsendVideo()\fR, \fBsendAudio()\fR,
|
||||||
|
\fBsendImage()\fR, \fBsendSticker()\fR, \fBgetPlugin()\fR, and \fBlog\fR methods.
|
||||||
|
.SH MANYPLUG
|
||||||
|
ManyPlug is the official CLI for managing ManyBot plugins. Install it with:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
$ npm install -g @freakk.dev/manyplug
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Common commands:
|
||||||
|
.TP
|
||||||
|
.B manyplug init \fIname\fR
|
||||||
|
Create a new plugin boilerplate in the current directory.
|
||||||
|
.TP
|
||||||
|
.B manyplug install [\fIname\fR]
|
||||||
|
Install a plugin from the registry or use \fB--local \fIpath\fR to install from a local directory.
|
||||||
|
.TP
|
||||||
|
.B manyplug list
|
||||||
|
List all installed plugins in \fIsrc/plugins/\fR.
|
||||||
|
.TP
|
||||||
|
.B manyplug validate [\fIpath\fR]
|
||||||
|
Validate the \fImanyplug.json\fR manifest file.
|
||||||
|
.PP
|
||||||
|
See \fBmanyplug(1)\fR for complete documentation.
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I manybot.conf
|
||||||
|
Main configuration file. See \fBmanybot.conf(5)\fR.
|
||||||
|
.TP
|
||||||
|
.I session/
|
||||||
|
Authentication data and WhatsApp session storage.
|
||||||
|
.TP
|
||||||
|
.I src/plugins/
|
||||||
|
Plugin directory containing all installed plugins.
|
||||||
|
.TP
|
||||||
|
.I src/main.js
|
||||||
|
Bot entry point.
|
||||||
|
.TP
|
||||||
|
.I logs/
|
||||||
|
Log files directory.
|
||||||
|
.TP
|
||||||
|
.I update.log
|
||||||
|
Update script log output.
|
||||||
|
.SH ENVIRONMENT
|
||||||
|
.TP
|
||||||
|
.B NODE_ENV
|
||||||
|
Set to \fBproduction\fR to disable development features.
|
||||||
|
.SH EXIT STATUS
|
||||||
|
.TP
|
||||||
|
.B 0
|
||||||
|
Success
|
||||||
|
.TP
|
||||||
|
.B 1
|
||||||
|
General error
|
||||||
|
.TP
|
||||||
|
.B 130
|
||||||
|
Interrupted by user (Ctrl+C)
|
||||||
|
.SH EXAMPLES
|
||||||
|
.SS First run
|
||||||
|
.nf
|
||||||
|
$ node ./src/main.js
|
||||||
|
# Scan QR code with WhatsApp:
|
||||||
|
# Menu \-> Linked Devices \-> Link a Device
|
||||||
|
.fi
|
||||||
|
.SS Get chat IDs
|
||||||
|
.nf
|
||||||
|
$ node src/utils/get_id.js
|
||||||
|
# Send a message in the target chat to see the ID
|
||||||
|
.fi
|
||||||
|
.SS Update to latest version
|
||||||
|
.nf
|
||||||
|
$ bash ./update
|
||||||
|
.fi
|
||||||
|
.SH SECURITY
|
||||||
|
\(bu Bot runs with same privileges as the user running it
|
||||||
|
.br
|
||||||
|
\(bu Session data stored in \fIsession/\fR should be protected (chmod 700)
|
||||||
|
.br
|
||||||
|
\(bu CHATS whitelist recommended to limit bot exposure
|
||||||
|
.br
|
||||||
|
\(bu No official WhatsApp API keys required or used
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR manybot.conf (5),
|
||||||
|
.BR manybot-plugin (1),
|
||||||
|
.BR manyplug (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Written by synt-xerror.
|
||||||
|
.SH COPYRIGHT
|
||||||
|
Licensed under GPLv3. See LICENSE file for details.
|
||||||
|
.br
|
||||||
|
https://github.com/synt-xerror/manybot
|
||||||
|
.SH BUGS
|
||||||
|
Report bugs at: https://github.com/synt-xerror/manybot/issues
|
||||||
225
man/man5/manybot.conf.5
Normal file
225
man/man5/manybot.conf.5
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
.TH MANYBOT.CONF 5 "April 2026" "ManyBot 2.4.3" "File Formats"
|
||||||
|
.SH NAME
|
||||||
|
manybot.conf \- ManyBot configuration file
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.I manybot.conf
|
||||||
|
.SH DESCRIPTION
|
||||||
|
The
|
||||||
|
.I manybot.conf
|
||||||
|
file configures the ManyBot WhatsApp bot. It uses a simple key-value format
|
||||||
|
with support for multiline lists. Comments start with \fB#\fR.
|
||||||
|
.PP
|
||||||
|
The file must be located in the project root directory, alongside
|
||||||
|
.IR package.json .
|
||||||
|
.SH FORMAT
|
||||||
|
.nf
|
||||||
|
# Comments start with '#'
|
||||||
|
|
||||||
|
KEY=value
|
||||||
|
KEY=[item1, item2, item3]
|
||||||
|
.fi
|
||||||
|
.SS Key-Value Pairs
|
||||||
|
Simple configuration values:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
CLIENT_ID=my_bot
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=en
|
||||||
|
.fi
|
||||||
|
.SS Multiline Lists
|
||||||
|
Arrays spanning multiple lines:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
CHATS=[
|
||||||
|
123456789@c.us,
|
||||||
|
123456789@g.us
|
||||||
|
]
|
||||||
|
|
||||||
|
PLUGINS=[
|
||||||
|
many,
|
||||||
|
figurinha,
|
||||||
|
audio,
|
||||||
|
video
|
||||||
|
]
|
||||||
|
.fi
|
||||||
|
.SH OPTIONS
|
||||||
|
.SS Core Settings
|
||||||
|
.TP
|
||||||
|
.B CLIENT_ID=\fIstring\fR
|
||||||
|
Unique identifier for the bot session.
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
Default: \fBbot_permanente\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
Creates a \fIsession/CLIENT_ID/\fR folder for authentication data
|
||||||
|
.IP \(bu 2
|
||||||
|
Changing this starts a new session (requires QR code rescan)
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B CMD_PREFIX=\fIcharacter\fR
|
||||||
|
Character that prefixes all bot commands.
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
Default: \fB!\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
Example: \fB!\fR makes commands like \fB!video\fR, \fB!audio\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
Changing to \fB.\fR would make commands \fB.video\fR, \fB.audio\fR
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B LANGUAGE=\fIcode\fR
|
||||||
|
Bot interface language.
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
Default: \fBen\fR (English)
|
||||||
|
.IP \(bu 2
|
||||||
|
Options: \fBen\fR, \fBpt\fR (Portuguese), \fBes\fR (Spanish)
|
||||||
|
.IP \(bu 2
|
||||||
|
Fallback to English if selected language not found
|
||||||
|
.RE
|
||||||
|
.SS Chat Settings
|
||||||
|
.TP
|
||||||
|
.B CHATS=[\fIid1\fR, \fIid2\fR, ...]
|
||||||
|
Whitelist of chat IDs where the bot responds.
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
Default: \fB[]\fR (empty = respond to all chats)
|
||||||
|
.IP \(bu 2
|
||||||
|
Private chat format: \fBnumber@c.us\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
Group format: \fBnumber@g.us\fR or \fBnumber-number@g.us\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
Use \fBnode src/utils/get_id.js\fR to discover chat IDs
|
||||||
|
.RE
|
||||||
|
.SS Plugin Settings
|
||||||
|
.TP
|
||||||
|
.B PLUGINS=[\fIname1\fR, \fIname2\fR, ...]
|
||||||
|
List of plugins to load at startup.
|
||||||
|
.RS
|
||||||
|
.IP \(bu 2
|
||||||
|
Default: \fB[]\fR (no plugins loaded)
|
||||||
|
.IP \(bu 2
|
||||||
|
Each name must match a folder in \fIsrc/plugins/\fR
|
||||||
|
.IP \(bu 2
|
||||||
|
Order matters: plugins load in listed order
|
||||||
|
.IP \(bu 2
|
||||||
|
Comment out or remove to disable without deleting files
|
||||||
|
.RE
|
||||||
|
.SS Built-in Plugins
|
||||||
|
.TP
|
||||||
|
.B many
|
||||||
|
Lists all available commands. Required for \fB!many\fR to work.
|
||||||
|
.TP
|
||||||
|
.B figurinha
|
||||||
|
Converts images/GIFs/videos to WhatsApp stickers.
|
||||||
|
.TP
|
||||||
|
.B video
|
||||||
|
Downloads videos from URLs.
|
||||||
|
.TP
|
||||||
|
.B audio
|
||||||
|
Downloads audio from videos as voice messages.
|
||||||
|
.TP
|
||||||
|
.B adivinha\(,c\(oao
|
||||||
|
Guessing game (1-100).
|
||||||
|
.TP
|
||||||
|
.B forca
|
||||||
|
Hangman game.
|
||||||
|
.TP
|
||||||
|
.B obrigado
|
||||||
|
Responds to thank-you messages.
|
||||||
|
.SH CUSTOM SETTINGS
|
||||||
|
You can add any custom key-value pairs for use by plugins:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
# In manybot.conf
|
||||||
|
ADMIN_NUMBER=5511999999999@c.us
|
||||||
|
API_KEY=your_secret_key
|
||||||
|
MAX_DOWNLOAD_SIZE=50MB
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Access in plugins:
|
||||||
|
.nf
|
||||||
|
import { ADMIN_NUMBER, API_KEY, MAX_DOWNLOAD_SIZE } from "../../config.js";
|
||||||
|
.fi
|
||||||
|
.SH EXAMPLES
|
||||||
|
.SS Minimal Configuration
|
||||||
|
.nf
|
||||||
|
# Basic bot setup
|
||||||
|
CLIENT_ID=my_bot
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=en
|
||||||
|
|
||||||
|
PLUGINS=[
|
||||||
|
many
|
||||||
|
]
|
||||||
|
.fi
|
||||||
|
.SS Production Configuration
|
||||||
|
.nf
|
||||||
|
# Production bot with whitelist
|
||||||
|
CLIENT_ID=bot_prod
|
||||||
|
CMD_PREFIX=/
|
||||||
|
LANGUAGE=pt
|
||||||
|
|
||||||
|
CHATS=[
|
||||||
|
5511999999999@c.us,
|
||||||
|
5511888888888-123456789@g.us
|
||||||
|
]
|
||||||
|
|
||||||
|
PLUGINS=[
|
||||||
|
many,
|
||||||
|
figurinha,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
obrigado
|
||||||
|
]
|
||||||
|
|
||||||
|
# Custom settings
|
||||||
|
ADMIN_NUMBER=5511999999999@c.us
|
||||||
|
LOG_LEVEL=info
|
||||||
|
.fi
|
||||||
|
.SS Development Configuration
|
||||||
|
.nf
|
||||||
|
# Debug/development setup
|
||||||
|
CLIENT_ID=bot_dev
|
||||||
|
CMD_PREFIX=!
|
||||||
|
LANGUAGE=en
|
||||||
|
|
||||||
|
# Respond to all chats
|
||||||
|
CHATS=[]
|
||||||
|
|
||||||
|
# All plugins for testing
|
||||||
|
PLUGINS=[
|
||||||
|
many,
|
||||||
|
figurinha,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
adivinha\(,c\(oao,
|
||||||
|
forca,
|
||||||
|
obrigado
|
||||||
|
]
|
||||||
|
.fi
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I manybot.conf
|
||||||
|
Main configuration file (must be created by user)
|
||||||
|
.TP
|
||||||
|
.I manybot.conf.example
|
||||||
|
Example configuration with documentation comments
|
||||||
|
.SH NOTES
|
||||||
|
\(bu Keys are case-sensitive
|
||||||
|
.br
|
||||||
|
\(bu Values are read as strings unless they're list syntax
|
||||||
|
.br
|
||||||
|
\(bu Inline comments supported: \fBKEY=value # comment\fR
|
||||||
|
.br
|
||||||
|
\(bu Multiline lists must end with \fB]\fR on its own line or last item line
|
||||||
|
.br
|
||||||
|
\(bu Whitespace in values is trimmed
|
||||||
|
.br
|
||||||
|
\(u Missing optional values use built-in defaults
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR manybot (1),
|
||||||
|
.BR manybot-plugin (1),
|
||||||
|
.BR manyplug (1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Written by synt-xerror.
|
||||||
30
manybot.conf.example
Normal file
30
manybot.conf.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# ManyBot Configuration File
|
||||||
|
# Copy this file to manybot.conf and customize as needed
|
||||||
|
|
||||||
|
# Bot identification
|
||||||
|
CLIENT_ID=meu_bot
|
||||||
|
|
||||||
|
# Command prefix (default: !)
|
||||||
|
CMD_PREFIX=!
|
||||||
|
|
||||||
|
# Language setting (en, pt, es)
|
||||||
|
# Default: en (English)
|
||||||
|
LANGUAGE=en
|
||||||
|
|
||||||
|
# Allowed chats (leave empty for all chats)
|
||||||
|
# Format: chatId1@g.us, chatId2@g.us for groups
|
||||||
|
# Format: number@c.us for private chats
|
||||||
|
CHATS=[
|
||||||
|
]
|
||||||
|
|
||||||
|
# Active plugins
|
||||||
|
PLUGINS=[
|
||||||
|
many,
|
||||||
|
figurinha,
|
||||||
|
audio,
|
||||||
|
video,
|
||||||
|
adivinhação,
|
||||||
|
forca,
|
||||||
|
obrigado,
|
||||||
|
a
|
||||||
|
]
|
||||||
3894
package-lock.json
generated
Normal file
3894
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Executable file
19
package.json
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "manybot",
|
||||||
|
"version": "2.4.3",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^7",
|
||||||
|
"node-gyp": "^12.2.0",
|
||||||
|
"node-webpmux": "^3.2.1",
|
||||||
|
"qrcode-terminal": "^0.12.0",
|
||||||
|
"wa-sticker-formatter": "^4.4.4",
|
||||||
|
"whatsapp-web.js": "^1.24.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"conventional-changelog-cli": "^5.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
203
setup
Executable file
203
setup
Executable file
@@ -0,0 +1,203 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Salvando diretório para evitar problemas
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Cores
|
||||||
|
# ------------------------
|
||||||
|
RESET="\033[0m"
|
||||||
|
BOLD="\033[1m"
|
||||||
|
|
||||||
|
RED="\033[31m"
|
||||||
|
GREEN="\033[32m"
|
||||||
|
YELLOW="\033[33m"
|
||||||
|
BLUE="\033[34m"
|
||||||
|
CYAN="\033[36m"
|
||||||
|
MAGENTA="\033[35m"
|
||||||
|
GRAY="\033[90m"
|
||||||
|
|
||||||
|
timestamp() {
|
||||||
|
date +"%H:%M:%S"
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local level="$1"
|
||||||
|
local color="$2"
|
||||||
|
shift 2
|
||||||
|
echo -e "${GRAY}[$(timestamp)]${RESET} ${color}${level}${RESET} $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info() { log "[INFO]" "$BLUE" "$@"; }
|
||||||
|
log_ok() { log "[OK]" "$GREEN" "$@"; }
|
||||||
|
log_warn() { log "[WARN]" "$YELLOW" "$@"; }
|
||||||
|
log_error() { log "[ERROR]" "$RED" "$@"; }
|
||||||
|
log_cmd() { log "[CMD]" "$CYAN" "${BOLD}$*${RESET}"; }
|
||||||
|
log_debug() { log "[DBG]" "$GRAY" "$@"; }
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Banner
|
||||||
|
# ------------------------
|
||||||
|
print_banner() {
|
||||||
|
echo -e "${MAGENTA}${BOLD}"
|
||||||
|
cat << "EOF"
|
||||||
|
_____ _____ _
|
||||||
|
| |___ ___ _ _| __ |___| |_
|
||||||
|
| | | | .'| | | | __ -| . | _|
|
||||||
|
|_|_|_|__,|_|_|_ |_____|___|_|
|
||||||
|
|___|
|
||||||
|
|
||||||
|
website: www.mlplovers.com.br/manybot
|
||||||
|
repos: git.maneos.net/synt-xerror/manybot
|
||||||
|
codeberg.org/synt-xerror/manybot
|
||||||
|
|
||||||
|
EOF
|
||||||
|
echo -e "${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_banner
|
||||||
|
log_info "Inicializando setup..."
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Executar comandos
|
||||||
|
# ------------------------
|
||||||
|
run_cmd() {
|
||||||
|
log_cmd "$*"
|
||||||
|
"$@"
|
||||||
|
log_ok "Comando finalizado: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Download
|
||||||
|
# ------------------------
|
||||||
|
download_file() {
|
||||||
|
local url="$1"
|
||||||
|
local dest="$2"
|
||||||
|
|
||||||
|
log_debug "download_file(url=$url, dest=$dest)"
|
||||||
|
|
||||||
|
if [[ -f "$dest" ]]; then
|
||||||
|
log_warn "Arquivo já existe: $dest"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Baixando $url"
|
||||||
|
log_debug "Destino: $dest"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
log_debug "Downloader: curl"
|
||||||
|
curl -L "$url" -o "$dest"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
log_debug "Downloader: wget"
|
||||||
|
wget "$url" -O "$dest"
|
||||||
|
else
|
||||||
|
log_error "curl ou wget não encontrados"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$dest" 2>/dev/null || true
|
||||||
|
log_ok "Arquivo pronto: $dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Detectar plataforma
|
||||||
|
# ------------------------
|
||||||
|
log_info "Detectando plataforma"
|
||||||
|
|
||||||
|
UNAME="$(uname -s)"
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
|
||||||
|
PLATFORM=""
|
||||||
|
case "$UNAME" in
|
||||||
|
Linux*) PLATFORM="linux";;
|
||||||
|
Darwin*) PLATFORM="mac";;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) PLATFORM="win";;
|
||||||
|
*) PLATFORM="unknown";;
|
||||||
|
esac
|
||||||
|
|
||||||
|
log_info "Sistema: $UNAME"
|
||||||
|
log_info "Arquitetura: $ARCH"
|
||||||
|
log_info "Plataforma: $PLATFORM"
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Informações do ambiente
|
||||||
|
# ------------------------
|
||||||
|
log_info "Verificando ambiente"
|
||||||
|
|
||||||
|
log_debug "Node: $(node -v 2>/dev/null || echo 'não encontrado')"
|
||||||
|
log_debug "npm: $(npm -v 2>/dev/null || echo 'não encontrado')"
|
||||||
|
log_debug "PREFIX: ${PREFIX:-<vazio>}"
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Termux
|
||||||
|
# ------------------------
|
||||||
|
install_deps() {
|
||||||
|
local packages=("$@")
|
||||||
|
|
||||||
|
for pkg in "${packages[@]}"; do
|
||||||
|
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
|
||||||
|
log_warn "$pkg não encontrado, instalando"
|
||||||
|
run_cmd pkg install -y "$pkg"
|
||||||
|
else
|
||||||
|
log_ok "$pkg já instalado"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$PREFIX" == *"com.termux"* ]]; then
|
||||||
|
log_info "Ambiente Termux detectado"
|
||||||
|
|
||||||
|
deps=(
|
||||||
|
chromium
|
||||||
|
)
|
||||||
|
|
||||||
|
install_deps "${deps[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Setup npm
|
||||||
|
# ------------------------
|
||||||
|
log_info "Instalando dependências npm"
|
||||||
|
|
||||||
|
export PUPPETEER_SKIP_DOWNLOAD=1
|
||||||
|
run_cmd npm install
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Chrome Puppeeter
|
||||||
|
# ------------------------
|
||||||
|
log_info "Instalando Chrome"
|
||||||
|
|
||||||
|
npx puppeteer browsers install chrome
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# ManyPlug CLI
|
||||||
|
# ------------------------
|
||||||
|
log_info "Instalando ManyPlug CLI"
|
||||||
|
|
||||||
|
if ! command -v manyplug &>/dev/null; then
|
||||||
|
log_info "ManyPlug não encontrado, instalando globalmente..."
|
||||||
|
npm install -g @freakk.dev/manyplug
|
||||||
|
log_ok "ManyPlug instalado com sucesso"
|
||||||
|
else
|
||||||
|
log_ok "ManyPlug já está instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Configuração de exemplo
|
||||||
|
# ------------------------
|
||||||
|
log_info "Verificando configuração"
|
||||||
|
|
||||||
|
if [[ ! -f "$SCRIPT_DIR/manybot.conf" ]]; then
|
||||||
|
if [[ -f "$SCRIPT_DIR/manybot.conf.example" ]]; then
|
||||||
|
cp "$SCRIPT_DIR/manybot.conf.example" "$SCRIPT_DIR/manybot.conf"
|
||||||
|
log_ok "Arquivo manybot.conf criado a partir do exemplo"
|
||||||
|
log_warn "Edite o manybot.conf para configurar seu bot antes de executar"
|
||||||
|
else
|
||||||
|
log_warn "Arquivo manybot.conf.example não encontrado"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_ok "manybot.conf já existe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_ok "Setup concluído com sucesso.\nRode sempre na raíz: 'node src/main.js' para rodar o bot."
|
||||||
48
src/client/banner.js
Normal file
48
src/client/banner.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { c } from "../logger/formatter.js";
|
||||||
|
|
||||||
|
export function printBanner() {
|
||||||
|
|
||||||
|
const banner = [
|
||||||
|
` _____ _____ _ `,
|
||||||
|
` | |___ ___ _ _| __ |___| |_ `,
|
||||||
|
` | | | | . | | | | __ -| . | _|`,
|
||||||
|
` |_|_|_|__,|_|_|_ |_____|___|_| `,
|
||||||
|
` |___| `
|
||||||
|
];
|
||||||
|
|
||||||
|
const pony = [
|
||||||
|
` ⠴⢮⠭⠍⠉⠉⠒⠤⣀`,
|
||||||
|
` ⢀⢊ ⢱⠊⠑⡀`,
|
||||||
|
` ⠋⡎ ⣀⡠⠤⠠⠖⠋⢉⠉ ⡄⢸`,
|
||||||
|
` ⣘⡠⠊⣩⡅ ⣴⡟⣯⠙⣊ ⢁⠜`,
|
||||||
|
` ⣿⡇⢸⣿⣷⡿⢀⠇⢀⢎`,
|
||||||
|
` ⠰⡉ ⠈⠛⠛⠋⠁⢀⠜ ⢂`,
|
||||||
|
` ⠈⠒⠒⡲⠂⣠⣔⠁ ⡇ ⢀⡴⣾⣛⡛⠻⣦`,
|
||||||
|
` ⢠⠃ ⢠⠞ ⡸⠉⠲⣿⠿⢿⣿⣿⣷⡌⢷`,
|
||||||
|
` ⢀⠔⠂⢼ ⡎⡔⡄⠰⠃ ⢣ ⢻⣿⣿⣿⠘⣷`,
|
||||||
|
` ⡐⠁ ⠸⡀ ⠏ ⠈⠃ ⢸ ⣿⣿⣿⡇⣿⡇`,
|
||||||
|
` ⡇ ⡎⠉⠉⢳ ⡤⠤⡤⠲⡀ ⢇ ⣿⣿⣿⣇⣿⣷`,
|
||||||
|
` ⡇ ⡠⠃ ⡸ ⡇ ⡇ ⢱⡀ ⢣ ⣿⣿⣿⣿⣿⡄`,
|
||||||
|
` ⠑⠊ ⢰ ⠇ ⢸ ⡇⡇ ⡇ ⢳⣿⣿⣿⣿⡇`,
|
||||||
|
` ⢠⠃ ⡸ ⡎ ⡜⡇ ⡇ ⠻⡏⠻⣿⣿⣄`,
|
||||||
|
` ⣔⣁⣀⣀⡠⠁ ⠈⠉⠉⠁⣎⣀⣀⣀⡸`
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`${c.blue}${c.bold}`);
|
||||||
|
|
||||||
|
const max = Math.max(banner.length, pony.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < max; i++) {
|
||||||
|
const left = banner[i] || " ".repeat(banner[0].length);
|
||||||
|
const right = pony[i] || "";
|
||||||
|
console.log(left + " " + right);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(` website : ${c.reset}${c.cyan}www.mlplovers.com.br/manybot${c.reset}`);
|
||||||
|
console.log(` repo : ${c.reset}${c.cyan}github.com/synt-xerror/manybot${c.reset}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
25
src/client/environment.js
Normal file
25
src/client/environment.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import os from "os";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if running inside Termux.
|
||||||
|
*/
|
||||||
|
export const isTermux =
|
||||||
|
(os.platform() === "linux" || os.platform() === "android") &&
|
||||||
|
process.env.PREFIX?.startsWith("/data/data/com.termux");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return Puppeteer config suitable for the environment.
|
||||||
|
* @returns {import("puppeteer").LaunchOptions}
|
||||||
|
*/
|
||||||
|
export function resolvePuppeteerConfig() {
|
||||||
|
if (!isTermux) return {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
executablePath: "/data/data/com.termux/files/usr/bin/chromium-browser",
|
||||||
|
args: [
|
||||||
|
"--headless=new", "--no-sandbox", "--disable-setuid-sandbox",
|
||||||
|
"--disable-dev-shm-usage", "--disable-gpu", "--single-process",
|
||||||
|
"--no-zygote", "--disable-software-rasterizer",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
26
src/client/qrHandler.js
Normal file
26
src/client/qrHandler.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import qrcode from "qrcode-terminal";
|
||||||
|
import path from "path";
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
import { isTermux } from "./environment.js";
|
||||||
|
|
||||||
|
const QR_PATH = path.resolve("qr.png");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or save QR Code based on environment.
|
||||||
|
* @param {string} qr — raw string from "qr" event
|
||||||
|
*/
|
||||||
|
export async function handleQR(qr) {
|
||||||
|
if (isTermux) {
|
||||||
|
try {
|
||||||
|
await QRCode.toFile(QR_PATH, qr, { width: 400 });
|
||||||
|
logger.info(t("system.qrSaved", { path: QR_PATH }));
|
||||||
|
logger.info(t("system.qrOpen"));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(t("system.qrSaveFailed"), err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(t("system.qrScan"));
|
||||||
|
qrcode.generate(qr, { small: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/client/whatsappClient.js
Normal file
49
src/client/whatsappClient.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import pkg from "whatsapp-web.js";
|
||||||
|
import { CLIENT_ID } from "../config.js";
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
import { isTermux, resolvePuppeteerConfig } from "./environment.js";
|
||||||
|
import { handleQR } from "./qrHandler.js";
|
||||||
|
import { printBanner } from "./banner.js";
|
||||||
|
|
||||||
|
export const { Client, LocalAuth, MessageMedia } = pkg;
|
||||||
|
|
||||||
|
// ── Environment ───────────────────────────────────────────────
|
||||||
|
logger.info(isTermux
|
||||||
|
? t("system.environmentTermux")
|
||||||
|
: t("system.environment", { platform: process.platform, puppeteer: "system Puppeteer" })
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Instance ──────────────────────────────────────────────────
|
||||||
|
export const client = new Client({
|
||||||
|
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
|
||||||
|
puppeteer: {
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
...(resolvePuppeteerConfig().args || [])
|
||||||
|
],
|
||||||
|
...resolvePuppeteerConfig()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Events ────────────────────────────────────────────────────
|
||||||
|
client.on("qr", handleQR);
|
||||||
|
|
||||||
|
client.on("ready", () => {
|
||||||
|
printBanner();
|
||||||
|
logger.success(t("system.connected"));
|
||||||
|
logger.info(t("system.clientId", { id: CLIENT_ID }));
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("disconnected", (reason) => {
|
||||||
|
logger.warn(t("system.disconnected", { reason }));
|
||||||
|
logger.info(t("system.reconnecting", { seconds: 5 }));
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info(t("system.reinitializing"));
|
||||||
|
client.initialize();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default client;
|
||||||
81
src/config.js
Normal file
81
src/config.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* config.js
|
||||||
|
*
|
||||||
|
* Reads and parses manybot.conf.
|
||||||
|
* Supports multiline lists and inline comments.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
function parseConf(raw) {
|
||||||
|
const lines = raw.split("\n");
|
||||||
|
|
||||||
|
const cleaned = [];
|
||||||
|
let insideList = false;
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.replace(/#.*$/, "").trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
if (!insideList) {
|
||||||
|
if (line.includes("=[") && !line.includes("]")) {
|
||||||
|
insideList = true;
|
||||||
|
buffer = line;
|
||||||
|
} else {
|
||||||
|
cleaned.push(line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer += line;
|
||||||
|
if (line.includes("]")) {
|
||||||
|
insideList = false;
|
||||||
|
cleaned.push(buffer);
|
||||||
|
buffer = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const line of cleaned) {
|
||||||
|
const eqIdx = line.indexOf("=");
|
||||||
|
if (eqIdx === -1) continue;
|
||||||
|
|
||||||
|
const key = line.slice(0, eqIdx).trim();
|
||||||
|
const raw = line.slice(eqIdx + 1).trim();
|
||||||
|
|
||||||
|
if (raw.startsWith("[") && raw.endsWith("]")) {
|
||||||
|
result[key] = raw
|
||||||
|
.slice(1, -1)
|
||||||
|
.split(",")
|
||||||
|
.map(x => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
} else {
|
||||||
|
result[key] = raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, "../manybot.conf");
|
||||||
|
|
||||||
|
const raw = fs.readFileSync(filePath, "utf8");
|
||||||
|
const config = parseConf(raw);
|
||||||
|
|
||||||
|
export const CLIENT_ID = config.CLIENT_ID ?? "bot_permanente";
|
||||||
|
export const CMD_PREFIX = config.CMD_PREFIX ?? "!";
|
||||||
|
export const CHATS = config.CHATS ?? [];
|
||||||
|
|
||||||
|
/** Active plugin list — e.g., PLUGINS=[video, audio, hello] */
|
||||||
|
export const PLUGINS = config.PLUGINS ?? [];
|
||||||
|
|
||||||
|
/** Bot language — e.g., LANGUAGE=en (fallback: en) */
|
||||||
|
export const LANGUAGE = config.LANGUAGE ?? "en";
|
||||||
|
|
||||||
|
/** Export full config for plugins that need custom values */
|
||||||
|
export const CONFIG = config;
|
||||||
55
src/download/queue.js
Normal file
55
src/download/queue.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* src/download/queue.js
|
||||||
|
*
|
||||||
|
* Sequential execution queue for heavy jobs (downloads, conversions).
|
||||||
|
* Ensures only one job runs at a time — without overloading yt-dlp or ffmpeg.
|
||||||
|
*
|
||||||
|
* Plugin passes a `workFn` that does everything: download, convert, send.
|
||||||
|
* Queue only handles sequence and error handling.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { enqueue } from "../../src/download/queue.js";
|
||||||
|
* enqueue(async () => { ... all plugin logic ... }, onError);
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* workFn: () => Promise<void>,
|
||||||
|
* errorFn: (err: Error) => Promise<void>,
|
||||||
|
* }} Job
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Job[]} */
|
||||||
|
let queue = [];
|
||||||
|
let processing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add job to queue and start processing if idle.
|
||||||
|
*
|
||||||
|
* @param {Function} workFn — async () => void — all plugin logic
|
||||||
|
* @param {Function} errorFn — async (err) => void — called if workFn throws
|
||||||
|
*/
|
||||||
|
export function enqueue(workFn, errorFn) {
|
||||||
|
queue.push({ workFn, errorFn });
|
||||||
|
if (!processing) processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processQueue() {
|
||||||
|
processing = true;
|
||||||
|
while (queue.length) {
|
||||||
|
await processJob(queue.shift());
|
||||||
|
}
|
||||||
|
processing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processJob({ workFn, errorFn }) {
|
||||||
|
try {
|
||||||
|
await workFn();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(t("system.downloadJobFailed", { message: err.message }));
|
||||||
|
try { await errorFn(err); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/i18n/index.js
Normal file
235
src/i18n/index.js
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* i18n/index.js
|
||||||
|
*
|
||||||
|
* Internationalization system for ManyBot.
|
||||||
|
* Loads translations based on LANGUAGE configuration.
|
||||||
|
* Fallback is always English (en).
|
||||||
|
*
|
||||||
|
* Plugins can use createPluginT() to have isolated i18n.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { CONFIG } from "../config.js";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const LOCALES_DIR = path.join(__dirname, "..", "locales");
|
||||||
|
|
||||||
|
// Default language (fallback)
|
||||||
|
const DEFAULT_LANG = "en";
|
||||||
|
|
||||||
|
// Cache of loaded translations
|
||||||
|
const translations = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a translation JSON file
|
||||||
|
* @param {string} lang - language code (en, pt, es)
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
function loadLocale(lang) {
|
||||||
|
if (translations.has(lang)) {
|
||||||
|
return translations.get(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(LOCALES_DIR, `${lang}.json`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
translations.set(lang, data);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[i18n] Failed to load locale ${lang}:`, err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets configured language or default
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getConfiguredLang() {
|
||||||
|
const lang = CONFIG.LANGUAGE?.trim().toLowerCase();
|
||||||
|
if (!lang) return DEFAULT_LANG;
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const filePath = path.join(LOCALES_DIR, `${lang}.json`);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.warn(`[i18n] Language "${lang}" not found, falling back to "${DEFAULT_LANG}"`);
|
||||||
|
return DEFAULT_LANG;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load languages
|
||||||
|
const currentLang = getConfiguredLang();
|
||||||
|
const currentTranslations = loadLocale(currentLang) || {};
|
||||||
|
const fallbackTranslations = loadLocale(DEFAULT_LANG) || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a nested value from an object using dot path
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} key - path like "system.connected"
|
||||||
|
* @returns {string|undefined}
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj, key) {
|
||||||
|
const parts = key.split(".");
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (current === null || current === undefined || typeof current !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces placeholders {{key}} with values from context
|
||||||
|
* @param {string} str
|
||||||
|
* @param {object} context
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function interpolate(str, context = {}) {
|
||||||
|
return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
||||||
|
return context[key] !== undefined ? String(context[key]) : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main translation function
|
||||||
|
* @param {string} key - translation key (e.g., "system.connected")
|
||||||
|
* @param {object} context - values to interpolate {{key}}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function t(key, context = {}) {
|
||||||
|
// Try current language first
|
||||||
|
let value = getNestedValue(currentTranslations, key);
|
||||||
|
|
||||||
|
// Fallback to English if not found
|
||||||
|
if (value === undefined) {
|
||||||
|
value = getNestedValue(fallbackTranslations, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found, return the key
|
||||||
|
if (value === undefined) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not string, convert
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate values
|
||||||
|
return interpolate(value, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an isolated translation function for a plugin.
|
||||||
|
* Plugins should have their own locale/ folder with en.json, es.json, etc.
|
||||||
|
*
|
||||||
|
* Usage in plugin:
|
||||||
|
* import { createPluginT } from "../../i18n/index.js";
|
||||||
|
* const { t } = createPluginT(import.meta.url);
|
||||||
|
*
|
||||||
|
* Folder structure:
|
||||||
|
* myPlugin/
|
||||||
|
* index.js
|
||||||
|
* locale/
|
||||||
|
* en.json
|
||||||
|
* es.json
|
||||||
|
* pt.json
|
||||||
|
*
|
||||||
|
* @param {string} pluginMetaUrl - import.meta.url from the plugin
|
||||||
|
* @returns {{ t: Function, lang: string }}
|
||||||
|
*/
|
||||||
|
export function createPluginT(pluginMetaUrl) {
|
||||||
|
const pluginDir = path.dirname(fileURLToPath(pluginMetaUrl));
|
||||||
|
const pluginLocaleDir = path.join(pluginDir, "locale");
|
||||||
|
|
||||||
|
// Get bot's configured language
|
||||||
|
const targetLang = currentLang;
|
||||||
|
|
||||||
|
// Load plugin translations
|
||||||
|
let pluginTranslations = {};
|
||||||
|
let pluginFallback = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to load the configured language
|
||||||
|
const targetPath = path.join(pluginLocaleDir, `${targetLang}.json`);
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
pluginTranslations = JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always load English as fallback
|
||||||
|
const fallbackPath = path.join(pluginLocaleDir, `${DEFAULT_LANG}.json`);
|
||||||
|
if (fs.existsSync(fallbackPath)) {
|
||||||
|
pluginFallback = JSON.parse(fs.readFileSync(fallbackPath, "utf8"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silent fail - plugin may not have translations
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin-specific translation function
|
||||||
|
* @param {string} key
|
||||||
|
* @param {object} context
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function pluginT(key, context = {}) {
|
||||||
|
// Try plugin's target language first
|
||||||
|
let value = getNestedValue(pluginTranslations, key);
|
||||||
|
|
||||||
|
// Fallback to plugin's English
|
||||||
|
if (value === undefined) {
|
||||||
|
value = getNestedValue(pluginFallback, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found, return the key
|
||||||
|
if (value === undefined) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolate(value, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { t: pluginT, lang: targetLang };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads translations (useful for hot-reload)
|
||||||
|
*/
|
||||||
|
export function reloadTranslations() {
|
||||||
|
translations.clear();
|
||||||
|
const lang = getConfiguredLang();
|
||||||
|
const newTranslations = loadLocale(lang) || {};
|
||||||
|
const newFallback = loadLocale(DEFAULT_LANG) || {};
|
||||||
|
|
||||||
|
// Update references
|
||||||
|
Object.assign(currentTranslations, newTranslations);
|
||||||
|
Object.assign(fallbackTranslations, newFallback);
|
||||||
|
|
||||||
|
console.log(`[i18n] Translations reloaded for language: ${lang}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current language
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getCurrentLang() {
|
||||||
|
return currentLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { t, createPluginT, reloadTranslations, getCurrentLang };
|
||||||
43
src/kernel/messageHandler.js
Normal file
43
src/kernel/messageHandler.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* messageHandler.js
|
||||||
|
*
|
||||||
|
* Central pipeline for received messages.
|
||||||
|
*
|
||||||
|
* Order:
|
||||||
|
* 1. Filter allowed chats (CHATS from .conf)
|
||||||
|
* — if CHATS is empty, accepts all chats
|
||||||
|
* 2. Log the message
|
||||||
|
* 3. Pass context to all active plugins
|
||||||
|
*
|
||||||
|
* Kernel knows no commands — only distributes.
|
||||||
|
* Each plugin decides on its own whether to act or ignore.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CHATS } from "../config.js";
|
||||||
|
import { getChatId } from "../utils/getChatId.js";
|
||||||
|
import { buildApi } from "./pluginApi.js";
|
||||||
|
import { pluginRegistry } from "./pluginLoader.js";
|
||||||
|
import { runPlugin } from "./pluginGuard.js";
|
||||||
|
import { buildMessageContext } from "../logger/messageContext.js";
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import client from "../client/whatsappClient.js";
|
||||||
|
|
||||||
|
export async function handleMessage(msg) {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
const chatId = getChatId(chat);
|
||||||
|
|
||||||
|
// CHATS empty = accepts all chats
|
||||||
|
if (CHATS.length > 0 && !CHATS.includes(chatId)) return;
|
||||||
|
|
||||||
|
const ctx = await buildMessageContext(msg, chat);
|
||||||
|
logger.msg(ctx);
|
||||||
|
|
||||||
|
const api = buildApi({ msg, chat, client, pluginRegistry });
|
||||||
|
const context = { msg: api.msg, chat: api.chat, api };
|
||||||
|
|
||||||
|
for (const plugin of pluginRegistry.values()) {
|
||||||
|
await runPlugin(plugin, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.done("message_create", `de +${ctx.senderNumber}`);
|
||||||
|
}
|
||||||
278
src/kernel/pluginApi.js
Normal file
278
src/kernel/pluginApi.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* pluginApi.js
|
||||||
|
*
|
||||||
|
* Builds the `api` object each plugin receives.
|
||||||
|
* Plugins can only do what's here — never touch client directly.
|
||||||
|
*
|
||||||
|
* `chat` is already filtered by kernel (only allowed chats from .conf),
|
||||||
|
* so plugins don't need and can't choose destination.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import pkg from "whatsapp-web.js";
|
||||||
|
|
||||||
|
const { MessageMedia } = pkg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} params
|
||||||
|
* @param {import("whatsapp-web.js").Message} params.msg
|
||||||
|
* @param {import("whatsapp-web.js").Chat} params.chat
|
||||||
|
* @param {Map<string, any>} params.pluginRegistry
|
||||||
|
* @returns {object} api
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Setup API — without message context.
|
||||||
|
* Passed to plugin.setup(api) during initialization.
|
||||||
|
* Only has sendTo variants, log and schedule.
|
||||||
|
*/
|
||||||
|
export function buildSetupApi(client) {
|
||||||
|
return {
|
||||||
|
async sendTo(chatId, text) {
|
||||||
|
return client.sendMessage(chatId, text);
|
||||||
|
},
|
||||||
|
async sendVideoTo(chatId, filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { caption });
|
||||||
|
},
|
||||||
|
async sendAudioTo(chatId, filePath) {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { sendAudioAsVoice: true });
|
||||||
|
},
|
||||||
|
async sendImageTo(chatId, filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { caption });
|
||||||
|
},
|
||||||
|
async sendStickerTo(chatId, source) {
|
||||||
|
const media = typeof source === "string"
|
||||||
|
? MessageMedia.fromFilePath(source)
|
||||||
|
: new MessageMedia("image/webp", source.toString("base64"));
|
||||||
|
return client.sendMessage(chatId, media, { sendMediaAsSticker: true });
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
info: (...a) => logger.info(...a),
|
||||||
|
warn: (...a) => logger.warn(...a),
|
||||||
|
error: (...a) => logger.error(...a),
|
||||||
|
success: (...a) => logger.success(...a),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApi({ msg, chat, client, pluginRegistry }) {
|
||||||
|
|
||||||
|
const currentChat = chat;
|
||||||
|
|
||||||
|
return {
|
||||||
|
|
||||||
|
// ── Message reading ─────────────────────────────────────
|
||||||
|
|
||||||
|
msg: {
|
||||||
|
/** Message body */
|
||||||
|
body: msg.body ?? "",
|
||||||
|
|
||||||
|
/** Type: "chat", "image", "video", "audio", "ptt", "sticker", "document" */
|
||||||
|
type: msg.type,
|
||||||
|
|
||||||
|
/** true if message came from bot itself */
|
||||||
|
fromMe: msg.fromMe,
|
||||||
|
|
||||||
|
/** Sender ID (ex: "5511999999999@c.us") */
|
||||||
|
sender: msg.author || msg.from,
|
||||||
|
|
||||||
|
/** Display name of sender */
|
||||||
|
senderName: msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, ""),
|
||||||
|
|
||||||
|
/** Tokens: ["!video", "https://..."] */
|
||||||
|
args: msg.body?.trim().split(/\s+/) ?? [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if message is a specific command.
|
||||||
|
* @param {string} cmd — ex: "!hello"
|
||||||
|
*/
|
||||||
|
is(cmd) {
|
||||||
|
return msg.body?.trim().toLowerCase().startsWith(cmd.toLowerCase());
|
||||||
|
},
|
||||||
|
|
||||||
|
/** true if message has attached media */
|
||||||
|
hasMedia: msg.hasMedia,
|
||||||
|
|
||||||
|
/** true if media is a GIF (short looping video) */
|
||||||
|
isGif: msg._data?.isGif ?? false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download message media.
|
||||||
|
* Returns neutral object { mimetype, data } — without exposing MessageMedia.
|
||||||
|
* @returns {Promise<{ mimetype: string, data: string } | null>}
|
||||||
|
*/
|
||||||
|
async downloadMedia() {
|
||||||
|
const media = await msg.downloadMedia();
|
||||||
|
if (!media) return null;
|
||||||
|
return { mimetype: media.mimetype, data: media.data };
|
||||||
|
},
|
||||||
|
|
||||||
|
/** true if message is a reply to another */
|
||||||
|
hasReply: msg.hasQuotedMsg,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns quoted message if exists.
|
||||||
|
* @returns {Promise<import("whatsapp-web.js").Message|null>}
|
||||||
|
*/
|
||||||
|
async getReply() {
|
||||||
|
if (!msg.hasQuotedMsg) return null;
|
||||||
|
return msg.getQuotedMessage();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reply directly to message (with quote).
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
async reply(text) {
|
||||||
|
return msg.reply(text);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Send to current chat ─────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send plain text.
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
async send(text) {
|
||||||
|
return currentChat.sendMessage(text);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send media (image, video, audio, document).
|
||||||
|
* @param {import("whatsapp-web.js").MessageMedia} media
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendMedia(media, caption = "") {
|
||||||
|
return currentChat.sendMessage(media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send video file from local path.
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendVideo(filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return currentChat.sendMessage(media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send audio file from local path.
|
||||||
|
* @param {string} filePath
|
||||||
|
*/
|
||||||
|
async sendAudio(filePath) {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return currentChat.sendMessage(media, { sendAudioAsVoice: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send image from local path.
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendImage(filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return currentChat.sendMessage(media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a sticker.
|
||||||
|
* Accepts filePath (string) or buffer (Buffer) — plugin never needs
|
||||||
|
* to know MessageMedia exists.
|
||||||
|
* @param {string | Buffer} source
|
||||||
|
*/
|
||||||
|
async sendSticker(source) {
|
||||||
|
const media = typeof source === "string"
|
||||||
|
? MessageMedia.fromFilePath(source)
|
||||||
|
: new MessageMedia("image/webp", source.toString("base64"));
|
||||||
|
return currentChat.sendMessage(media, { sendMediaAsSticker: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Send to specific chat ───────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send text to specific chat by ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
async sendTo(chatId, text) {
|
||||||
|
return client.sendMessage(chatId, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send video to specific chat by ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendVideoTo(chatId, filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send audio to specific chat by ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} filePath
|
||||||
|
*/
|
||||||
|
async sendAudioTo(chatId, filePath) {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { sendAudioAsVoice: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send image to specific chat by ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendImageTo(chatId, filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send sticker to specific chat by ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string | Buffer} source
|
||||||
|
*/
|
||||||
|
async sendStickerTo(chatId, source) {
|
||||||
|
const media = typeof source === "string"
|
||||||
|
? MessageMedia.fromFilePath(source)
|
||||||
|
: new MessageMedia("image/webp", source.toString("base64"));
|
||||||
|
return client.sendMessage(chatId, media, { sendMediaAsSticker: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Access other plugins ─────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return public API of another plugin (what it exported in `exports`).
|
||||||
|
* Returns null if plugin doesn't exist or is disabled.
|
||||||
|
* @param {string} name — plugin name (folder in /plugins)
|
||||||
|
* @returns {any|null}
|
||||||
|
*/
|
||||||
|
getPlugin(name) {
|
||||||
|
return pluginRegistry.get(name)?.exports ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Logger ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
log: {
|
||||||
|
info: (...a) => logger.info(...a),
|
||||||
|
warn: (...a) => logger.warn(...a),
|
||||||
|
error: (...a) => logger.error(...a),
|
||||||
|
success: (...a) => logger.success(...a),
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Current chat info ────────────────────────────────────
|
||||||
|
|
||||||
|
chat: {
|
||||||
|
id: currentChat.id._serialized,
|
||||||
|
name: currentChat.name || currentChat.id.user,
|
||||||
|
isGroup: /@g\.us$/.test(currentChat.id._serialized),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
37
src/kernel/pluginGuard.js
Normal file
37
src/kernel/pluginGuard.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* pluginGuard.js
|
||||||
|
*
|
||||||
|
* Runs a plugin safely.
|
||||||
|
* If plugin throws an error:
|
||||||
|
* - Logs error with context
|
||||||
|
* - Marks plugin as "error" in registry
|
||||||
|
* - Never crashes the bot
|
||||||
|
*
|
||||||
|
* Disabled or errored plugins are silently ignored.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
import { pluginRegistry } from "./pluginLoader.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} plugin — pluginRegistry entry
|
||||||
|
* @param {object} context — { msg, chat, api }
|
||||||
|
*/
|
||||||
|
export async function runPlugin(plugin, context) {
|
||||||
|
if (plugin.status !== "active") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await plugin.run(context);
|
||||||
|
} catch (err) {
|
||||||
|
// Disable plugin to prevent further breakage
|
||||||
|
plugin.status = "error";
|
||||||
|
plugin.error = err;
|
||||||
|
pluginRegistry.set(plugin.name, plugin);
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
t("system.pluginDisabledAfterError", { name: plugin.name, message: err.message }),
|
||||||
|
`\n ${t("errors.stack")}: ${err.stack?.split("\n")[1]?.trim() ?? ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/kernel/pluginLoader.js
Normal file
111
src/kernel/pluginLoader.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* pluginLoader.js
|
||||||
|
*
|
||||||
|
* Responsible for:
|
||||||
|
* 1. Reading active plugins from manybot.conf (PLUGINS=[...])
|
||||||
|
* 2. Loading each plugin from /plugins folder
|
||||||
|
* 3. Registering in pluginRegistry with status and public exports
|
||||||
|
* 4. Exposing pluginRegistry to kernel and pluginApi
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
|
||||||
|
const PLUGINS_DIR = path.resolve("src/plugins");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each entry in registry:
|
||||||
|
* {
|
||||||
|
* name: string,
|
||||||
|
* status: "active" | "disabled" | "error",
|
||||||
|
* run: async function({ msg, chat, api }) — plugin default function
|
||||||
|
* exports: any — what plugin exposed via `export const api = { ... }`
|
||||||
|
* error: Error | null
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @type {Map<string, object>}
|
||||||
|
*/
|
||||||
|
export const pluginRegistry = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all active plugins listed in `activePlugins`.
|
||||||
|
* Called once during bot initialization.
|
||||||
|
*
|
||||||
|
* @param {string[]} activePlugins — active plugin names (from .conf)
|
||||||
|
*/
|
||||||
|
export async function loadPlugins(activePlugins) {
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
logger.warn(t("system.pluginsFolderNotFound"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of activePlugins) {
|
||||||
|
await loadPlugin(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = pluginRegistry.size;
|
||||||
|
const ativos = [...pluginRegistry.values()].filter(p => p.status === "active").length;
|
||||||
|
const erros = total - ativos;
|
||||||
|
|
||||||
|
logger.success(t("system.pluginsLoaded", {
|
||||||
|
count: ativos,
|
||||||
|
errors: erros ? t("system.pluginsLoadedWithErrors", { count: erros }) : ""
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call setup(api) on all plugins that export it.
|
||||||
|
* Executed once after bot connects to WhatsApp.
|
||||||
|
*
|
||||||
|
* @param {object} api — api without message context (only sendTo, log, schedule...)
|
||||||
|
*/
|
||||||
|
export async function setupPlugins(api) {
|
||||||
|
for (const plugin of pluginRegistry.values()) {
|
||||||
|
if (plugin.status !== "active" || !plugin.setup) continue;
|
||||||
|
try {
|
||||||
|
await plugin.setup(api);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(t("system.pluginSetupFailed", { name: plugin.name, message: err.message }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carrega um único plugin pelo nome.
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
async function loadPlugin(name) {
|
||||||
|
const pluginPath = path.join(PLUGINS_DIR, name, "index.js");
|
||||||
|
|
||||||
|
if (!fs.existsSync(pluginPath)) {
|
||||||
|
logger.warn(t("system.pluginNotFound", { name, path: pluginPath }));
|
||||||
|
pluginRegistry.set(name, { name, status: "disabled", run: null, exports: null, error: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mod = await import(pluginPath);
|
||||||
|
|
||||||
|
// Plugin must export a default function — this is called on every message
|
||||||
|
if (typeof mod.default !== "function") {
|
||||||
|
throw new Error(`Plugin "${name}" does not export a default function`);
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginRegistry.set(name, {
|
||||||
|
name,
|
||||||
|
status: "active",
|
||||||
|
run: mod.default,
|
||||||
|
setup: mod.setup ?? null, // opcional — chamado uma vez na inicialização
|
||||||
|
exports: mod.api ?? null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(t("system.pluginLoaded", { name }));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(t("system.pluginLoadFailed", { name, message: err.message }));
|
||||||
|
pluginRegistry.set(name, { name, status: "error", run: null, exports: null, error: err });
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/kernel/pluginState.js
Normal file
99
src/kernel/pluginState.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* pluginState.js
|
||||||
|
*
|
||||||
|
* Tracks plugin execution state per chat.
|
||||||
|
* Used to implement the service vs non-service behavior:
|
||||||
|
* - Services (service: true) can run regardless of state
|
||||||
|
* - Non-services are blocked when another plugin is running in the same chat
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map<chatId, { pluginName: string, startedAt: Date }>
|
||||||
|
* Tracks which plugin is currently "holding the lock" in each chat
|
||||||
|
*/
|
||||||
|
const runningPlugins = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any plugin is currently running in a specific chat
|
||||||
|
* @param {string} chatId - Chat ID (serialized)
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isPluginRunning(chatId) {
|
||||||
|
return runningPlugins.has(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get info about the plugin running in a chat
|
||||||
|
* @param {string} chatId - Chat ID (serialized)
|
||||||
|
* @returns {{ pluginName: string, startedAt: Date } | null}
|
||||||
|
*/
|
||||||
|
export function getRunningPlugin(chatId) {
|
||||||
|
return runningPlugins.get(chatId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a plugin as running in a chat
|
||||||
|
* @param {string} chatId - Chat ID (serialized)
|
||||||
|
* @param {string} pluginName - Name of the plugin taking the lock
|
||||||
|
*/
|
||||||
|
export function startPluginRun(chatId, pluginName) {
|
||||||
|
runningPlugins.set(chatId, {
|
||||||
|
pluginName,
|
||||||
|
startedAt: new Date()
|
||||||
|
});
|
||||||
|
logger.debug(`Plugin "${pluginName}" started in chat ${chatId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a plugin as finished in a chat
|
||||||
|
* @param {string} chatId - Chat ID (serialized)
|
||||||
|
* @param {string} pluginName - Name of the plugin releasing the lock
|
||||||
|
*/
|
||||||
|
export function endPluginRun(chatId, pluginName) {
|
||||||
|
const current = runningPlugins.get(chatId);
|
||||||
|
if (current && current.pluginName === pluginName) {
|
||||||
|
runningPlugins.delete(chatId);
|
||||||
|
logger.debug(`Plugin "${pluginName}" ended in chat ${chatId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force clear the running state for a chat
|
||||||
|
* Useful for cleanup or admin commands
|
||||||
|
* @param {string} chatId - Chat ID (serialized)
|
||||||
|
*/
|
||||||
|
export function clearPluginRun(chatId) {
|
||||||
|
runningPlugins.delete(chatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all chats where a specific plugin is running
|
||||||
|
* @param {string} pluginName - Plugin name
|
||||||
|
* @returns {string[]} Array of chat IDs
|
||||||
|
*/
|
||||||
|
export function getChatsWithPlugin(pluginName) {
|
||||||
|
const chats = [];
|
||||||
|
for (const [chatId, info] of runningPlugins.entries()) {
|
||||||
|
if (info.pluginName === pluginName) {
|
||||||
|
chats.push(chatId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stats about running plugins
|
||||||
|
* @returns {{ total: number, byPlugin: Record<string, number> }}
|
||||||
|
*/
|
||||||
|
export function getStats() {
|
||||||
|
const byPlugin = {};
|
||||||
|
for (const info of runningPlugins.values()) {
|
||||||
|
byPlugin[info.pluginName] = (byPlugin[info.pluginName] || 0) + 1;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total: runningPlugins.size,
|
||||||
|
byPlugin
|
||||||
|
};
|
||||||
|
}
|
||||||
48
src/kernel/scheduler.js
Normal file
48
src/kernel/scheduler.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* scheduler.js
|
||||||
|
*
|
||||||
|
* Allows plugins to register scheduled tasks via cron.
|
||||||
|
* Uses node-cron underneath, but plugins never import node-cron directly —
|
||||||
|
* they only call api.schedule(cron, fn).
|
||||||
|
*
|
||||||
|
* Usage in plugin:
|
||||||
|
* import { schedule } from "many";
|
||||||
|
* schedule("0 9 * * 1", async () => { await api.send("Good morning!"); });
|
||||||
|
*/
|
||||||
|
|
||||||
|
import cron from "node-cron";
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
|
||||||
|
/** List of active tasks (for eventual teardown) */
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a cron task.
|
||||||
|
* @param {string} expression — cron expression e.g., "0 9 * * 1"
|
||||||
|
* @param {Function} fn — async function to execute
|
||||||
|
* @param {string} pluginName — plugin name (for logging)
|
||||||
|
*/
|
||||||
|
export function schedule(expression, fn, pluginName = "unknown") {
|
||||||
|
if (!cron.validate(expression)) {
|
||||||
|
logger.warn(t("system.schedulerInvalidCron", { name: pluginName, expression }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = cron.schedule(expression, async () => {
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(t("system.schedulerError", { name: pluginName, message: err.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tasks.push({ pluginName, expression, task });
|
||||||
|
logger.info(t("system.schedulerRegistered", { name: pluginName, expression }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop all schedules (useful for shutdown) */
|
||||||
|
export function stopAll() {
|
||||||
|
tasks.forEach(({ task }) => task.stop());
|
||||||
|
tasks.length = 0;
|
||||||
|
}
|
||||||
56
src/locales/en.json
Normal file
56
src/locales/en.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"bot": {
|
||||||
|
"starting": "Starting ManyBot...",
|
||||||
|
"initialized": "Client initialized. Waiting for WhatsApp connection...",
|
||||||
|
"ready": "Bot is ready!",
|
||||||
|
"error": {
|
||||||
|
"uncaught": "Uncaught exception",
|
||||||
|
"unhandled": "Unhandled rejection"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"info": "INFO",
|
||||||
|
"success": "OK",
|
||||||
|
"warn": "WARN",
|
||||||
|
"error": "ERROR",
|
||||||
|
"msg": "MSG",
|
||||||
|
"cmd": "CMD",
|
||||||
|
"done": "DONE",
|
||||||
|
"context": {
|
||||||
|
"group": "group",
|
||||||
|
"from": "From",
|
||||||
|
"type": "Type",
|
||||||
|
"replyTo": "replies to"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"environment": "Environment: {{platform}} — using {{puppeteer}}",
|
||||||
|
"environmentTermux": "Environment: Termux — using system Chromium",
|
||||||
|
"connected": "WhatsApp connected and ready!",
|
||||||
|
"disconnected": "Disconnected — reason: {{reason}}",
|
||||||
|
"reconnecting": "Reconnecting in {{seconds}}s...",
|
||||||
|
"reinitializing": "Reinitializing client...",
|
||||||
|
"qrSaved": "QR Code saved to: {{path}}",
|
||||||
|
"qrOpen": "Open with: termux-open qr.png",
|
||||||
|
"qrSaveFailed": "Failed to save QR Code:",
|
||||||
|
"qrScan": "Scan the QR Code below:",
|
||||||
|
"clientId": "Client ID: {{id}}",
|
||||||
|
"pluginsFolderNotFound": "Plugins folder not found. No plugins loaded.",
|
||||||
|
"pluginsLoaded": "Plugins loaded: {{count}} active{{errors}}",
|
||||||
|
"pluginsLoadedWithErrors": ", {{count}} with error",
|
||||||
|
"pluginSetupFailed": "Plugin \"{{name}}\" setup failed: {{message}}",
|
||||||
|
"pluginNotFound": "Plugin \"{{name}}\" not found at {{path}}",
|
||||||
|
"pluginLoaded": "Plugin loaded: {{name}}",
|
||||||
|
"pluginLoadFailed": "Failed to load plugin \"{{name}}\": {{message}}",
|
||||||
|
"pluginDisabledAfterError": "Plugin \"{{name}}\" disabled after error: {{message}}",
|
||||||
|
"schedulerInvalidCron": "Plugin \"{{name}}\" registered invalid cron expression: \"{{expression}}\"",
|
||||||
|
"schedulerError": "Plugin \"{{name}}\" scheduling error: {{message}}",
|
||||||
|
"schedulerRegistered": "Schedule registered — plugin \"{{name}}\" → \"{{expression}}\"",
|
||||||
|
"downloadJobFailed": "Download job failed — {{message}}"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"pluginLoad": "Failed to load plugin",
|
||||||
|
"messageProcess": "Failed to process message",
|
||||||
|
"stack": "Stack"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/locales/es.json
Normal file
56
src/locales/es.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"bot": {
|
||||||
|
"starting": "Iniciando ManyBot...",
|
||||||
|
"initialized": "Cliente inicializado. Esperando conexión con WhatsApp...",
|
||||||
|
"ready": "¡Bot está listo!",
|
||||||
|
"error": {
|
||||||
|
"uncaught": "Excepción no capturada",
|
||||||
|
"unhandled": "Rechazo no manejado"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"info": "INFO",
|
||||||
|
"success": "OK",
|
||||||
|
"warn": "WARN",
|
||||||
|
"error": "ERROR",
|
||||||
|
"msg": "MSG",
|
||||||
|
"cmd": "CMD",
|
||||||
|
"done": "DONE",
|
||||||
|
"context": {
|
||||||
|
"group": "grupo",
|
||||||
|
"from": "De",
|
||||||
|
"type": "Tipo",
|
||||||
|
"replyTo": "Responde a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"environment": "Entorno: {{platform}} — usando {{puppeteer}}",
|
||||||
|
"environmentTermux": "Entorno: Termux — usando Chromium del sistema",
|
||||||
|
"connected": "¡WhatsApp conectado y listo!",
|
||||||
|
"disconnected": "Desconectado — motivo: {{reason}}",
|
||||||
|
"reconnecting": "Reconectando en {{seconds}}s...",
|
||||||
|
"reinitializing": "Reinicializando cliente...",
|
||||||
|
"qrSaved": "Código QR guardado en: {{path}}",
|
||||||
|
"qrOpen": "Abrir con: termux-open qr.png",
|
||||||
|
"qrSaveFailed": "Error al guardar el Código QR:",
|
||||||
|
"qrScan": "Escanea el Código QR abajo:",
|
||||||
|
"clientId": "Client ID: {{id}}",
|
||||||
|
"pluginsFolderNotFound": "Carpeta de plugins no encontrada. Ningún plugin cargado.",
|
||||||
|
"pluginsLoaded": "Plugins cargados: {{count}} activos{{errors}}",
|
||||||
|
"pluginsLoadedWithErrors": ", {{count}} con error",
|
||||||
|
"pluginSetupFailed": "Error en la configuración del plugin \"{{name}}\": {{message}}",
|
||||||
|
"pluginNotFound": "Plugin \"{{name}}\" no encontrado en {{path}}",
|
||||||
|
"pluginLoaded": "Plugin cargado: {{name}}",
|
||||||
|
"pluginLoadFailed": "Error al cargar el plugin \"{{name}}\": {{message}}",
|
||||||
|
"pluginDisabledAfterError": "Plugin \"{{name}}\" desactivado después del error: {{message}}",
|
||||||
|
"schedulerInvalidCron": "Plugin \"{{name}}\" registró expresión cron inválida: \"{{expression}}\"",
|
||||||
|
"schedulerError": "Error en la programación del plugin \"{{name}}\": {{message}}",
|
||||||
|
"schedulerRegistered": "Programación registrada — plugin \"{{name}}\" → \"{{expression}}\"",
|
||||||
|
"downloadJobFailed": "Error en el trabajo de descarga — {{message}}"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"pluginLoad": "Error al cargar el plugin",
|
||||||
|
"messageProcess": "Error al procesar el mensaje",
|
||||||
|
"stack": "Stack"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/locales/pt.json
Normal file
56
src/locales/pt.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"bot": {
|
||||||
|
"starting": "Iniciando ManyBot...",
|
||||||
|
"initialized": "Cliente inicializado. Aguardando conexão com WhatsApp...",
|
||||||
|
"ready": "Bot está pronto!",
|
||||||
|
"error": {
|
||||||
|
"uncaught": "Exceção não capturada",
|
||||||
|
"unhandled": "Rejeição não tratada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"info": "INFO",
|
||||||
|
"success": "OK",
|
||||||
|
"warn": "WARN",
|
||||||
|
"error": "ERRO",
|
||||||
|
"msg": "MSG",
|
||||||
|
"cmd": "CMD",
|
||||||
|
"done": "DONE",
|
||||||
|
"context": {
|
||||||
|
"group": "grupo",
|
||||||
|
"from": "De",
|
||||||
|
"type": "Tipo",
|
||||||
|
"replyTo": "Responde"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"environment": "Ambiente: {{platform}} — usando {{puppeteer}}",
|
||||||
|
"environmentTermux": "Ambiente: Termux — usando Chromium do sistema",
|
||||||
|
"connected": "WhatsApp conectado e pronto!",
|
||||||
|
"disconnected": "Desconectado — motivo: {{reason}}",
|
||||||
|
"reconnecting": "Reconectando em {{seconds}}s...",
|
||||||
|
"reinitializing": "Reinicializando cliente...",
|
||||||
|
"qrSaved": "QR Code salvo em: {{path}}",
|
||||||
|
"qrOpen": "Abra com: termux-open qr.png",
|
||||||
|
"qrSaveFailed": "Falha ao salvar QR Code:",
|
||||||
|
"qrScan": "Escaneie o QR Code abaixo:",
|
||||||
|
"clientId": "Client ID: {{id}}",
|
||||||
|
"pluginsFolderNotFound": "Pasta de plugins não encontrada. Nenhum plugin carregado.",
|
||||||
|
"pluginsLoaded": "Plugins carregados: {{count}} ativos{{errors}}",
|
||||||
|
"pluginsLoadedWithErrors": ", {{count}} com erro",
|
||||||
|
"pluginSetupFailed": "Falha na configuração do plugin \"{{name}}\": {{message}}",
|
||||||
|
"pluginNotFound": "Plugin \"{{name}}\" não encontrado em {{path}}",
|
||||||
|
"pluginLoaded": "Plugin carregado: {{name}}",
|
||||||
|
"pluginLoadFailed": "Falha ao carregar plugin \"{{name}}\": {{message}}",
|
||||||
|
"pluginDisabledAfterError": "Plugin \"{{name}}\" desativado após erro: {{message}}",
|
||||||
|
"schedulerInvalidCron": "Plugin \"{{name}}\" registrou expressão cron inválida: \"{{expression}}\"",
|
||||||
|
"schedulerError": "Erro no agendamento do plugin \"{{name}}\": {{message}}",
|
||||||
|
"schedulerRegistered": "Agendamento registrado — plugin \"{{name}}\" → \"{{expression}}\"",
|
||||||
|
"downloadJobFailed": "Falha no job de download — {{message}}"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"pluginLoad": "Falha ao carregar plugin",
|
||||||
|
"messageProcess": "Falha ao processar mensagem",
|
||||||
|
"stack": "Stack"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/logger/formatter.js
Normal file
36
src/logger/formatter.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// ── ANSI Palette ─────────────────────────────────────────────
|
||||||
|
export const c = {
|
||||||
|
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
||||||
|
green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m",
|
||||||
|
red: "\x1b[31m", gray: "\x1b[90m", white: "\x1b[37m",
|
||||||
|
blue: "\x1b[34m", magenta: "\x1b[35m",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SEP = `${c.gray}${"─".repeat(52)}${c.reset}`;
|
||||||
|
|
||||||
|
export const now = () =>
|
||||||
|
`[${new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "medium" })}]`;
|
||||||
|
|
||||||
|
export const formatType = (type) => ({
|
||||||
|
sticker: `${c.magenta}sticker${c.reset}`,
|
||||||
|
image: `${c.cyan}imagen${c.reset}`,
|
||||||
|
video: `${c.cyan}video${c.reset}`,
|
||||||
|
audio: `${c.cyan}audio${c.reset}`,
|
||||||
|
ptt: `${c.cyan}audio${c.reset}`,
|
||||||
|
document: `${c.cyan}archivo${c.reset}`,
|
||||||
|
chat: `${c.white}texto${c.reset}`,
|
||||||
|
}[type] ?? `${c.gray}${type}${c.reset}`);
|
||||||
|
|
||||||
|
export const formatContext = (chatName, isGroup) =>
|
||||||
|
isGroup
|
||||||
|
? `${c.bold}${chatName}${c.reset} ${c.dim}(group)${c.reset}`
|
||||||
|
: `${c.bold}${chatName}${c.reset} ${c.dim}(private)${c.reset}`;
|
||||||
|
|
||||||
|
export const formatBody = (body, isCommand) =>
|
||||||
|
body?.trim()
|
||||||
|
? `${isCommand ? c.yellow : c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}`
|
||||||
|
: `${c.dim}<media>${c.reset}`;
|
||||||
|
|
||||||
|
export const formatReply = (quotedName, quotedNumber, quotedPreview) =>
|
||||||
|
`\n${c.gray} ↩ Para: ${c.reset}${c.white}${quotedName}${c.reset} ${c.dim}+${quotedNumber}${c.reset}` +
|
||||||
|
`\n${c.gray} ↩ Msg: ${c.reset}${c.dim}${quotedPreview}${c.reset}`;
|
||||||
41
src/logger/logger.js
Normal file
41
src/logger/logger.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
c, now,
|
||||||
|
formatType, formatContext, formatBody, formatReply,
|
||||||
|
} from "./formatter.js";
|
||||||
|
import { t } from "../i18n/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ManyBot central logger.
|
||||||
|
* Each method only handles output — no business logic or external I/O.
|
||||||
|
*/
|
||||||
|
export const logger = {
|
||||||
|
info: (...a) => console.log(`${c.gray}${now()}${c.reset}${c.cyan}INFO ${c.reset}`, ...a),
|
||||||
|
success: (...a) => console.log(`${c.gray}${now()}${c.reset}${c.green}OK ${c.reset}`, ...a),
|
||||||
|
warn: (...a) => console.log(`${c.gray}${now()}${c.reset}${c.yellow}WARN ${c.reset}`, ...a),
|
||||||
|
error: (...a) => console.log(`${c.gray}${now()}${c.reset}${c.red}ERROR ${c.reset}`, ...a),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a received message from a resolved context.
|
||||||
|
* @param {import("./messageContext.js").MessageContext} ctx
|
||||||
|
*/
|
||||||
|
msg(ctx) {
|
||||||
|
const { chatName, isGroup, senderName, senderNumber, type, body, quoted } = ctx;
|
||||||
|
const context = isGroup ? `${chatName} (${t("log.context.group")})` : chatName;
|
||||||
|
const reply = quoted ? ` → ${t("log.context.replyTo")} ${quoted.name} +${quoted.number}: "${quoted.preview}"` : "";
|
||||||
|
console.log(`\n${c.gray}${now()}${c.reset}${c.cyan}MSG${c.reset} ${context} ${c.gray}— ${t("log.context.from")}:${c.reset} ${c.white}${senderName}${c.reset} ${c.dim}+${senderNumber}${c.reset} ${c.gray}— ${t("log.context.type")}:${c.reset} ${type} — ${c.green}"${body}"${c.reset}${c.gray}${reply}${c.reset}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
cmd: (cmd, extra = "") =>
|
||||||
|
console.log(
|
||||||
|
`${c.gray}${now()}${c.reset}${c.yellow}CMD ${c.reset}` +
|
||||||
|
`${c.bold}${cmd}${c.reset}` +
|
||||||
|
(extra ? ` ${c.dim}${extra}${c.reset}` : "")
|
||||||
|
),
|
||||||
|
|
||||||
|
done: (cmd, detail = "") =>
|
||||||
|
console.log(
|
||||||
|
`${c.gray}${now()}${c.reset}${c.green}DONE ${c.reset}` +
|
||||||
|
`${c.dim}${cmd}${c.reset}` +
|
||||||
|
(detail ? ` — ${detail}` : "")
|
||||||
|
),
|
||||||
|
};
|
||||||
81
src/logger/messageContext.js
Normal file
81
src/logger/messageContext.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import client from "../client/whatsappClient.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract clean number from message.
|
||||||
|
* @param {import("whatsapp-web.js").Message} msg
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
export async function getNumber(msg) {
|
||||||
|
if (msg.fromMe) return String(msg.from).split("@")[0];
|
||||||
|
const contact = await msg.getContact();
|
||||||
|
return contact.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build full message context for logging.
|
||||||
|
* Resolves contact, quoted message and chat metadata.
|
||||||
|
*
|
||||||
|
* @param {import("whatsapp-web.js").Message} msg
|
||||||
|
* @param {import("whatsapp-web.js").Chat} chat
|
||||||
|
* @param {string} botPrefix
|
||||||
|
* @returns {Promise<MessageContext>}
|
||||||
|
*
|
||||||
|
* @typedef {Object} MessageContext
|
||||||
|
* @property {string} chatName
|
||||||
|
* @property {string} chatId
|
||||||
|
* @property {boolean} isGroup
|
||||||
|
* @property {string} senderName
|
||||||
|
* @property {string} senderNumber
|
||||||
|
* @property {string} type
|
||||||
|
* @property {string} body
|
||||||
|
* @property {{ name: string, number: string, preview: string } | null} quoted
|
||||||
|
*/
|
||||||
|
export async function buildMessageContext(msg, chat, botPrefix) {
|
||||||
|
const chatId = chat.id._serialized;
|
||||||
|
const isGroup = /@g\.us$/.test(chatId);
|
||||||
|
const number = await getNumber(msg);
|
||||||
|
const name = msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, "");
|
||||||
|
|
||||||
|
const quoted = await resolveQuotedMessage(msg);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatName: chat.name || chat.id.user,
|
||||||
|
chatId,
|
||||||
|
isGroup,
|
||||||
|
senderName: name,
|
||||||
|
senderNumber: number,
|
||||||
|
type: msg?.type || "text",
|
||||||
|
body: msg.body,
|
||||||
|
quoted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve quoted message data if exists.
|
||||||
|
* Returns null on error or if not present.
|
||||||
|
*
|
||||||
|
* @param {import("whatsapp-web.js").Message} msg
|
||||||
|
* @returns {Promise<{ name: string, number: string, preview: string } | null>}
|
||||||
|
*/
|
||||||
|
async function resolveQuotedMessage(msg) {
|
||||||
|
if (!msg?.hasQuotedMsg) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const quoted = await msg.getQuotedMessage();
|
||||||
|
const quotedNumber = String(quoted.from).split("@")[0];
|
||||||
|
|
||||||
|
let quotedName = quotedNumber;
|
||||||
|
try {
|
||||||
|
const contact = await client.getContactById(quoted.from);
|
||||||
|
quotedName = contact?.pushname || contact?.formattedName || quotedNumber;
|
||||||
|
} catch { /* contact not found — use number */ }
|
||||||
|
|
||||||
|
const quotedPreview = quoted.body?.trim()
|
||||||
|
? `"${quoted.body.length > 80 ? quoted.body.slice(0, 80) + "…" : quoted.body}"`
|
||||||
|
: `<${quoted.type}>`;
|
||||||
|
|
||||||
|
return { name: quotedName, number: quotedNumber, preview: quotedPreview };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/main.js
Normal file
47
src/main.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* main.js
|
||||||
|
*
|
||||||
|
* ManyBot entry point.
|
||||||
|
* Initializes WhatsApp client and loads plugins.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import client from "./client/whatsappClient.js";
|
||||||
|
import { handleMessage } from "./kernel/messageHandler.js";
|
||||||
|
import { loadPlugins, setupPlugins } from "./kernel/pluginLoader.js";
|
||||||
|
import { buildSetupApi } from "./kernel/pluginApi.js";
|
||||||
|
import { logger } from "./logger/logger.js";
|
||||||
|
import { PLUGINS } from "./config.js";
|
||||||
|
import { t } from "./i18n/index.js";
|
||||||
|
|
||||||
|
logger.info(t("bot.starting"));
|
||||||
|
|
||||||
|
// Global safety net — no error should crash the bot
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
logger.error(`${t("bot.error.uncaught")} — ${err.message}`, `\n ${t("errors.stack")}: ${err.stack?.split("\n")[1]?.trim() ?? ""}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
||||||
|
logger.error(`${t("bot.error.unhandled")} — ${msg}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load plugins before connecting
|
||||||
|
await loadPlugins(PLUGINS);
|
||||||
|
|
||||||
|
client.on("message_create", async (msg) => {
|
||||||
|
try {
|
||||||
|
await handleMessage(msg);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`${t("errors.messageProcess")} — ${err.message}`,
|
||||||
|
`\n ${t("errors.stack")}: ${err.stack?.split("\n")[1]?.trim() ?? ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("ready", async () => {
|
||||||
|
await setupPlugins(buildSetupApi(client));
|
||||||
|
});
|
||||||
|
client.initialize();
|
||||||
|
console.log("\n");
|
||||||
|
logger.info(t("bot.initialized"));
|
||||||
5
src/utils/botMsg.js
Normal file
5
src/utils/botMsg.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { BOT_PREFIX } from "../config.js";
|
||||||
|
|
||||||
|
export function botMsg(text) {
|
||||||
|
return `${BOT_PREFIX}\n${text}`;
|
||||||
|
}
|
||||||
9
src/utils/file.js
Normal file
9
src/utils/file.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export function emptyFolder(folder) {
|
||||||
|
fs.readdirSync(folder).forEach(file => {
|
||||||
|
const filePath = path.join(folder, file);
|
||||||
|
if (fs.lstatSync(filePath).isFile()) fs.unlinkSync(filePath);
|
||||||
|
});
|
||||||
|
}
|
||||||
3
src/utils/getChatId.js
Normal file
3
src/utils/getChatId.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function getChatId(chat) {
|
||||||
|
return chat.id._serialized;
|
||||||
|
}
|
||||||
63
src/utils/get_id.js
Normal file
63
src/utils/get_id.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* CLI utility to discover chat/group IDs.
|
||||||
|
* Usage: node src/utils/get_id.js groups|contacts|<name>
|
||||||
|
*/
|
||||||
|
import pkg from "whatsapp-web.js";
|
||||||
|
import qrcode from "qrcode-terminal";
|
||||||
|
import { resolvePuppeteerConfig } from "../client/environment.js";
|
||||||
|
|
||||||
|
const CLIENT_ID="getId"
|
||||||
|
const { Client, LocalAuth } = pkg;
|
||||||
|
|
||||||
|
const arg = process.argv[2];
|
||||||
|
|
||||||
|
if (!arg) {
|
||||||
|
console.log("Usage: node get_id.js groups|contacts|<name>");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
|
||||||
|
puppeteer: {
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
...(resolvePuppeteerConfig().args || [])
|
||||||
|
],
|
||||||
|
...resolvePuppeteerConfig()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("qr", (qr) => {
|
||||||
|
console.log("[QR] Scan to authenticate:");
|
||||||
|
qrcode.generate(qr, { small: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("ready", async () => {
|
||||||
|
console.log("[OK] Connected. Searching chats...\n");
|
||||||
|
|
||||||
|
const chats = await client.getChats();
|
||||||
|
const search = arg.toLowerCase();
|
||||||
|
|
||||||
|
const filtered =
|
||||||
|
search === "groups" ? chats.filter(c => c.isGroup) :
|
||||||
|
search === "contacts" ? chats.filter(c => !c.isGroup) :
|
||||||
|
chats.filter(c => (c.name || c.id.user).toLowerCase().includes(search));
|
||||||
|
|
||||||
|
if (!filtered.length) {
|
||||||
|
console.log("No results found.");
|
||||||
|
} else {
|
||||||
|
filtered.forEach(c => {
|
||||||
|
console.log("─".repeat(40));
|
||||||
|
console.log("Name: ", c.name || c.id.user);
|
||||||
|
console.log("ID: ", c.id._serialized);
|
||||||
|
console.log("Group: ", c.isGroup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.destroy();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.initialize();
|
||||||
129
src/utils/pluginI18n.js
Normal file
129
src/utils/pluginI18n.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* src/utils/pluginI18n.js
|
||||||
|
*
|
||||||
|
* Independent i18n system for plugins.
|
||||||
|
* Plugins load their own translations from locale/ folder.
|
||||||
|
* Completely separate from bot core i18n.
|
||||||
|
*
|
||||||
|
* Usage in plugin:
|
||||||
|
* import { createPluginI18n } from "../utils/pluginI18n.js";
|
||||||
|
* const { t } = createPluginI18n(import.meta.url);
|
||||||
|
*
|
||||||
|
* Folder structure:
|
||||||
|
* myPlugin/
|
||||||
|
* index.js
|
||||||
|
* locale/
|
||||||
|
* en.json (required - fallback)
|
||||||
|
* pt.json
|
||||||
|
* es.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { LANGUAGE } from "../config.js";
|
||||||
|
|
||||||
|
// Default/fallback language
|
||||||
|
const DEFAULT_LANG = "en";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a nested value from an object using dot path
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} key - path like "error.notFound"
|
||||||
|
* @returns {string|undefined}
|
||||||
|
*/
|
||||||
|
function getNestedValue(obj, key) {
|
||||||
|
const parts = key.split(".");
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (current === null || current === undefined || typeof current !== "object") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces placeholders {{key}} with values from context
|
||||||
|
* @param {string} str
|
||||||
|
* @param {object} context
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function interpolate(str, context = {}) {
|
||||||
|
return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
||||||
|
return context[key] !== undefined ? String(context[key]) : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load translations for a plugin
|
||||||
|
* @param {string} localeDir - path to plugin's locale folder
|
||||||
|
* @param {string} lang - target language
|
||||||
|
* @returns {{ translations: object, fallback: object }}
|
||||||
|
*/
|
||||||
|
function loadTranslations(localeDir, lang) {
|
||||||
|
let translations = {};
|
||||||
|
let fallback = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const targetPath = path.join(localeDir, `${lang}.json`);
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
translations = JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackPath = path.join(localeDir, `${DEFAULT_LANG}.json`);
|
||||||
|
if (fs.existsSync(fallbackPath)) {
|
||||||
|
fallback = JSON.parse(fs.readFileSync(fallbackPath, "utf8"));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silent fail - plugin may not have translations
|
||||||
|
}
|
||||||
|
|
||||||
|
return { translations, fallback };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an isolated translation function for a plugin.
|
||||||
|
* Language priority: PLUGIN_LANG env var > manybot.conf LANGUAGE > en
|
||||||
|
*
|
||||||
|
* @param {string} pluginMetaUrl - import.meta.url from the plugin
|
||||||
|
* @returns {{ t: Function, lang: string }}
|
||||||
|
*/
|
||||||
|
export function createPluginI18n(pluginMetaUrl) {
|
||||||
|
const pluginDir = path.dirname(fileURLToPath(pluginMetaUrl));
|
||||||
|
const localeDir = path.join(pluginDir, "locale");
|
||||||
|
|
||||||
|
const targetLang =
|
||||||
|
process.env.PLUGIN_LANG?.trim().toLowerCase() ||
|
||||||
|
LANGUAGE?.trim().toLowerCase() ||
|
||||||
|
DEFAULT_LANG;
|
||||||
|
|
||||||
|
const { translations, fallback } = loadTranslations(localeDir, targetLang);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translation function
|
||||||
|
* @param {string} key - translation key (e.g., "error.notFound")
|
||||||
|
* @param {object} context - values to interpolate {{key}}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function t(key, context = {}) {
|
||||||
|
let value = getNestedValue(translations, key);
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
value = getNestedValue(fallback, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined) return key;
|
||||||
|
|
||||||
|
if (typeof value !== "string") return String(value);
|
||||||
|
|
||||||
|
return interpolate(value, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { t, lang: targetLang };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { createPluginI18n };
|
||||||
141
update
Normal file
141
update
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================
|
||||||
|
# update.sh — Script de atualização segura do ManyBot
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Configuração
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
tmp_dir="$dir/tmp"
|
||||||
|
log_file="$dir/update.log"
|
||||||
|
config_items=(".wwebjs_auth" ".wwebjs_cache")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log() {
|
||||||
|
local level="$1"; shift
|
||||||
|
local msg="$*"
|
||||||
|
local timestamp
|
||||||
|
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "[$timestamp] [$level] $msg" | tee -a "$log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Trap: executa em caso de qualquer erro
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cleanup_on_error() {
|
||||||
|
local exit_code=$?
|
||||||
|
log "ERROR" "Falha durante a atualização (código: $exit_code)."
|
||||||
|
if [ -d "$tmp_dir" ] && [ "$(ls -A "$tmp_dir" 2>/dev/null)" ]; then
|
||||||
|
log "WARN" "Arquivos de configuração preservados em: $tmp_dir"
|
||||||
|
log "WARN" "Restaure manualmente com: mv \"$tmp_dir\"/* \"$dir\"/"
|
||||||
|
fi
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
trap cleanup_on_error ERR
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Validações iniciais
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "Iniciando atualização do ManyBot..."
|
||||||
|
|
||||||
|
# Verifica dependências obrigatórias
|
||||||
|
for cmd in git npm; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
log "ERROR" "Dependência ausente: '$cmd' não encontrado no PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verifica se está em um repositório git
|
||||||
|
if ! git -C "$dir" rev-parse --is-inside-work-tree &>/dev/null; then
|
||||||
|
log "ERROR" "'$dir' não é um repositório git."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exibe o remote para transparência
|
||||||
|
remote_url=$(git -C "$dir" remote get-url origin 2>/dev/null || echo "(não definido)")
|
||||||
|
log "INFO" "Remote: $remote_url"
|
||||||
|
|
||||||
|
# Verifica se há alterações locais não commitadas
|
||||||
|
if ! git -C "$dir" diff --quiet || ! git -C "$dir" diff --cached --quiet; then
|
||||||
|
log "WARN" "Existem alterações locais não commitadas. Elas serão descartadas pelo reset."
|
||||||
|
read -r -p "Deseja continuar mesmo assim? [s/N] " confirm
|
||||||
|
confirm="${confirm:-N}"
|
||||||
|
if [[ ! "$confirm" =~ ^[sS]$ ]]; then
|
||||||
|
log "INFO" "Atualização cancelada pelo usuário."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Backup dos arquivos de configuração
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
mkdir -p "$tmp_dir"
|
||||||
|
|
||||||
|
backed_up=()
|
||||||
|
for item in "${config_items[@]}"; do
|
||||||
|
src="$dir/$item"
|
||||||
|
if [ -e "$src" ]; then
|
||||||
|
mv "$src" "$tmp_dir/"
|
||||||
|
backed_up+=("$item")
|
||||||
|
log "INFO" "Backup: $item → tmp/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Atualização do repositório
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "Buscando atualizações..."
|
||||||
|
git -C "$dir" fetch origin
|
||||||
|
|
||||||
|
branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD)
|
||||||
|
log "INFO" "Branch atual: $branch"
|
||||||
|
|
||||||
|
# Registra o hash antes e depois para auditoria
|
||||||
|
hash_before=$(git -C "$dir" rev-parse HEAD)
|
||||||
|
git -C "$dir" reset --hard "origin/$branch"
|
||||||
|
hash_after=$(git -C "$dir" rev-parse HEAD)
|
||||||
|
|
||||||
|
if [ "$hash_before" = "$hash_after" ]; then
|
||||||
|
log "INFO" "Repositório já estava atualizado (sem mudanças)."
|
||||||
|
else
|
||||||
|
log "INFO" "Atualizado: $hash_before → $hash_after"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Instalação de dependências Node
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "Instalando dependências Node..."
|
||||||
|
npm ci --omit=dev 2>&1 | tee -a "$log_file"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Restauração dos arquivos de configuração
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
if [ ${#backed_up[@]} -gt 0 ]; then
|
||||||
|
for item in "${backed_up[@]}"; do
|
||||||
|
src="$tmp_dir/$item"
|
||||||
|
dst="$dir/$item"
|
||||||
|
if [ -e "$src" ]; then
|
||||||
|
# Remove o que npm possa ter criado (ex: node_modules)
|
||||||
|
rm -rf "$dst"
|
||||||
|
mv "$src" "$dst"
|
||||||
|
log "INFO" "Restaurado: $item"
|
||||||
|
else
|
||||||
|
log "WARN" "Item esperado no backup não encontrado: $item"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Limpa tmp apenas após restauração bem-sucedida
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Concluído
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "ManyBot atualizado com sucesso."
|
||||||
Reference in New Issue
Block a user