forked from sch/KaikOut
[WIP] This is the first commit, due to a demand of Kris. We will continue after Sunday in order to be coordinated with our Christian brothers.
This commit is contained in:
commit
632622f98f
27 changed files with 4744 additions and 0 deletions
661
LICENSE
Normal file
661
LICENSE
Normal file
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
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.
|
||||
|
||||
KaikOut moderation chat bot for the XMPP communication network.
|
||||
Copyright (C) 2024 Schimon Jehudah Zakai Zockaim Zachary
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
130
README.md
Normal file
130
README.md
Normal file
|
@ -0,0 +1,130 @@
|
|||
# Moderation bot for XMPP
|
||||
|
||||
KaikOut is a portmanteau of Kaiko and Out.
|
||||
|
||||
Kaiko (懐古) translates from Japanese to "Old-Fashioned".
|
||||
|
||||
_Because spam has never been in fashion, unless it is served on a plate._
|
||||
|
||||
## KaikOut
|
||||
|
||||
KaikOut is a moderation bot for the XMPP communication network.
|
||||
|
||||
KaikOut is an XMPP bot that suprvises group chat activity and assists in blocking and preventing of abusive and unsolicited type of messages and activities.
|
||||
|
||||
KaikOut is designed primarily for the XMPP communication network (aka Jabber). Visit https://xmpp.org/software/ for more information.
|
||||
|
||||
You can run your own KaikOut instance as a client, from your own computer, server, and even from a Linux phone (i.e. Droidian, Kupfer, Mobian, NixOS, postmarketOS), as well as from Termux.
|
||||
|
||||
All you need is one of the above and an XMPP account to connect KaikOut with.
|
||||
|
||||
Good luck!
|
||||
|
||||
### Slixmpp
|
||||
|
||||
KaikOut is a powered by [slixmpp](https://codeberg.org/poezio/slixmpp).
|
||||
|
||||
### XMPP
|
||||
|
||||
XMPP is the Extensible Messaging and Presence Protocol, a set of open technologies for instant messaging, presence, multi-party chat, voice and video calls, collaboration, lightweight middleware, content syndication, and generalized routing of XML data.
|
||||
|
||||
Visit [XMPP](https://xmpp.org/) for more information [about](https://xmpp.org/about/) the XMPP protocol and check the [list](https://xmpp.org/software/) of XMPP clients.
|
||||
|
||||
KaikOut is primarily designed for XMPP (aka Jabber), yet it is built to be extended to other protocols.
|
||||
|
||||
## Features
|
||||
|
||||
### Control
|
||||
|
||||
- **Blocklist** - Check messages for denied phrases and words (activated by default).
|
||||
- **Frequency** - Check the frequency of messages and status messages (activated by default).
|
||||
- **Inactivity** - Check for inactivity (deactivated by default).
|
||||
- **Moderation Abuse** - KaikOut moderates the moderators.
|
||||
|
||||
### Report
|
||||
|
||||
- **Logger** - Log messages to CSV files.
|
||||
- **Moderation Reports** - KaikOut immediately reports to moderators about moderation activities made by other moderators.
|
||||
- **Self Reports** - KaikOut immediately reports to moderators about its moderation activities.
|
||||
|
||||
### Special
|
||||
|
||||
- **Remote Management** - KaikOut can be managed in two fashions, publicly (groupchat) and privately (chat).
|
||||
- **Simultaneous** - KaikOut is designed to handle multiple contacts, including groupchats, Simultaneously.
|
||||
- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands,
|
||||
|
||||
## Preview
|
||||
|
||||
KaikOut as appears with Cheogram.
|
||||
|
||||
<img alt="Chat: Session" src="kaikout/documentation/screenshots/chat_session.jpg" width="200px"/>
|
||||
<img alt="Ad-Hoc: Commands" src="kaikout/documentation/screenshots/adhoc_commands.jpg" width="200px"/>
|
||||
<img alt="Ad-Hoc: Edit bookmark" src="kaikout/documentation/screenshots/adhoc_edit.jpg" width="200px"/>
|
||||
<img alt="Ad-Hoc: Settings" width="200px" src="kaikout/documentation/screenshots/adhoc_settings.jpg"/>
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install
|
||||
|
||||
It is possible to install KaikOut using pip and pipx.
|
||||
|
||||
#### pip inside venv
|
||||
|
||||
```
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate
|
||||
$ pip install git+https://git.xmpp-it.net/sch/KaikOut
|
||||
```
|
||||
|
||||
#### pipx
|
||||
|
||||
##### Install
|
||||
|
||||
```
|
||||
$ pipx install git+https://git.xmpp-it.net/sch/KaikOut
|
||||
```
|
||||
|
||||
##### Update
|
||||
|
||||
```
|
||||
$ pipx uninstall kaikout
|
||||
$ pipx install git+https://git.xmpp-it.net/sch/KaikOut
|
||||
```
|
||||
|
||||
### Start
|
||||
|
||||
Start by executing the command `kaikout` and enter Username and Password of an existing XMPP account.
|
||||
|
||||
```
|
||||
$ kaikout
|
||||
```
|
||||
|
||||
You can also start KaikOut as follows:
|
||||
|
||||
```
|
||||
$ kaikout --jid ACCOUNT_JABBER_ID --password ACCOUNT_PASSWORD
|
||||
```
|
||||
|
||||
It is advised to use a dedicated extra account for KaikOut.
|
||||
|
||||
## Recommended Clients
|
||||
|
||||
KaikOut works with any XMPP chat client; if you want to make use of the visual interface which KaikOut has to offer (i.e. Ad-Hoc Commands), then you are advised to use [Cheogram](https://cheogram.com), [Converse](https://conversejs.org), [Gajim](https://gajim.org), [monocles chat](https://monocles.chat), [Movim](https://mov.im), [Poezio](https://poez.io), [Profanity](https://profanity-im.github.io), [Psi](https://psi-im.org) or [Psi+](https://psi-plus.com).
|
||||
|
||||
### Support
|
||||
|
||||
Please join our support groupchat whether you want help, discuss new features or just greet us.
|
||||
|
||||
- [Main Groupchat](xmpp:kaikout@chat.woodpeckersnest.space?join) (International)
|
||||
|
||||
## Authors
|
||||
|
||||
[Schimon](xmpp:sch@pimux.de?message) (Author).
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 license.
|
||||
|
||||
## Copyright
|
||||
|
||||
Schimon Jehudah Zackary, 2024
|
3
kaikout/__init__.py
Normal file
3
kaikout/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from kaikout.version import __version__, __version_info__
|
||||
|
||||
print('kaikout', __version__)
|
60
kaikout/__main__.py
Normal file
60
kaikout/__main__.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Kaikout: Kaiko Out anti-spam bot for XMPP
|
||||
# Copyright (C) 2024 Schimon Zackary
|
||||
# This file is part of Kaikout.
|
||||
# See the file LICENSE for copying permission.
|
||||
|
||||
# from kaikout.about import Documentation
|
||||
from kaikout.utilities import Config
|
||||
# from kaikout.xmpp.chat import XmppChat
|
||||
from kaikout.xmpp.client import XmppClient
|
||||
from getpass import getpass
|
||||
from argparse import ArgumentParser
|
||||
import logging
|
||||
# import os
|
||||
# import slixmpp
|
||||
# import sys
|
||||
|
||||
def main():
|
||||
# Setup the command line arguments.
|
||||
parser = ArgumentParser(description=XmppClient.__doc__)
|
||||
|
||||
# Output verbosity options.
|
||||
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
|
||||
action="store_const", dest="loglevel",
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_argument("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
parser.add_argument("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=args.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if args.jid is None:
|
||||
args.jid = input("Username: ")
|
||||
if args.password is None:
|
||||
args.password = getpass("Password: ")
|
||||
|
||||
account_xmpp = Config.get_values('accounts.toml', 'xmpp')
|
||||
|
||||
# Try configuration file
|
||||
if 'client' in account_xmpp:
|
||||
jid = account_xmpp['client']['jid']
|
||||
password = account_xmpp['client']['password']
|
||||
alias = account_xmpp['client']['alias'] if 'alias' in account_xmpp['client'] else None
|
||||
hostname = account_xmpp['client']['hostname'] if 'hostname' in account_xmpp['client'] else None
|
||||
port = account_xmpp['client']['port'] if 'port' in account_xmpp['client'] else None
|
||||
XmppClient(jid, password, hostname, port, alias)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
100
kaikout/about.py
Normal file
100
kaikout/about.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class Documentation:
|
||||
|
||||
def about():
|
||||
return ('KaikOut'
|
||||
'\n'
|
||||
'KaikOut - Kaiko Out group chat anti-spam and moderation bot.'
|
||||
'Spam has never been in fashion.'
|
||||
'\n\n'
|
||||
'KaikOut is an XMPP bot that suprvises group chat activity '
|
||||
'and assists in blocking and preventing of unsolicited type '
|
||||
'of messages.'
|
||||
'\n\n'
|
||||
'KaikOut is a portmanteau of Kaiko and Out'
|
||||
'\n'
|
||||
'Kaiko (懐古) translates from Japanese to "Old-Fashioned"'
|
||||
'\n\n'
|
||||
'https://git.xmpp-it.net/sch/Kaikout'
|
||||
'\n\n'
|
||||
'Copyright 2024 Schimon Jehudah Zackary'
|
||||
'\n\n'
|
||||
'Made in Switzerland'
|
||||
'\n\n'
|
||||
'🇨🇭️')
|
||||
|
||||
def commands():
|
||||
return ("add URL [tag1,tag2,tag3,...]"
|
||||
"\n"
|
||||
" Bookmark URL along with comma-separated tags."
|
||||
"\n\n"
|
||||
"mod name <ID> <TEXT>"
|
||||
"\n"
|
||||
" Modify bookmark title."
|
||||
"\n"
|
||||
"mod note <ID> <TEXT>"
|
||||
"\n"
|
||||
" Modify bookmark description."
|
||||
"\n"
|
||||
"tag [+|-] <ID> [tag1,tag2,tag3,...]"
|
||||
"\n"
|
||||
" Modify bookmark tags. Appends or deletes tags, if flag tag "
|
||||
"is preceded by \'+\' or \'-\' respectively."
|
||||
"\n"
|
||||
"del <ID> or <URL>"
|
||||
"\n"
|
||||
" Delete a bookmark by ID or URL."
|
||||
"\n"
|
||||
"\n"
|
||||
"id <ID>"
|
||||
"\n"
|
||||
" Print a bookmark by ID."
|
||||
"\n"
|
||||
"last"
|
||||
"\n"
|
||||
" Print most recently bookmarked item."
|
||||
"\n"
|
||||
"tag <TEXT>"
|
||||
"\n"
|
||||
" Search bookmarks of given tag."
|
||||
"\n"
|
||||
"search <TEXT>"
|
||||
"\n"
|
||||
" Search bookmarks by a given search query."
|
||||
"\n"
|
||||
"search any <TEXT>"
|
||||
"\n"
|
||||
" Search bookmarks by a any given keyword."
|
||||
# "\n"
|
||||
# "regex"
|
||||
# "\n"
|
||||
# " Search bookmarks using Regular Expression."
|
||||
"\n")
|
||||
|
||||
def notice():
|
||||
return ('Copyright 2024 Schimon Jehudah Zackary'
|
||||
'\n\n'
|
||||
'Permission is hereby granted, free of charge, to any person '
|
||||
'obtaining a copy of this software and associated '
|
||||
'documentation files (the “Software”), to deal in the '
|
||||
'Software without restriction, including without limitation '
|
||||
'the rights to use, copy, modify, merge, publish, distribute, '
|
||||
'sublicense, and/or sell copies of the Software, and to '
|
||||
'permit persons to whom the Software is furnished to do so, '
|
||||
'subject to the following conditions:'
|
||||
'\n\n'
|
||||
'The above copyright notice and this permission notice shall '
|
||||
'be included in all copies or substantial portions of the '
|
||||
'Software.'
|
||||
'\n\n'
|
||||
'THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY '
|
||||
'KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE '
|
||||
'WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR '
|
||||
'PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR '
|
||||
'COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER '
|
||||
'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR '
|
||||
'OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE '
|
||||
'SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.')
|
||||
|
327
kaikout/assets/about.toml
Normal file
327
kaikout/assets/about.toml
Normal file
|
@ -0,0 +1,327 @@
|
|||
[[about]]
|
||||
title = "About"
|
||||
subtitle = "KaikOut"
|
||||
|
||||
[[about]]
|
||||
name = "KaikOut"
|
||||
desc = "XMPP moderation bot. Spam has never been in fashion."
|
||||
info = ["""
|
||||
KaikOut is an XMPP bot that suprvises group chat activity and \
|
||||
assists in blocking and preventing of unsolicited type of messages.
|
||||
|
||||
KaikOut is a portmanteau of Kaiko and Out.
|
||||
Kaiko (懐古) translates from Japanese to "Old-Fashioned".
|
||||
|
||||
KaikOut is designed primarily for the XMPP communication network \
|
||||
(aka Jabber). Visit https://xmpp.org/software/ for more information.
|
||||
|
||||
You can run your own KaikOut instance as a client, from your own \
|
||||
computer, server, and even from a Linux phone (i.e. Droidian, \
|
||||
Kupfer, Mobian, NixOS, postmarketOS), as well as from Termux.
|
||||
|
||||
All you need is one of the above and an XMPP account to connect \
|
||||
KaikOut with.
|
||||
|
||||
Good luck!
|
||||
"""]
|
||||
|
||||
platforms = "XMPP"
|
||||
# platforms = "ActivityPub, Briar, DeltaChat, Email, IRC, LXMF, MQTT, Nostr, Session, Tox."
|
||||
comment = "For an ideal experience, we recommend of using XMPP."
|
||||
url = "https://git.xmpp-it.net/sch/Kaikout"
|
||||
|
||||
[[about]]
|
||||
name = "slixmpp"
|
||||
desc = "XMPP library"
|
||||
info = ["""
|
||||
Slixmpp is an MIT licensed XMPP library for Python 3.7+. It is a fork of \
|
||||
SleekXMPP.
|
||||
|
||||
Slixmpp's goals is to only rewrite the core of the SleekXMPP library \
|
||||
(the low level socket handling, the timers, the events dispatching) \
|
||||
in order to remove all threads.
|
||||
"""]
|
||||
url = "https://codeberg.org/poezio/slixmpp"
|
||||
|
||||
[[about]]
|
||||
name = "SleekXMPP"
|
||||
desc = "XMPP library"
|
||||
info = ["""
|
||||
SleekXMPP is an MIT licensed XMPP library for Python 2.6/3.1+, and is \
|
||||
featured in examples in the book XMPP: The Definitive Guide by Kevin Smith, \
|
||||
Remko Tronçon, and Peter Saint-Andre.
|
||||
"""]
|
||||
url = "https://codeberg.org/fritzy/SleekXMPP"
|
||||
|
||||
[[about]]
|
||||
name = "XMPP"
|
||||
desc = "Messaging protocol (also known as Jabber)"
|
||||
info = ["""
|
||||
XMPP is the Extensible Messaging and Presence Protocol, a set of open \
|
||||
technologies for instant messaging, presence, multi-party chat, voice and \
|
||||
video calls, collaboration, lightweight middleware, content syndication, \
|
||||
and generalized routing of XML data.
|
||||
|
||||
XMPP was originally developed in the Jabber open-source community to \
|
||||
provide an open, decentralized alternative to the closed instant messaging \
|
||||
services at that time.
|
||||
"""]
|
||||
url = "https://xmpp.org/about"
|
||||
|
||||
[[authors]]
|
||||
title = "Authors"
|
||||
subtitle = "The people who have made KaikOut"
|
||||
|
||||
[[authors]]
|
||||
name = "Schimon Zackary"
|
||||
role = "Author and Creator"
|
||||
info = ["""
|
||||
A middle eastern cowboy, farmer, pianist and lawyer who engages in criminal \
|
||||
and corporate law, and who took a decision to make a moderation bot for XMPP, \
|
||||
after he was informed that several spy agencies attempt to discredit the XMPP \
|
||||
network, albeit they make an extensive use of XMPP themselves.
|
||||
"""]
|
||||
url = "http://schimon.i2p"
|
||||
|
||||
[[legal]]
|
||||
title = "Legal"
|
||||
subtitle = "Legal Notice"
|
||||
|
||||
[[legal]]
|
||||
info = ["""
|
||||
Kaikout is free software; you can redistribute it and/or modify it under the \
|
||||
terms of the AGPL License version 3.
|
||||
|
||||
Kaikout 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 AGPL License (version 3) for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""]
|
||||
link = "https://git.xmpp-it.net/sch/Kaikout"
|
||||
|
||||
[[license]]
|
||||
title = "License"
|
||||
subtitle = "AGPL-3.0-only"
|
||||
|
||||
[[license]]
|
||||
info = ["""
|
||||
KaikOut moderation chat bot for the XMPP communication network.
|
||||
Copyright (C) 2024 Schimon Zachary
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License version 3 as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""]
|
||||
owner = "Schimon Zachary"
|
||||
|
||||
[[support]]
|
||||
title = "Support"
|
||||
subtitle = "Kaikout Support Groupchat"
|
||||
|
||||
[[support]]
|
||||
jid = "xmpp:kaikout@chat.woodpeckersnest.space?join"
|
||||
lang = "de, en, fr, ja, nl"
|
||||
|
||||
[[operators]]
|
||||
title = "Operators"
|
||||
subtitle = "Kaikout Operators"
|
||||
|
||||
[[operators]]
|
||||
name = "Mr. Operator"
|
||||
info = "No operator was specified for this instance."
|
||||
|
||||
[[policies]]
|
||||
title = "Policies"
|
||||
subtitle = "Terms of service"
|
||||
|
||||
[[policies]]
|
||||
name = "Terms and Conditions"
|
||||
info = ["""
|
||||
Abusers will be baptized.
|
||||
"""]
|
||||
|
||||
[[policies]]
|
||||
name = "Privacy Policy"
|
||||
info = ["""
|
||||
1. KaikOut logs public message activity, including status messages and aliases;
|
||||
2. KaikOut logs private activities of moderators, including Jabber ID;
|
||||
3. KaikOut reports moderators private activities to respective groupchat owners;
|
||||
4. KaikOut does not log Jabber ID addresses, excluding Jabber ID addresses of \
|
||||
banned and outcasted Jabber IDs.
|
||||
"""]
|
||||
|
||||
[[clients]]
|
||||
title = "Clients"
|
||||
subtitle = """
|
||||
As a chat bot, Kaikout works with any XMPP messenger, yet we have deemed it \
|
||||
appropriate to list the software that work best with Kaikout, namely those \
|
||||
that provide support for XEP-0050: Ad-Hoc Commands.
|
||||
"""
|
||||
|
||||
[[clients]]
|
||||
name = "Cheogram"
|
||||
desc = "XMPP client for mobile"
|
||||
info = ["""
|
||||
The Cheogram Android app allows you to join a worldwide communication network. \
|
||||
It especially focuses on features useful to users who want to contact those on \
|
||||
other networks as well, such as SMS-enabled phone numbers.
|
||||
"""]
|
||||
url = "https://cheogram.com"
|
||||
platform = "Android"
|
||||
|
||||
# [[clients]]
|
||||
# name = "Conversations"
|
||||
# info = "XMPP client for mobile"
|
||||
# url = "https://conversations.im"
|
||||
|
||||
[[clients]]
|
||||
name = "Converse"
|
||||
desc = "XMPP client for desktop and mobile"
|
||||
info = ["""
|
||||
Converse is a free and open-source XMPP chat client that runs in an HTML \
|
||||
browser or on your desktop.
|
||||
"""]
|
||||
url = "https://conversejs.org"
|
||||
platform = "HTML"
|
||||
|
||||
[[clients]]
|
||||
name = "Gajim"
|
||||
info = "XMPP client for desktop"
|
||||
url = "https://gajim.org"
|
||||
|
||||
# [[clients]]
|
||||
# name = "Monal IM"
|
||||
# info = "XMPP client for desktop and mobile"
|
||||
# url = "https://monal-im.org"
|
||||
|
||||
[[clients]]
|
||||
name = "monocles chat"
|
||||
desc = "XMPP client for mobile"
|
||||
info = """
|
||||
monocles chat is a modern and secure Android XMPP chat client. Based on \
|
||||
blabber.im and Conversations with a lot of changes and additional features \
|
||||
to improve usability and security.
|
||||
"""
|
||||
url = "https://monocles.chat"
|
||||
platform = "Android"
|
||||
|
||||
[[clients]]
|
||||
name = "Movim"
|
||||
desc = "XMPP client for desktop and mobile"
|
||||
info = ["""
|
||||
Movim is a social and chat platform that acts as a frontend for the XMPP network.
|
||||
|
||||
Once deployed Movim offers a complete social and chat experience for the \
|
||||
decentralized XMPP network users. It can easily connect to several XMPP \
|
||||
servers at the same time.
|
||||
|
||||
With a simple configuration it can also be restricted to one XMPP server \
|
||||
and will then act as a powerful frontend for it. Movim is fully compatible \
|
||||
with the most used XMPP servers such as ejabberd or Prosody.
|
||||
"""]
|
||||
url = "https://mov.im"
|
||||
platform = "HTML"
|
||||
|
||||
# [[clients]]
|
||||
# name = "Moxxy"
|
||||
# info = "XMPP client for mobile"
|
||||
# url = "https://moxxy.org"
|
||||
|
||||
[[clients]]
|
||||
name = "Poezio"
|
||||
desc = "XMPP client for console"
|
||||
info = ["""
|
||||
Poezio is a free console XMPP client (the protocol on which the Jabber IM \
|
||||
network is built).
|
||||
|
||||
Its goal is to let you connect very easily (no account creation needed) to \
|
||||
the network and join various chatrooms, immediately. It tries to look like \
|
||||
the most famous IRC clients (weechat, irssi, etc). Many commands are identical \
|
||||
and you won't be lost if you already know these clients. Configuration can be \
|
||||
made in a configuration file or directly from the client.
|
||||
"""]
|
||||
url = "https://poez.io"
|
||||
platform = "FreeBSD and Linux"
|
||||
|
||||
[[clients]]
|
||||
name = "Psi"
|
||||
desc = "XMPP client for desktop"
|
||||
info = ["""
|
||||
Instant messaging as free and open as it should be.
|
||||
|
||||
Psi is a free instant messaging application designed for the XMPP network. \
|
||||
Fast and lightweight, Psi is fully open-source and compatible with Windows, \
|
||||
Linux, and macOS.
|
||||
|
||||
With Psi's full Unicode support and localizations, easy file transfers, \
|
||||
customizable iconsets, and many other great features, you'll learn why users \
|
||||
around the world are making the switch to free, open instant messaging.
|
||||
"""]
|
||||
url = "https://psi-im.org"
|
||||
platform = "Any"
|
||||
|
||||
[[clients]]
|
||||
name = "Psi+"
|
||||
desc = "XMPP client for desktop"
|
||||
info = ["""
|
||||
In 2009 a Psi fork named Psi+ was started. Project purpose are: implementation \
|
||||
of new features, writing of patches and plugins for transferring them to upstream. \
|
||||
As of 2017 the most of active Psi+ developers have become official Psi developers, \
|
||||
but Psi+ still has a number of unique features. From developers point of view Psi+ \
|
||||
is just a development branch of Psi IM client which is hosted at separate git \
|
||||
repositories and for which rolling release development model is used.
|
||||
"""]
|
||||
url = "https://psi-plus.com"
|
||||
platform = "Any"
|
||||
|
||||
# [[clients]]
|
||||
# name = "Swift"
|
||||
# info = "XMPP client for desktop"
|
||||
# url = "https://swift.im"
|
||||
|
||||
# [[clients]]
|
||||
# name = "yaxim"
|
||||
# info = "XMPP client for mobile"
|
||||
# url = "https://yaxim.org"
|
||||
|
||||
[[resources]]
|
||||
title = "Useful Resources"
|
||||
subtitle = "Technologies which Kaikout is based upon"
|
||||
|
||||
[[resources]]
|
||||
name = "Slixmpp"
|
||||
info = "XMPP library"
|
||||
desc = """
|
||||
Slixmpp is an MIT licensed XMPP library for Python 3.7+. It is a fork of \
|
||||
SleekXMPP.
|
||||
|
||||
Slixmpp's goals is to only rewrite the core of the SleekXMPP library \
|
||||
(the low level socket handling, the timers, the events dispatching) \
|
||||
in order to remove all threads.
|
||||
"""
|
||||
url = "https://slixmpp.readthedocs.io"
|
||||
|
||||
[[resources]]
|
||||
name = "XMPP"
|
||||
info = "Messaging protocol (also known as Jabber)"
|
||||
desc = """
|
||||
XMPP is the Extensible Messaging and Presence Protocol, a set of open \
|
||||
technologies for instant messaging, presence, multi-party chat, voice and \
|
||||
video calls, collaboration, lightweight middleware, content syndication, \
|
||||
and generalized routing of XML data.
|
||||
|
||||
XMPP was originally developed in the Jabber open-source community to \
|
||||
provide an open, decentralized alternative to the closed instant messaging \
|
||||
services at that time.
|
||||
"""
|
||||
url = "https://xmpp.org/about"
|
57
kaikout/assets/accounts.toml
Normal file
57
kaikout/assets/accounts.toml
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Settings to tell the bot to which accounts to connect
|
||||
# and also from which accounts it receives instructions.
|
||||
|
||||
[xmpp.settings]
|
||||
reconnect_timeout = 3
|
||||
|
||||
[[xmpp.operators]]
|
||||
name = ""
|
||||
jid = ""
|
||||
|
||||
[xmpp.proxy.socks5]
|
||||
host = "127.0.0.1"
|
||||
port = 9050
|
||||
#username = ""
|
||||
#password = ""
|
||||
|
||||
[xmpp.profile]
|
||||
FN = "KaikOut"
|
||||
NICKNAME = "KaikOut"
|
||||
ROLE = "KaikOut Moderation Chat Bot"
|
||||
ORG = "KaikOut Inc."
|
||||
URL = "https://xmpp-it.net/sch/KaikOut"
|
||||
NOTE = "A moderation chat bot made for XMPP."
|
||||
BDAY = "20 June 2024"
|
||||
#TITLE = ""
|
||||
#DESC = ""
|
||||
|
||||
[xmpp.client]
|
||||
alias = "KaikOut"
|
||||
jid = "/KaikOut"
|
||||
password = ""
|
||||
|
||||
[tox]
|
||||
username = ""
|
||||
password = ""
|
||||
operator = ""
|
||||
|
||||
[session]
|
||||
username = ""
|
||||
password = ""
|
||||
operator = ""
|
||||
|
||||
[irc]
|
||||
username = ""
|
||||
password = ""
|
||||
#port = 6667
|
||||
operator = ""
|
||||
|
||||
[deltachat]
|
||||
username = ""
|
||||
password = ""
|
||||
operator = ""
|
||||
|
||||
[cabal]
|
||||
username = ""
|
||||
password = ""
|
||||
operator = ""
|
174
kaikout/assets/commands.toml
Normal file
174
kaikout/assets/commands.toml
Normal file
|
@ -0,0 +1,174 @@
|
|||
[all]
|
||||
all = """
|
||||
all
|
||||
Show all options.
|
||||
"""
|
||||
|
||||
[bookmarks]
|
||||
bookmark = """
|
||||
bookmark [+|-] <muc>
|
||||
Groupchat to add or remove.
|
||||
'+' appends to, '-' removes from.
|
||||
"""
|
||||
bookmarks = """
|
||||
bookmarks
|
||||
List bookmarked groupchats.
|
||||
"""
|
||||
|
||||
[filters]
|
||||
allow = """
|
||||
allow [+|-] <keyword>
|
||||
Keywords to allow
|
||||
comma-separated keywords
|
||||
'+' appends to, '-' removes from.
|
||||
"""
|
||||
deny = """
|
||||
deny [+|-] <keyword>
|
||||
Keywords to block
|
||||
comma-separated keywords
|
||||
'+' appends to, '-' removes from.
|
||||
"""
|
||||
clear = """
|
||||
clear allow/deny
|
||||
Clear allow or deny list.
|
||||
"""
|
||||
reset = """
|
||||
Reset allow/deny
|
||||
Reset allow or deny list.
|
||||
"""
|
||||
|
||||
[groupchat]
|
||||
uri = """
|
||||
<muc>
|
||||
Join groupchat by given <muc> (prefix xmpp).
|
||||
"""
|
||||
join = """
|
||||
join <muc>
|
||||
Join groupchat by given <muc>.
|
||||
"""
|
||||
leave = """
|
||||
goodbye
|
||||
Leave groupchat and delete it from bookmarks.
|
||||
"""
|
||||
|
||||
[manual]
|
||||
all = """
|
||||
help all
|
||||
Print a complete list of commands.
|
||||
"""
|
||||
help = """
|
||||
help
|
||||
Print list of command types.
|
||||
"""
|
||||
command = """
|
||||
help key command
|
||||
Print command usage and description.
|
||||
"""
|
||||
info = """
|
||||
info
|
||||
Print information page.
|
||||
"""
|
||||
key = """
|
||||
help <key>
|
||||
Print list of commands for selected command type.
|
||||
"""
|
||||
|
||||
[options]
|
||||
action = """
|
||||
action <boolean>
|
||||
Set action. 1 for ban, 0 for devoice.
|
||||
"""
|
||||
count = """
|
||||
count <number>
|
||||
Number of maximum allowed words to repeat per message.
|
||||
"""
|
||||
default = """
|
||||
default <key>
|
||||
Restore a setting to its default value.
|
||||
"""
|
||||
defaults = """
|
||||
defaults
|
||||
Restore all settings to default value.
|
||||
"""
|
||||
finished = """
|
||||
finished [off|on]
|
||||
Display a consequent message with the amount of a done task time.
|
||||
"""
|
||||
frequency_messages = """
|
||||
frequency messages <number>
|
||||
Minimum allowed frequency for sending consequent messages.
|
||||
"""
|
||||
frequency_presence = """
|
||||
frequency presence <number>
|
||||
Minimum allowed frequency for sending consequent status messages.
|
||||
"""
|
||||
inactivity = """
|
||||
inactivity [off|on]
|
||||
Check for inactivity.
|
||||
"""
|
||||
inactivity_span = """
|
||||
inactivity span <number>
|
||||
The maximum allowed time (in days) of inactivity.
|
||||
"""
|
||||
inactivity_warn = """
|
||||
inactivity warn <number>
|
||||
The time (in minutes) of inactivity to send a warning upon before action. Value can not be higher than of inactivity_span.
|
||||
"""
|
||||
kick = """
|
||||
kick <alias>
|
||||
Kick a participant by specified alias.
|
||||
"""
|
||||
message = """
|
||||
message [off|on]
|
||||
Check messages for faults.
|
||||
"""
|
||||
options = """
|
||||
options
|
||||
List options.
|
||||
"""
|
||||
score_messages = """
|
||||
score messages <number>
|
||||
Number of maximum allowed message faults before committing an action.
|
||||
"""
|
||||
score_presence = """
|
||||
score presence <number>
|
||||
Number of maximum allowed presence faults before committing an action.
|
||||
"""
|
||||
scores = """
|
||||
scores <jid>
|
||||
Display scores of specified Jabber ID.
|
||||
"""
|
||||
scores_reset = """
|
||||
scores reset <jid>
|
||||
Reset scores of specified Jabber ID.
|
||||
"""
|
||||
start = """
|
||||
start
|
||||
Enable bot and send updates.
|
||||
"""
|
||||
status = """
|
||||
status [off|on]
|
||||
Check status messages for faults.
|
||||
"""
|
||||
stop = """
|
||||
stop
|
||||
Disable bot and stop updates.
|
||||
"""
|
||||
timer = """
|
||||
timer <number>
|
||||
Timer value (in seconds) for countdown before committing an action.
|
||||
"""
|
||||
|
||||
[statistics]
|
||||
score = """
|
||||
scores <jid>
|
||||
Display scores for a given Jabber ID.
|
||||
"""
|
||||
reset = """
|
||||
scores reset <jid>
|
||||
Reset scores of a given Jabber ID.
|
||||
"""
|
||||
stats = """
|
||||
stats
|
||||
Show general statistics.
|
||||
"""
|
76
kaikout/assets/settings.toml
Normal file
76
kaikout/assets/settings.toml
Normal file
|
@ -0,0 +1,76 @@
|
|||
# This file lists default settings per database.
|
||||
# See file /usr/share/kaikout/settings.toml
|
||||
|
||||
[defaults]
|
||||
action = 0 # 1 to ban, or 0 to devioce.
|
||||
check_inactivity = 0 # Enable inactivity detection.
|
||||
check_message = 1 # Enable detection of message abuse.
|
||||
check_status = 1 # Enable detection of status message abuse.
|
||||
count = 9 # The maximum allowed number of instances of a word or a phrase in a message.
|
||||
enabled = 1 # Work status (Value 0 to disable).
|
||||
finished = 0 # Send an extra message which indicates of the amount of time of a done task (Value 1 to enable).
|
||||
frequency_messages = 1 # The maximum allowed frequency (in seconds) of sent messages.
|
||||
frequency_presence = 180 # The maximum allowed frequency (in seconds) of changed status messages.
|
||||
inactivity_span = 30 # The maximum allowed time (in days) of inactivity.
|
||||
inactivity_warn = 300 # The time (in minutes) of inactivity to send a warning upon before action. Value can not be higher than of inactivity_span.
|
||||
score_messages = 3 # The maximum allowed number of message faults to act upon.
|
||||
score_presence = 10 # The maximum allowed number of presence faults to act upon.
|
||||
timer = 180 # Timer value (in seconds) for countdown before committing an action.
|
||||
allow = [
|
||||
"jesus saves",
|
||||
"oracle broadcasting radio",
|
||||
"real liberty media",
|
||||
"woop ass"
|
||||
]
|
||||
deny = [
|
||||
"666",
|
||||
"arse",
|
||||
"arsehead",
|
||||
"arsehole",
|
||||
"ass hole",
|
||||
"asshole",
|
||||
"bollocks",
|
||||
"bugger",
|
||||
"christ on a bike",
|
||||
"christ on a cracker",
|
||||
"cock sucker",
|
||||
"cocksucker",
|
||||
"damm it",
|
||||
"damn it",
|
||||
"dammit",
|
||||
"damnit",
|
||||
"dick head",
|
||||
"dickhead",
|
||||
"dumb ass",
|
||||
"dumbass",
|
||||
"frigger",
|
||||
"fuck you",
|
||||
"fucker",
|
||||
"fucking",
|
||||
"god damn",
|
||||
"god damnit",
|
||||
"god damm",
|
||||
"god dammit",
|
||||
"goddammed",
|
||||
"goddammit",
|
||||
"goddamn",
|
||||
"goddamned",
|
||||
"goddamnit",
|
||||
"holy shit",
|
||||
"holyshit",
|
||||
"horseshit",
|
||||
"in shit",
|
||||
"jack-ass",
|
||||
"jackarse",
|
||||
"jackass",
|
||||
"jesus fuck",
|
||||
"jesus wept",
|
||||
"nigga",
|
||||
"shite",
|
||||
"son of a bitch",
|
||||
"son of a whore",
|
||||
"wanker"
|
||||
]
|
||||
|
||||
[ipc]
|
||||
bsd = 0 # IPC (BSD/UDS) POSIX sockets
|
70
kaikout/config.py
Normal file
70
kaikout/config.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from kaikout.log import Logger
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
import tomllib
|
||||
except:
|
||||
import tomli as tomllib
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
|
||||
def get_default_data_directory():
|
||||
if os.environ.get('HOME'):
|
||||
data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
|
||||
return os.path.join(data_home, 'kaikout')
|
||||
elif sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__) + '/kaikout_data')
|
||||
else:
|
||||
return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
|
||||
|
||||
|
||||
def get_default_config_directory():
|
||||
"""
|
||||
Determine the directory path where configuration will be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(os.environ.get('HOME'), '.config')
|
||||
return os.path.join(config_home, 'kaikout')
|
||||
|
||||
|
||||
def get_values(filename, key=None):
|
||||
config_dir = Config.get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/kaikout/'
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = os.path.dirname(__file__) + "/assets"
|
||||
config_file = os.path.join(config_dir, filename)
|
||||
with open(config_file, mode="rb") as defaults:
|
||||
result = tomllib.load(defaults)
|
||||
values = result[key] if key else result
|
||||
return values
|
513
kaikout/database.py
Normal file
513
kaikout/database.py
Normal file
|
@ -0,0 +1,513 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from asyncio import Lock
|
||||
from kaikout.log import Logger
|
||||
from sqlite3 import connect, Error, IntegrityError
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import tomli_w
|
||||
import tomllib
|
||||
|
||||
# from eliot import start_action, to_file
|
||||
# # with start_action(action_type="list_feeds()", db=db_file):
|
||||
# # with start_action(action_type="last_entries()", num=num):
|
||||
# # with start_action(action_type="get_feeds()"):
|
||||
# # with start_action(action_type="remove_entry()", source=source):
|
||||
# # with start_action(action_type="search_entries()", query=query):
|
||||
# # with start_action(action_type="check_entry()", link=link):
|
||||
|
||||
CURSORS = {}
|
||||
|
||||
# aiosqlite
|
||||
DBLOCK = Lock()
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class SQLite:
|
||||
|
||||
|
||||
def create_connection(db_file):
|
||||
"""
|
||||
Create a database connection to the SQLite database
|
||||
specified by db_file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
conn : object
|
||||
Connection object or None.
|
||||
"""
|
||||
time_begin = time.time()
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
message_log = '{}'
|
||||
logger.debug(message_log.format(function_name))
|
||||
conn = None
|
||||
try:
|
||||
conn = connect(db_file)
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
# return conn
|
||||
except Error as e:
|
||||
logger.warning('Error creating a connection to database {}.'.format(db_file))
|
||||
logger.error(e)
|
||||
time_end = time.time()
|
||||
difference = time_end - time_begin
|
||||
if difference > 1: logger.warning('{} (time: {})'.format(function_name,
|
||||
difference))
|
||||
return conn
|
||||
|
||||
|
||||
def create_tables(db_file):
|
||||
"""
|
||||
Create SQLite tables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {}'
|
||||
.format(function_name, db_file))
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
activity_table_sql = (
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS activity (
|
||||
id INTEGER NOT NULL,
|
||||
stanza_id TEXT,
|
||||
alias TEXT,
|
||||
jid TEXT,
|
||||
body TEXT,
|
||||
thread TEXT,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
"""
|
||||
)
|
||||
filters_table_sql = (
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS filters (
|
||||
id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
"""
|
||||
)
|
||||
outcast_table_sql = (
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS outcast (
|
||||
id INTEGER NOT NULL,
|
||||
alias TEXT,
|
||||
jid TEXT,
|
||||
reason TEXT,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
"""
|
||||
)
|
||||
settings_table_sql = (
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value INTEGER,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
"""
|
||||
)
|
||||
cur = conn.cursor()
|
||||
# cur = get_cursor(db_file)
|
||||
cur.execute(activity_table_sql)
|
||||
cur.execute(filters_table_sql)
|
||||
cur.execute(outcast_table_sql)
|
||||
cur.execute(settings_table_sql)
|
||||
|
||||
|
||||
def get_cursor(db_file):
|
||||
"""
|
||||
Allocate a cursor to connection per database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
CURSORS[db_file] : object
|
||||
Cursor.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {}'
|
||||
.format(function_name, db_file))
|
||||
if db_file in CURSORS:
|
||||
return CURSORS[db_file]
|
||||
else:
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
cur = conn.cursor()
|
||||
CURSORS[db_file] = cur
|
||||
return CURSORS[db_file]
|
||||
|
||||
|
||||
async def import_feeds(db_file, feeds):
|
||||
"""
|
||||
Insert a new feed into the feeds table.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
feeds : list
|
||||
Set of feeds (Title and URL).
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {}'
|
||||
.format(function_name, db_file))
|
||||
async with DBLOCK:
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
cur = conn.cursor()
|
||||
for feed in feeds:
|
||||
logger.debug('{}: feed: {}'
|
||||
.format(function_name, feed))
|
||||
url = feed['url']
|
||||
title = feed['title']
|
||||
sql = (
|
||||
"""
|
||||
INSERT
|
||||
INTO feeds_properties(
|
||||
title, url)
|
||||
VALUES(
|
||||
?, ?)
|
||||
"""
|
||||
)
|
||||
par = (title, url)
|
||||
try:
|
||||
cur.execute(sql, par)
|
||||
except IntegrityError as e:
|
||||
logger.warning("Skipping: " + str(url))
|
||||
logger.error(e)
|
||||
|
||||
|
||||
async def add_metadata(db_file):
|
||||
"""
|
||||
Insert a new feed into the feeds table.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {}'
|
||||
.format(function_name, db_file))
|
||||
async with DBLOCK:
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"""
|
||||
SELECT id
|
||||
FROM feeds_properties
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
)
|
||||
ixs = cur.execute(sql).fetchall()
|
||||
for ix in ixs:
|
||||
feed_id = ix[0]
|
||||
# Set feed status
|
||||
sql = (
|
||||
"""
|
||||
INSERT
|
||||
INTO feeds_state(
|
||||
feed_id)
|
||||
VALUES(
|
||||
?)
|
||||
"""
|
||||
)
|
||||
par = (feed_id,)
|
||||
try:
|
||||
cur.execute(sql, par)
|
||||
except IntegrityError as e:
|
||||
logger.warning(
|
||||
"Skipping feed_id {} for table feeds_state".format(feed_id))
|
||||
logger.error(e)
|
||||
# Set feed preferences.
|
||||
sql = (
|
||||
"""
|
||||
INSERT
|
||||
INTO feeds_preferences(
|
||||
feed_id)
|
||||
VALUES(
|
||||
?)
|
||||
"""
|
||||
)
|
||||
par = (feed_id,)
|
||||
try:
|
||||
cur.execute(sql, par)
|
||||
except IntegrityError as e:
|
||||
logger.warning(
|
||||
"Skipping feed_id {} for table feeds_preferences".format(feed_id))
|
||||
logger.error(e)
|
||||
|
||||
|
||||
async def insert_feed(db_file, url, title, identifier, entries=None, version=None,
|
||||
encoding=None, language=None, status_code=None,
|
||||
updated=None):
|
||||
"""
|
||||
Insert a new feed into the feeds table.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
url : str
|
||||
URL.
|
||||
title : str
|
||||
Feed title.
|
||||
identifier : str
|
||||
Feed identifier.
|
||||
entries : int, optional
|
||||
Number of entries. The default is None.
|
||||
version : str, optional
|
||||
Type of feed. The default is None.
|
||||
encoding : str, optional
|
||||
Encoding of feed. The default is None.
|
||||
language : str, optional
|
||||
Language code of feed. The default is None.
|
||||
status : str, optional
|
||||
HTTP status code. The default is None.
|
||||
updated : ???, optional
|
||||
Date feed was last updated. The default is None.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {} url: {}'
|
||||
.format(function_name, db_file, url))
|
||||
async with DBLOCK:
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"""
|
||||
INSERT
|
||||
INTO feeds_properties(
|
||||
url, title, identifier, entries, version, encoding, language, updated)
|
||||
VALUES(
|
||||
?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
)
|
||||
par = (url, title, identifier, entries, version, encoding, language, updated)
|
||||
cur.execute(sql, par)
|
||||
sql = (
|
||||
"""
|
||||
SELECT id
|
||||
FROM feeds_properties
|
||||
WHERE url = :url
|
||||
"""
|
||||
)
|
||||
par = (url,)
|
||||
feed_id = cur.execute(sql, par).fetchone()[0]
|
||||
sql = (
|
||||
"""
|
||||
INSERT
|
||||
INTO feeds_state(
|
||||
feed_id, status_code, valid)
|
||||
VALUES(
|
||||
?, ?, ?)
|
||||
"""
|
||||
)
|
||||
par = (feed_id, status_code, 1)
|
||||
cur.execute(sql, par)
|
||||
sql = (
|
||||
"""
|
||||
INSERT
|
||||
INTO feeds_preferences(
|
||||
feed_id)
|
||||
VALUES(
|
||||
?)
|
||||
"""
|
||||
)
|
||||
par = (feed_id,)
|
||||
cur.execute(sql, par)
|
||||
|
||||
|
||||
async def remove_feed_by_url(db_file, url):
|
||||
"""
|
||||
Delete a feed by feed URL.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
url : str
|
||||
URL of feed.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {} url: {}'
|
||||
.format(function_name, db_file, url))
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
async with DBLOCK:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"""
|
||||
DELETE
|
||||
FROM feeds_properties
|
||||
WHERE url = ?
|
||||
"""
|
||||
)
|
||||
par = (url,)
|
||||
cur.execute(sql, par)
|
||||
|
||||
|
||||
async def remove_feed_by_index(db_file, ix):
|
||||
"""
|
||||
Delete a feed by feed ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
ix : str
|
||||
Index of feed.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {} ix: {}'
|
||||
.format(function_name, db_file, ix))
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
async with DBLOCK:
|
||||
cur = conn.cursor()
|
||||
# # NOTE Should we move DBLOCK to this line? 2022-12-23
|
||||
# sql = (
|
||||
# "DELETE "
|
||||
# "FROM entries "
|
||||
# "WHERE feed_id = ?"
|
||||
# )
|
||||
# par = (url,)
|
||||
# cur.execute(sql, par) # Error? 2024-01-05
|
||||
# sql = (
|
||||
# "DELETE "
|
||||
# "FROM archive "
|
||||
# "WHERE feed_id = ?"
|
||||
# )
|
||||
# par = (url,)
|
||||
# cur.execute(sql, par)
|
||||
sql = (
|
||||
"""
|
||||
DELETE
|
||||
FROM feeds_properties
|
||||
WHERE id = ?
|
||||
"""
|
||||
)
|
||||
par = (ix,)
|
||||
cur.execute(sql, par)
|
||||
|
||||
|
||||
def get_feeds_by_tag_id(db_file, tag_id):
|
||||
"""
|
||||
Get feeds of given tag.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
tag_id : str
|
||||
Tag ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : tuple
|
||||
List of tags.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {} tag_id: {}'
|
||||
.format(function_name, db_file, tag_id))
|
||||
with SQLite.create_connection(db_file) as conn:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"""
|
||||
SELECT feeds_properties.*
|
||||
FROM feeds_properties
|
||||
INNER JOIN tagged_feeds ON feeds_properties.id = tagged_feeds.feed_id
|
||||
INNER JOIN tags ON tags.id = tagged_feeds.tag_id
|
||||
WHERE tags.id = ?
|
||||
ORDER BY feeds_properties.title;
|
||||
"""
|
||||
)
|
||||
par = (tag_id,)
|
||||
result = cur.execute(sql, par).fetchall()
|
||||
return result
|
||||
|
||||
|
||||
class Toml:
|
||||
|
||||
|
||||
def instantiate(self, room):
|
||||
"""
|
||||
Callback function to instantiate action on database.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jid_file : str
|
||||
Filename.
|
||||
callback : ?
|
||||
Function name.
|
||||
message : str, optional
|
||||
Optional kwarg when a message is a part or
|
||||
required argument. The default is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
object
|
||||
Coroutine object.
|
||||
"""
|
||||
data_dir = Toml.get_default_data_directory()
|
||||
if not os.path.isdir(data_dir):
|
||||
os.mkdir(data_dir)
|
||||
if not os.path.isdir(data_dir + "/toml"):
|
||||
os.mkdir(data_dir + "/toml")
|
||||
filename = os.path.join(data_dir, "toml", r"{}.toml".format(room))
|
||||
if not os.path.exists(filename):
|
||||
Toml.create_settings_file(self, filename)
|
||||
Toml.load_jid_settings(self, room, filename)
|
||||
return filename
|
||||
|
||||
|
||||
def get_default_data_directory():
|
||||
if os.environ.get('HOME'):
|
||||
data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
|
||||
return os.path.join(data_home, 'kaikout')
|
||||
elif sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__) + '/kaikout_data')
|
||||
else:
|
||||
return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
|
||||
|
||||
|
||||
def get_data_file(data_dir, room):
|
||||
toml_file = os.path.join(data_dir, "toml", r"{}.toml".format(room))
|
||||
return toml_file
|
||||
|
||||
|
||||
def create_settings_file(self, filename):
|
||||
data = self.defaults
|
||||
content = tomli_w.dumps(data)
|
||||
with open(filename, 'w') as f: f.write(content)
|
||||
|
||||
|
||||
def load_jid_settings(self, room, filename):
|
||||
# data_dir = Toml.get_default_data_directory()
|
||||
# filename = Toml.get_data_file(data_dir, room)
|
||||
with open(filename, 'rb') as f: self.settings[room] = tomllib.load(f)
|
||||
|
||||
|
||||
def update_jid_settings(self, room, filename, key, value):
|
||||
with open(filename, 'rb') as f: data = tomllib.load(f)
|
||||
self.settings[room][key] = value
|
||||
data = self.settings[room]
|
||||
content = tomli_w.dumps(data)
|
||||
with open(filename, 'w') as f: f.write(content)
|
||||
|
61
kaikout/log.py
Normal file
61
kaikout/log.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
TODO Rename module to console or print
|
||||
|
||||
To use this class, first, instantiate Logger with the name of your module
|
||||
or class, then call the appropriate logging methods on that instance.
|
||||
|
||||
logger = Logger(__name__)
|
||||
logger.debug('This is a debug message')
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
|
||||
class Logger:
|
||||
|
||||
|
||||
def __init__(self, name):
|
||||
self.logger = logging.getLogger(name)
|
||||
self.logger.setLevel(logging.WARNING) # DEBUG
|
||||
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.WARNING)
|
||||
|
||||
formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(name)s: %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
|
||||
self.logger.addHandler(ch)
|
||||
|
||||
def critical(self, message):
|
||||
self.logger.critical(message)
|
||||
|
||||
def debug(self, message):
|
||||
self.logger.debug(message)
|
||||
|
||||
def error(self, message):
|
||||
self.logger.error(message)
|
||||
|
||||
def info(self, message):
|
||||
self.logger.info(message)
|
||||
|
||||
def warning(self, message):
|
||||
self.logger.warning(message)
|
||||
|
||||
# def check_difference(function_name, difference):
|
||||
# if difference > 1:
|
||||
# Logger.warning(message)
|
||||
|
||||
|
||||
class Message:
|
||||
|
||||
|
||||
def printer(text):
|
||||
now = datetime.now()
|
||||
current_time = now.strftime("%H:%M:%S")
|
||||
print('{} {}'.format(current_time, text), end='\r')
|
245
kaikout/utilities.py
Normal file
245
kaikout/utilities.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import csv
|
||||
from email.utils import parseaddr
|
||||
import hashlib
|
||||
from kaikout.database import Toml
|
||||
from kaikout.log import Logger
|
||||
import kaikout.sqlite as sqlite
|
||||
import os
|
||||
import sys
|
||||
import tomli_w
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except:
|
||||
import tomli as tomllib
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
|
||||
def get_default_data_directory():
|
||||
"""
|
||||
Determine the directory path where data will be stored.
|
||||
|
||||
* If $XDG_DATA_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to data directory.
|
||||
"""
|
||||
if os.environ.get('HOME'):
|
||||
data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
|
||||
return os.path.join(data_home, 'kaikout')
|
||||
elif sys.platform == 'win32':
|
||||
data_home = os.environ.get('APPDATA')
|
||||
if data_home is None:
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__) + '/kaikout_data')
|
||||
else:
|
||||
return os.path.join(os.path.dirname(__file__) + '/kaikout_data')
|
||||
|
||||
|
||||
def get_default_config_directory():
|
||||
"""
|
||||
Determine the directory path where configuration will be stored.
|
||||
|
||||
* If $XDG_CONFIG_HOME is defined, use it;
|
||||
* else if $HOME exists, use it;
|
||||
* else if the platform is Windows, use %APPDATA%;
|
||||
* else use the current directory.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Path to configuration directory.
|
||||
"""
|
||||
# config_home = xdg.BaseDirectory.xdg_config_home
|
||||
config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||
if config_home is None:
|
||||
if os.environ.get('HOME') is None:
|
||||
if sys.platform == 'win32':
|
||||
config_home = os.environ.get('APPDATA')
|
||||
if config_home is None:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
return os.path.abspath('.')
|
||||
else:
|
||||
config_home = os.path.join(
|
||||
os.environ.get('HOME'), '.config'
|
||||
)
|
||||
return os.path.join(config_home, 'kaikout')
|
||||
|
||||
|
||||
def get_setting_value(db_file, key):
|
||||
value = sqlite.get_setting_value(db_file, key)
|
||||
if value:
|
||||
value = value[0]
|
||||
else:
|
||||
value = Config.get_value('settings', 'Settings', key)
|
||||
return value
|
||||
|
||||
|
||||
def get_values(filename, key=None):
|
||||
config_dir = Config.get_default_config_directory()
|
||||
if not os.path.isdir(config_dir): config_dir = '/usr/share/kaikout/'
|
||||
if not os.path.isdir(config_dir): config_dir = os.path.dirname(__file__) + "/assets"
|
||||
config_file = os.path.join(config_dir, filename)
|
||||
with open(config_file, mode="rb") as f: result = tomllib.load(f)
|
||||
values = result[key] if key else result
|
||||
return values
|
||||
|
||||
|
||||
class Documentation:
|
||||
|
||||
|
||||
def manual(filename, section=None, command=None):
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: filename: {}'.format(function_name, filename))
|
||||
config_dir = Config.get_default_config_directory()
|
||||
with open(config_dir + '/' + filename, mode="rb") as f: cmds = tomllib.load(f)
|
||||
if section == 'all':
|
||||
cmd_list = ''
|
||||
for cmd in cmds:
|
||||
for i in cmds[cmd]:
|
||||
cmd_list += cmds[cmd][i] + '\n'
|
||||
elif command and section:
|
||||
try:
|
||||
cmd_list = cmds[section][command]
|
||||
except KeyError as e:
|
||||
logger.error(e)
|
||||
cmd_list = None
|
||||
elif section:
|
||||
try:
|
||||
cmd_list = []
|
||||
for cmd in cmds[section]:
|
||||
cmd_list.extend([cmd])
|
||||
except KeyError as e:
|
||||
logger.error('KeyError:' + str(e))
|
||||
cmd_list = None
|
||||
else:
|
||||
cmd_list = []
|
||||
for cmd in cmds:
|
||||
cmd_list.extend([cmd])
|
||||
return cmd_list
|
||||
|
||||
|
||||
class Log:
|
||||
|
||||
|
||||
def csv(filename, fields):
|
||||
"""
|
||||
Log message to CSV file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message : slixmpp.stanza.message.Message
|
||||
Message object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None.
|
||||
"""
|
||||
data_dir = Config.get_default_data_directory()
|
||||
if not os.path.isdir(data_dir): os.mkdir(data_dir)
|
||||
if not os.path.isdir(data_dir + "/logs"): os.mkdir(data_dir + "/logs")
|
||||
csv_file = os.path.join(data_dir, "logs", r"{}.csv".format(filename))
|
||||
if not os.path.exists(csv_file):
|
||||
columns = ['type', 'timestamp', 'alias', 'body', 'lang', 'identifier']
|
||||
with open(csv_file, 'a') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(columns)
|
||||
with open(csv_file, 'a') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(fields)
|
||||
|
||||
|
||||
def toml(self, room, fields, stanza_type):
|
||||
"""
|
||||
Log message to TOML file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
room : str
|
||||
Group chat Jabber ID.
|
||||
fields : list
|
||||
alias, room, identifier, timestamp.
|
||||
stanza_type: str
|
||||
message or presence.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None.
|
||||
"""
|
||||
alias, content, identifier, timestamp = fields
|
||||
data_dir = Toml.get_default_data_directory()
|
||||
filename = Toml.get_data_file(data_dir, room)
|
||||
# filename = room + '.toml'
|
||||
entry = {}
|
||||
entry['alias'] = alias
|
||||
entry['body'] = content
|
||||
entry['id'] = identifier
|
||||
entry['timestamp'] = timestamp
|
||||
activity_type = 'activity_' + stanza_type
|
||||
message_activity_list = self.settings[room][activity_type] if activity_type in self.settings[room] else []
|
||||
while len(message_activity_list) > 20: message_activity_list.pop(0)
|
||||
message_activity_list.append(entry)
|
||||
self.settings[room][activity_type] = message_activity_list # NOTE This directive might not be needed
|
||||
data = self.settings[room]
|
||||
content = tomli_w.dumps(data)
|
||||
with open(filename, 'w') as f: f.write(content)
|
||||
|
||||
|
||||
class Url:
|
||||
|
||||
|
||||
def check_xmpp_uri(uri):
|
||||
"""
|
||||
Check validity of XMPP URI.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
uri : str
|
||||
URI.
|
||||
|
||||
Returns
|
||||
-------
|
||||
jid : str
|
||||
JID or None.
|
||||
"""
|
||||
jid = urlsplit(uri).path
|
||||
if parseaddr(jid)[1] != jid:
|
||||
jid = False
|
||||
return jid
|
||||
|
||||
|
||||
class String:
|
||||
|
||||
|
||||
def md5_hash(url):
|
||||
"""
|
||||
Hash URL string to MD5 checksum.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
URL.
|
||||
|
||||
Returns
|
||||
-------
|
||||
url_digest : str
|
||||
Hashed URL as an MD5 checksum.
|
||||
"""
|
||||
url_encoded = url.encode()
|
||||
url_hashed = hashlib.md5(url_encoded)
|
||||
url_digest = url_hashed.hexdigest()
|
||||
return url_digest
|
2
kaikout/version.py
Normal file
2
kaikout/version.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
__version__ = '0.0.1'
|
||||
__version_info__ = (0, 0, 1)
|
99
kaikout/xmpp/bookmark.py
Normal file
99
kaikout/xmpp/bookmark.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
TODO
|
||||
|
||||
1) Save groupchat name instead of jid in field name.
|
||||
|
||||
"""
|
||||
|
||||
from slixmpp.plugins.xep_0048.stanza import Bookmarks
|
||||
|
||||
|
||||
class XmppBookmark:
|
||||
|
||||
|
||||
async def get_bookmarks(self):
|
||||
result = await self.plugin['xep_0048'].get_bookmarks()
|
||||
conferences = result['private']['bookmarks']['conferences']
|
||||
return conferences
|
||||
|
||||
|
||||
async def get_bookmark_properties(self, jid):
|
||||
result = await self.plugin['xep_0048'].get_bookmarks()
|
||||
groupchats = result['private']['bookmarks']['conferences']
|
||||
for groupchat in groupchats:
|
||||
if jid == groupchat['jid']:
|
||||
properties = {'password': groupchat['password'],
|
||||
'jid': groupchat['jid'],
|
||||
'name': groupchat['name'],
|
||||
'nick': groupchat['nick'],
|
||||
'autojoin': groupchat['autojoin'],
|
||||
'lang': groupchat['lang']}
|
||||
break
|
||||
return properties
|
||||
|
||||
|
||||
async def add(self, jid=None, properties=None):
|
||||
result = await self.plugin['xep_0048'].get_bookmarks()
|
||||
conferences = result['private']['bookmarks']['conferences']
|
||||
groupchats = []
|
||||
if properties:
|
||||
properties['jid'] = properties['room'] + '@' + properties['host']
|
||||
if not properties['alias']: properties['alias'] = self.alias
|
||||
else:
|
||||
properties = {
|
||||
'jid' : jid,
|
||||
'alias' : self.alias,
|
||||
'name' : jid.split('@')[0],
|
||||
'autojoin' : True,
|
||||
'password' : None,
|
||||
}
|
||||
for conference in conferences:
|
||||
if conference['jid'] != properties['jid']:
|
||||
groupchats.extend([conference])
|
||||
# FIXME Ad-hoc bookmark form is stuck
|
||||
# if jid not in groupchats:
|
||||
if properties['jid'] not in groupchats:
|
||||
bookmarks = Bookmarks()
|
||||
for groupchat in groupchats:
|
||||
# if groupchat['jid'] == groupchat['name']:
|
||||
# groupchat['name'] = groupchat['name'].split('@')[0]
|
||||
bookmarks.add_conference(groupchat['jid'],
|
||||
groupchat['nick'],
|
||||
name=groupchat['name'],
|
||||
autojoin=groupchat['autojoin'],
|
||||
password=groupchat['password'])
|
||||
bookmarks.add_conference(properties['jid'],
|
||||
properties['alias'],
|
||||
name=properties['name'],
|
||||
autojoin=properties['autojoin'],
|
||||
password=properties['password'])
|
||||
# await self.plugin['xep_0048'].set_bookmarks(bookmarks)
|
||||
self.plugin['xep_0048'].set_bookmarks(bookmarks)
|
||||
# bookmarks = Bookmarks()
|
||||
# await self.plugin['xep_0048'].set_bookmarks(bookmarks)
|
||||
# print(await self.plugin['xep_0048'].get_bookmarks())
|
||||
|
||||
# bm = BookmarkStorage()
|
||||
# bm.conferences.append(Conference(muc_jid, autojoin=True, nick=self.alias))
|
||||
# await self['xep_0402'].publish(bm)
|
||||
|
||||
|
||||
async def remove(self, jid):
|
||||
result = await self.plugin['xep_0048'].get_bookmarks()
|
||||
conferences = result['private']['bookmarks']['conferences']
|
||||
groupchats = []
|
||||
for conference in conferences:
|
||||
if not conference['jid'] == jid:
|
||||
groupchats.extend([conference])
|
||||
bookmarks = Bookmarks()
|
||||
for groupchat in groupchats:
|
||||
bookmarks.add_conference(groupchat['jid'],
|
||||
groupchat['nick'],
|
||||
name=groupchat['name'],
|
||||
autojoin=groupchat['autojoin'],
|
||||
password=groupchat['password'])
|
||||
await self.plugin['xep_0048'].set_bookmarks(bookmarks)
|
507
kaikout/xmpp/chat.py
Normal file
507
kaikout/xmpp/chat.py
Normal file
|
@ -0,0 +1,507 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# from slixmpp import JID
|
||||
from kaikout.database import Toml
|
||||
from kaikout.log import Logger
|
||||
from kaikout.utilities import Documentation
|
||||
from kaikout.xmpp.commands import XmppCommands
|
||||
from kaikout.xmpp.message import XmppMessage
|
||||
from kaikout.xmpp.muc import XmppMuc
|
||||
from kaikout.xmpp.status import XmppStatus
|
||||
from kaikout.xmpp.utilities import XmppUtilities
|
||||
import time
|
||||
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
# for task in main_task:
|
||||
# task.cancel()
|
||||
|
||||
# Deprecated in favour of event "presence_available"
|
||||
# if not main_task:
|
||||
# await select_file()
|
||||
|
||||
|
||||
class XmppChat:
|
||||
|
||||
|
||||
async def process_message(self, message):
|
||||
"""
|
||||
Process incoming message stanzas. Be aware that this also
|
||||
includes MUC messages and error messages. It is usually
|
||||
a good practice to check the messages's type before
|
||||
processing or sending replies.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message : str
|
||||
The received message stanza. See the documentation
|
||||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
# jid_bare = message['from'].bare
|
||||
# jid_full = str(message['from'])
|
||||
|
||||
# Process commands.
|
||||
message_type = message['type']
|
||||
message_body = message['body']
|
||||
if message_type == 'groupchat':
|
||||
alias = message['mucnick']
|
||||
room = message['mucroom']
|
||||
if (message['mucnick'] == self.alias or
|
||||
not XmppUtilities.is_moderator(self, room, alias) or
|
||||
not message_body.startswith('%')):
|
||||
return
|
||||
elif message_type in ('chat', 'normal'):
|
||||
jid = message['from']
|
||||
jid_bare = jid.bare
|
||||
jid_full = jid.full
|
||||
room = self.sessions[jid_bare] if jid_bare in self.sessions else message_body
|
||||
status_mode,status_text, message_response = None, None, None
|
||||
if '@' in room:
|
||||
if room in XmppMuc.get_joined_rooms(self):
|
||||
alias = await XmppUtilities.is_jid_of_moderators(
|
||||
self, room, jid_full)
|
||||
if jid_bare not in self.sessions:
|
||||
if alias:
|
||||
# alias = XmppMuc.get_alias(self, room, jid)
|
||||
# if XmppUtilities.is_moderator(self, room, alias):
|
||||
self.sessions[jid_bare] = room
|
||||
message_response = (
|
||||
'A session to configure groupchat {} has been '
|
||||
'established.'.format(room))
|
||||
status_mode = 'chat'
|
||||
status_text = 'Session is opened: {}'.format(room)
|
||||
owners = await XmppMuc.get_affiliation(self, room,
|
||||
'owner')
|
||||
for owner in owners:
|
||||
message_notification = (
|
||||
'A session for groupchat {} has been '
|
||||
'activated by moderator {}'
|
||||
.format(room, jid_bare))
|
||||
XmppMessage.send(
|
||||
self, owner, message_notification, 'chat')
|
||||
else:
|
||||
message_response = (
|
||||
'You do not appear to be a moderator of '
|
||||
'groupchat {}'.format(room))
|
||||
status_mode = 'available'
|
||||
status_text = (
|
||||
'Type the desired groupchat - in which you '
|
||||
'are a moderator at - to configure')
|
||||
moderators = await XmppMuc.get_role(
|
||||
self, room, 'moderator')
|
||||
message_notification = (
|
||||
'An unauthorized attempt to establish a '
|
||||
'session for groupchat {} has been made by {}'
|
||||
.format(room, jid_bare))
|
||||
for moderator in moderators:
|
||||
jid_full = XmppMuc.get_full_jid(self, room,
|
||||
moderator)
|
||||
XmppMessage.send(
|
||||
self, jid_full, message_notification, 'chat')
|
||||
elif not alias:
|
||||
del self.sessions[jid_bare]
|
||||
message_response = (
|
||||
'The session has been ended, because you are no '
|
||||
'longer a moderator at groupchat {}'.format(room))
|
||||
status_mode = 'away'
|
||||
status_text = 'Session is closed: {}'.format(room)
|
||||
moderators = await XmppMuc.get_role(
|
||||
self, room, 'moderator')
|
||||
message_notification = (
|
||||
'The session for groupchat {} with former '
|
||||
'moderator {} has been terminated.\n'
|
||||
'A termination message has been sent to {}'
|
||||
.format(room, jid_bare, jid_bare))
|
||||
for moderator in moderators:
|
||||
jid_full = XmppMuc.get_full_jid(self, room,
|
||||
moderator)
|
||||
XmppMessage.send(
|
||||
self, jid_full, message_notification, 'chat')
|
||||
else:
|
||||
room = self.sessions[jid_bare]
|
||||
else:
|
||||
message_response = (
|
||||
'Invite KaikOut to groupchat "{}" and try again.\n'
|
||||
'A session will not begin if KaikOut is not present '
|
||||
'in groupchat.'.format(room))
|
||||
else:
|
||||
message_response = ('The text "{}" does not appear to be a '
|
||||
'valid groupchat Jabber ID.'.format(room))
|
||||
if status_mode and status_text:
|
||||
XmppStatus.send_status_message(self, jid_full, status_mode,
|
||||
status_text)
|
||||
if message_response:
|
||||
XmppMessage.send_reply(self, message, message_response)
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
db_file = Toml.instantiate(self, room)
|
||||
|
||||
# # Support private message via groupchat
|
||||
# # See https://codeberg.org/poezio/slixmpp/issues/3506
|
||||
# if message_type == 'chat' and message.get_plugin('muc', check=True):
|
||||
# # jid_bare = message['from'].bare
|
||||
# if (jid_bare == jid_full[:jid_full.index('/')]):
|
||||
# # TODO Count and alert of MUC-PM attempts
|
||||
# return
|
||||
|
||||
command_time_start = time.time()
|
||||
|
||||
command = message_body[1:] if message_type == 'groupchat' else message_body
|
||||
command_lowercase = command.lower()
|
||||
|
||||
# if not self.settings[room]['enabled']:
|
||||
# if not command_lowercase.startswith('start'):
|
||||
# return
|
||||
|
||||
response = None
|
||||
match command_lowercase:
|
||||
case 'help':
|
||||
command_list = XmppCommands.print_help()
|
||||
response = ('Available command keys:\n'
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `help <key>`'
|
||||
.format(command_list))
|
||||
case 'help all':
|
||||
command_list = Documentation.manual('commands.toml',
|
||||
section='all')
|
||||
response = ('Complete list of commands:\n'
|
||||
'```\n{}\n```'
|
||||
.format(command_list))
|
||||
case _ if command_lowercase.startswith('help'):
|
||||
command = command[5:].lower()
|
||||
command = command.split(' ')
|
||||
if len(command) == 2:
|
||||
command_root = command[0]
|
||||
command_name = command[1]
|
||||
command_list = Documentation.manual(
|
||||
'commands.toml', section=command_root,
|
||||
command=command_name)
|
||||
if command_list:
|
||||
command_list = ''.join(command_list)
|
||||
response = (command_list)
|
||||
else:
|
||||
response = ('KeyError for {} {}'
|
||||
.format(command_root, command_name))
|
||||
elif len(command) == 1:
|
||||
command = command[0]
|
||||
command_list = Documentation.manual('commands.toml',
|
||||
command)
|
||||
if command_list:
|
||||
command_list = ' '.join(command_list)
|
||||
response = ('Available command `{}` keys:\n'
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `help {} <command>`'
|
||||
.format(command, command_list, command))
|
||||
else:
|
||||
response = 'KeyError for {}'.format(command)
|
||||
else:
|
||||
response = ('Invalid. Enter command key '
|
||||
'or command key & name')
|
||||
case 'info':
|
||||
entries = XmppCommands.print_info_list()
|
||||
response = ('Available command options:\n'
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `info <option>`'
|
||||
.format(entries))
|
||||
case _ if command_lowercase.startswith('info'):
|
||||
entry = command[5:].lower()
|
||||
response = XmppCommands.print_info_specific(entry)
|
||||
case _ if command_lowercase.startswith('action'):
|
||||
value = command[6:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'action', value)
|
||||
atype = 'ban' if value else 'devoice'
|
||||
response = 'Action has been set to {} ({})'.format(
|
||||
atype, value)
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['action'])
|
||||
case _ if command_lowercase.startswith('allow +'):
|
||||
value = command[7:]
|
||||
if value:
|
||||
response = XmppCommands.set_filter(
|
||||
self, room, db_file, value, 'allow', True)
|
||||
else:
|
||||
response = ('No action has been taken. '
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('allow -'):
|
||||
value = command[7:]
|
||||
if value:
|
||||
response = XmppCommands.set_filter(
|
||||
self, room, db_file, value, 'allow', False)
|
||||
else:
|
||||
response = ('No action has been taken. '
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('ban'):
|
||||
if command[4:]:
|
||||
value = command[4:].split()
|
||||
alias = value[0]
|
||||
reason = ' '.join(value[1:]) if len(value) > 1 else None
|
||||
await XmppCommands.outcast(self, room, alias, reason)
|
||||
case _ if command_lowercase.startswith('bookmark +'):
|
||||
if XmppUtilities.is_operator(self, jid_bare):
|
||||
muc_jid = command[11:]
|
||||
response = await XmppCommands.bookmark_add(
|
||||
self, muc_jid)
|
||||
else:
|
||||
response = ('This action is restricted. '
|
||||
'Type: adding bookmarks.')
|
||||
case _ if command_lowercase.startswith('bookmark -'):
|
||||
if XmppUtilities.is_operator(self, jid_bare):
|
||||
muc_jid = command[11:]
|
||||
response = await XmppCommands.bookmark_del(
|
||||
self, muc_jid)
|
||||
else:
|
||||
response = ('This action is restricted. '
|
||||
'Type: removing bookmarks.')
|
||||
case 'bookmarks':
|
||||
if XmppUtilities.is_operator(self, jid_bare):
|
||||
response = await XmppCommands.print_bookmarks(self)
|
||||
else:
|
||||
response = ('This action is restricted. '
|
||||
'Type: viewing bookmarks.')
|
||||
case _ if command_lowercase.startswith('clear'):
|
||||
key = command[6:]
|
||||
response = await XmppCommands.clear_filter(self, room, db_file,
|
||||
key)
|
||||
case _ if command_lowercase.startswith('count'):
|
||||
value = command[5:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'count', value)
|
||||
response = 'Count has been set to {}'.format(value)
|
||||
except:
|
||||
response = ('No action has been taken. '
|
||||
'Enter a numerical value.')
|
||||
else:
|
||||
response = str(self.settings[room]['count'])
|
||||
case _ if command_lowercase.startswith('default'):
|
||||
key = command[8:]
|
||||
if key:
|
||||
response = await XmppCommands.restore_default(
|
||||
self, room, db_file, key)
|
||||
else:
|
||||
response = ('No action has been taken. '
|
||||
'Missing key.')
|
||||
case 'defaults':
|
||||
response = await XmppCommands.restore_default(self, room,
|
||||
db_file)
|
||||
case _ if command_lowercase.startswith('deny +'):
|
||||
value = command[6:].strip()
|
||||
if value:
|
||||
response = XmppCommands.set_filter(
|
||||
self, room, db_file, value, 'deny', True)
|
||||
else:
|
||||
response = ('No action has been taken. '
|
||||
'Missing keywords.')
|
||||
case _ if command_lowercase.startswith('deny -'):
|
||||
value = command[6:].strip()
|
||||
if value:
|
||||
response = XmppCommands.set_filter(
|
||||
self, room, db_file, value, 'deny', False)
|
||||
else:
|
||||
response = ('No action has been taken. '
|
||||
'Missing keywords.')
|
||||
case 'finished off':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'finished', 0)
|
||||
response = 'Finished indicator has deactivated.'
|
||||
case 'finished on':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'finished', 1)
|
||||
response = 'Finished indicator has activated.'
|
||||
case _ if command_lowercase.startswith('frequency messages'):
|
||||
value = command[18:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'frequency_messages', value)
|
||||
response = ('Minimum allowed frequency for messages '
|
||||
'has been set to {}'.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['frequency_messages'])
|
||||
case _ if command_lowercase.startswith('frequency presence'):
|
||||
value = command[18:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'frequency_presence', value)
|
||||
response = ('Minimum allowed frequency for presence '
|
||||
'has been set to {}'.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['frequency_presence'])
|
||||
case 'goodbye':
|
||||
if message_type == 'groupchat':
|
||||
await XmppCommands.muc_leave(self, room)
|
||||
else:
|
||||
response = 'This command is valid in groupchat only.'
|
||||
case 'inactivity off':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'check_inactivity', 0)
|
||||
response = 'Inactivity check has been deactivated.'
|
||||
case 'inactivity on':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'check_inactivity', 1)
|
||||
response = 'Inactivity check has been activated.'
|
||||
case _ if command_lowercase.startswith('inactivity span'):
|
||||
value = command[15:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'inactivity_span', value)
|
||||
response = ('The maximum allowed time of inactivity '
|
||||
'has been set to {} days'.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['inactivity_span'])
|
||||
case _ if command_lowercase.startswith('inactivity warn'):
|
||||
value = command[15:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'inactivity_warn', value)
|
||||
response = ('The time of inactivity to send a warning '
|
||||
'upon before action has been set to {} '
|
||||
'minutes'.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['inactivity_warn'])
|
||||
case _ if command_lowercase.startswith('join'):
|
||||
muc_jid = command[5:]
|
||||
response = await XmppCommands.muc_join(self, muc_jid)
|
||||
case 'options':
|
||||
response = 'Options:\n```'
|
||||
response += XmppCommands.print_options(self, room)
|
||||
response += '\n```'
|
||||
case _ if command_lowercase.startswith('kick'):
|
||||
if command[5:]:
|
||||
value = command[5:].split()
|
||||
alias = value[0]
|
||||
reason = ' '.join(value[1:]) if len(value) > 1 else None
|
||||
await XmppCommands.kick(self, room, alias, reason)
|
||||
case 'message off':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'check_message', 0)
|
||||
response = 'Message check has been deactivated.'
|
||||
case 'message on':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'check_message', 1)
|
||||
response = 'Message check has been activated.'
|
||||
case _ if command_lowercase.startswith('score messages'):
|
||||
value = command[14:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'score_messages', value)
|
||||
response = ('Score for messages has been set to {}'
|
||||
.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['score_messages'])
|
||||
case _ if command_lowercase.startswith('score presence'):
|
||||
value = command[14:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'score_presence', value)
|
||||
response = ('Score for presence has been set to {}'
|
||||
.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['score_presence'])
|
||||
case _ if command_lowercase.startswith('scores reset'):
|
||||
jid_bare = command[12:].strip()
|
||||
if jid_bare:
|
||||
del self.settings[room]['scores'][jid_bare]
|
||||
value = self.settings[room]['scores']
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'scores', value)
|
||||
response = 'Score for {} has been reset'.format(jid_bare)
|
||||
else:
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'scores', {})
|
||||
response = 'All scores have been reset'
|
||||
case _ if command_lowercase.startswith('scores'):
|
||||
jid_bare = command[6:].strip()
|
||||
if jid_bare:
|
||||
response = str(self.settings[room]['scores'][jid_bare])
|
||||
else:
|
||||
response = str(self.settings[room]['scores'])
|
||||
case 'start':
|
||||
XmppCommands.update_setting_value(self, room, db_file,
|
||||
'enabled', 1)
|
||||
XmppStatus.send_status_message(self, room)
|
||||
case 'stats':
|
||||
response = XmppCommands.print_statistics(db_file)
|
||||
case 'status off':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'check_status', 0)
|
||||
response = 'Status message check has been deactivated'
|
||||
case 'status on':
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'check_status', 1)
|
||||
response = 'Status message check has been activated'
|
||||
case 'stop':
|
||||
XmppCommands.update_setting_value(self, room, db_file,
|
||||
'enabled', 0)
|
||||
XmppStatus.send_status_message(self, room)
|
||||
case 'support':
|
||||
response = XmppCommands.print_support_jid()
|
||||
await XmppCommands.invite_jid_to_muc(self, room)
|
||||
case _ if command_lowercase.startswith('timer'):
|
||||
value = command[5:].strip()
|
||||
if value:
|
||||
try:
|
||||
value = int(value)
|
||||
XmppCommands.update_setting_value(
|
||||
self, room, db_file, 'timer', value)
|
||||
response = ('Timer value for countdown before '
|
||||
'committing an action has been set to {} '
|
||||
'seconds'.format(value))
|
||||
except:
|
||||
response = 'Enter a numerical value.'
|
||||
else:
|
||||
response = str(self.settings[room]['timer'])
|
||||
case 'version':
|
||||
response = XmppCommands.print_version()
|
||||
case _ if command_lowercase.startswith('xmpp:'):
|
||||
response = await XmppCommands.muc_join(self, command)
|
||||
case _:
|
||||
response = XmppCommands.print_unknown()
|
||||
|
||||
command_time_finish = time.time()
|
||||
command_time_total = command_time_finish - command_time_start
|
||||
command_time_total = round(command_time_total, 3)
|
||||
|
||||
if response: XmppMessage.send_reply(self, message, response)
|
||||
|
||||
if room in self.settings and self.settings[room]['finished']:
|
||||
response_finished = ('Finished. Total time: {}s'
|
||||
.format(command_time_total))
|
||||
XmppMessage.send_reply(self, message, response_finished)
|
440
kaikout/xmpp/client.py
Normal file
440
kaikout/xmpp/client.py
Normal file
|
@ -0,0 +1,440 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import slixmpp
|
||||
from kaikout.about import Documentation
|
||||
from kaikout.database import Toml
|
||||
from kaikout.log import Logger
|
||||
from kaikout.utilities import Config, Log
|
||||
from kaikout.xmpp.bookmark import XmppBookmark
|
||||
from kaikout.xmpp.chat import XmppChat
|
||||
from kaikout.xmpp.commands import XmppCommands
|
||||
from kaikout.xmpp.groupchat import XmppGroupchat
|
||||
from kaikout.xmpp.message import XmppMessage
|
||||
from kaikout.xmpp.moderation import XmppModeration
|
||||
from kaikout.xmpp.muc import XmppMuc
|
||||
from kaikout.xmpp.status import XmppStatus
|
||||
import time
|
||||
|
||||
# time_now = datetime.now()
|
||||
# time_now = time_now.strftime("%H:%M:%S")
|
||||
|
||||
# def print_time():
|
||||
# # return datetime.now().strftime("%H:%M:%S")
|
||||
# now = datetime.now()
|
||||
# current_time = now.strftime("%H:%M:%S")
|
||||
# return current_time
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
class XmppClient(slixmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
KaikOut - A moderation chat bot for Jabber/XMPP.
|
||||
KaikOut is a chat control bot for XMPP groupchats.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, hostname, port, alias):
|
||||
slixmpp.ClientXMPP.__init__(self, jid, password, hostname, port, alias)
|
||||
# Handlers for action messages.
|
||||
self.actions = {}
|
||||
self.action_count = 0
|
||||
# A handler for alias.
|
||||
self.alias = alias
|
||||
# A handler for configuration.
|
||||
self.defaults = Config.get_values('settings.toml', 'defaults')
|
||||
# Handlers for connectivity.
|
||||
self.connection_attempts = 0
|
||||
self.max_connection_attempts = 10
|
||||
self.task_ping_instance = {}
|
||||
self.reconnect_timeout = Config.get_values('accounts.toml', 'xmpp')['settings']['reconnect_timeout']
|
||||
# A handler for operators.
|
||||
self.operators = Config.get_values('accounts.toml', 'xmpp')['operators']
|
||||
# A handler for settings.
|
||||
self.settings = {}
|
||||
# A handler for sessions.
|
||||
self.sessions = {}
|
||||
# A handler for tasks.
|
||||
self.tasks = {}
|
||||
# Register plugins.
|
||||
self.register_plugin('xep_0030') # Service Discovery
|
||||
self.register_plugin('xep_0004') # Data Forms
|
||||
self.register_plugin('xep_0045') # Multi-User Chat
|
||||
self.register_plugin('xep_0048') # Bookmarks
|
||||
self.register_plugin('xep_0060') # Publish-Subscribe
|
||||
self.register_plugin('xep_0050') # Ad-Hoc Commands
|
||||
self.register_plugin('xep_0084') # User Avatar
|
||||
self.register_plugin('xep_0085') # Chat State Notifications
|
||||
self.register_plugin('xep_0115') # Entity Capabilities
|
||||
self.register_plugin('xep_0122') # Data Forms Validation
|
||||
self.register_plugin('xep_0199') # XMPP Ping
|
||||
self.register_plugin('xep_0249') # Direct MUC Invitations
|
||||
self.register_plugin('xep_0369') # Mediated Information eXchange (MIX)
|
||||
self.register_plugin('xep_0437') # Room Activity Indicators
|
||||
self.register_plugin('xep_0444') # Message Reactions
|
||||
# Register events.
|
||||
# self.add_event_handler("chatstate_composing", self.on_chatstate_composing)
|
||||
# self.add_event_handler('connection_failed', self.on_connection_failed)
|
||||
self.add_event_handler("disco_info", self.on_disco_info)
|
||||
self.add_event_handler("groupchat_direct_invite", self.on_groupchat_direct_invite) # XEP_0249
|
||||
self.add_event_handler("groupchat_invite", self.on_groupchat_invite) # XEP_0045
|
||||
self.add_event_handler("message", self.on_message)
|
||||
# self.add_event_handler("reactions", self.on_reactions)
|
||||
# self.add_event_handler("room_activity", self.on_room_activity)
|
||||
# self.add_event_handler("session_resumed", self.on_session_resumed)
|
||||
self.add_event_handler("session_start", self.on_session_start)
|
||||
# Connect and process.
|
||||
self.connect()
|
||||
self.process()
|
||||
|
||||
def muc_online(self, presence):
|
||||
"""
|
||||
Process a presence stanza from a chat room. In this case,
|
||||
presences from users that have just come online are
|
||||
handled by sending a welcome message that includes
|
||||
the user's nickname and role in the room.
|
||||
|
||||
Arguments:
|
||||
presence -- The received presence stanza. See the
|
||||
documentation for the Presence stanza
|
||||
to see how else it may be used.
|
||||
"""
|
||||
if presence['muc']['nick'] != self.alias:
|
||||
self.send_message(mto=presence['from'].bare,
|
||||
mbody="Hello, %s %s" % (presence['muc']['role'],
|
||||
presence['muc']['nick']),
|
||||
mtype='groupchat')
|
||||
|
||||
|
||||
async def on_disco_info(self, DiscoInfo):
|
||||
jid = DiscoInfo['from']
|
||||
await self['xep_0115'].update_caps(jid=jid)
|
||||
# jid_bare = DiscoInfo['from'].bare
|
||||
|
||||
|
||||
# TODO Test
|
||||
async def on_groupchat_invite(self, message):
|
||||
jid_full = str(message['from'])
|
||||
room = message['groupchat_invite']['jid']
|
||||
result = await XmppMuc.join(self, room)
|
||||
if result == 'ban':
|
||||
message_body = '{} is banned from {}'.format(self.alias, room)
|
||||
jid_bare = message['from'].bare
|
||||
# This might not be necessary because JID might not be of the inviter, but rather of the MUC
|
||||
XmppMessage.send(self, jid_bare, message_body, 'chat')
|
||||
logger.warning(message_body)
|
||||
print("on_groupchat_invite")
|
||||
print("BAN BAN BAN BAN BAN")
|
||||
print("on_groupchat_invite")
|
||||
print(jid_full)
|
||||
print(jid_full)
|
||||
print(jid_full)
|
||||
print("on_groupchat_invite")
|
||||
print("BAN BAN BAN BAN BAN")
|
||||
print("on_groupchat_invite")
|
||||
else:
|
||||
await XmppBookmark.add(self, room)
|
||||
message_body = (
|
||||
'Greetings! I am {}, the news anchor.\n'
|
||||
'My job is to bring you the latest news from sources you '
|
||||
'provide me with.\n'
|
||||
'You may always reach me via xmpp:{}?message'
|
||||
.format(self.alias, self.boundjid.bare))
|
||||
XmppMessage.send(self, room, message_body, 'groupchat')
|
||||
XmppStatus.send_status_message(self, room)
|
||||
|
||||
|
||||
async def on_groupchat_direct_invite(self, message):
|
||||
room = message['groupchat_invite']['jid']
|
||||
result = await XmppMuc.join(self, room)
|
||||
if result == 'ban':
|
||||
message_body = '{} is banned from {}'.format(self.alias, room)
|
||||
jid_bare = message['from'].bare
|
||||
XmppMessage.send(self, jid_bare, message_body, 'chat')
|
||||
logger.warning(message_body)
|
||||
else:
|
||||
await XmppBookmark.add(self, room)
|
||||
message_body = ('/me moderation chat bot. Jabber ID: xmpp:{}?message'
|
||||
.format(self.boundjid.bare))
|
||||
XmppMessage.send(self, room, message_body, 'groupchat')
|
||||
XmppStatus.send_status_message(self, room)
|
||||
|
||||
|
||||
async def on_message(self, message):
|
||||
await XmppChat.process_message(self, message)
|
||||
# if message['type'] == 'groupchat':
|
||||
# if 'mucroom' in message.keys():
|
||||
if message['mucroom']:
|
||||
alias = message['mucnick']
|
||||
message_body = message['body']
|
||||
identifier = message['id']
|
||||
lang = message['lang']
|
||||
room = message['mucroom']
|
||||
timestamp_iso = datetime.now().isoformat()
|
||||
fields = ['message', timestamp_iso, alias, message_body, lang, identifier]
|
||||
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
|
||||
Log.csv(filename, fields)
|
||||
db_file = Toml.instantiate(self, room)
|
||||
timestamp = time.time()
|
||||
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
||||
XmppCommands.update_last_activity(self, room, jid_bare, db_file, timestamp)
|
||||
# Toml.load_jid_settings(self, room)
|
||||
# await XmppChat.process_message(self, message)
|
||||
if (XmppMuc.is_moderator(self, room, self.alias) and
|
||||
self.settings[room]['enabled'] and
|
||||
alias != self.alias):
|
||||
identifier = message['id']
|
||||
fields = [alias, message_body, identifier, timestamp]
|
||||
Log.toml(self, room, fields, 'message')
|
||||
# Check for message
|
||||
if self.settings[room]['check_message']:
|
||||
reason = XmppModeration.moderate_message(self, message_body, room)
|
||||
if reason:
|
||||
score_max = self.settings[room]['score_messages']
|
||||
score = XmppCommands.raise_score(self, room, alias, db_file, reason)
|
||||
if score > score_max:
|
||||
if self.settings[room]['action']:
|
||||
jid_bare = await XmppCommands.outcast(self, room, alias, reason)
|
||||
# admins = await XmppMuc.get_affiliation(self, room, 'admin')
|
||||
# owners = await XmppMuc.get_affiliation(self, room, 'owner')
|
||||
moderators = await XmppMuc.get_role(self, room, 'moderator')
|
||||
# Report to the moderators.
|
||||
message_to_moderators = (
|
||||
'Participant {} ({}) has been banned from '
|
||||
'groupchat {}.'.format(alias, jid_bare, room))
|
||||
for alias in moderators:
|
||||
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
||||
XmppMessage.send(self, jid_full, message_to_moderators, 'chat')
|
||||
# Inform the subject
|
||||
message_to_participant = (
|
||||
'You were banned from groupchat {}. Please '
|
||||
'contact the moderators if you think this was '
|
||||
'a mistake.'.format(room))
|
||||
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
||||
else:
|
||||
await XmppCommands.devoice(self, room, alias, reason)
|
||||
# Check for inactivity
|
||||
if self.settings[room]['check_inactivity']:
|
||||
roster_muc = XmppMuc.get_roster(self, room)
|
||||
for alias in roster_muc:
|
||||
if alias != self.alias:
|
||||
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
||||
result, span = XmppModeration.moderate_last_activity(
|
||||
self, room, jid_bare, timestamp)
|
||||
if result:
|
||||
message_to_participant = None
|
||||
if 'inactivity_notice' not in self.settings[room]:
|
||||
self.settings[room]['inactivity_notice'] = []
|
||||
noticed_jids = self.settings[room]['inactivity_notice']
|
||||
if result == 'Inactivity':
|
||||
if jid_bare in noticed_jids: noticed_jids.remove(jid_bare)
|
||||
await XmppCommands.kick(self, room, alias, reason)
|
||||
message_to_participant = (
|
||||
'You were expelled from groupchat {} due to '
|
||||
'being inactive for {} days.'.format(room, span))
|
||||
elif result == 'Warning' and jid_bare not in noticed_jids:
|
||||
noticed_jids.append(jid_bare)
|
||||
time_left = int(span)
|
||||
if not time_left: time_left = 'an'
|
||||
message_to_participant = (
|
||||
'This is an inactivity-warning.\n'
|
||||
'You are expected to be expelled from '
|
||||
'groupchat {} within {} hour time.'
|
||||
.format(room, int(span) or 'an'))
|
||||
Toml.update_jid_settings(
|
||||
self, room, db_file, 'inactivity_notice', noticed_jids)
|
||||
if message_to_participant:
|
||||
XmppMessage.send(
|
||||
self, jid_bare, message_to_participant, 'chat')
|
||||
|
||||
|
||||
async def on_muc_presence(self, presence):
|
||||
alias = presence['muc']['nick']
|
||||
identifier = presence['id']
|
||||
jid_full = presence['muc']['jid']
|
||||
jid_bare = jid_full.bare
|
||||
lang = presence['lang']
|
||||
presence_body = presence['status']
|
||||
room = presence['muc']['room']
|
||||
timestamp_iso = datetime.now().isoformat()
|
||||
fields = ['presence', timestamp_iso, alias, presence_body, lang, identifier]
|
||||
filename = datetime.today().strftime('%Y-%m-%d') + '_' + room
|
||||
# if identifier and presence_body:
|
||||
Log.csv(filename, fields)
|
||||
db_file = Toml.instantiate(self, room)
|
||||
if (XmppMuc.is_moderator(self, room, self.alias) and
|
||||
self.settings[room]['enabled'] and
|
||||
alias != self.alias):
|
||||
# import time # FIXME Why is this required if it is already stated at the top?
|
||||
timestamp = time.time()
|
||||
fields = [alias, presence_body, identifier, timestamp]
|
||||
Log.toml(self, room, fields, 'presence')
|
||||
# Check for status message
|
||||
if self.settings[room]['check_status']:
|
||||
reason, timer = XmppModeration.moderate_status_message(self, presence_body, room)
|
||||
if reason and timer and not (room in self.tasks and
|
||||
jid_bare in self.tasks[room] and
|
||||
'countdown' in self.tasks[room][jid_bare]):
|
||||
print('reason and timer for jid: ' + jid_bare + ' at room ' + room)
|
||||
score_max = self.settings[room]['score_presence']
|
||||
score = XmppCommands.raise_score(self, room, alias, db_file, reason)
|
||||
if room not in self.tasks:
|
||||
self.tasks[room] = {}
|
||||
if jid_bare not in self.tasks[room]:
|
||||
self.tasks[room][jid_bare] = {}
|
||||
# if 'countdown' in self.tasks[room][jid_bare]:
|
||||
# self.tasks[room][jid_bare]['countdown'].cancel()
|
||||
if 'countdown' not in self.tasks[room][jid_bare]:
|
||||
seconds = self.settings[room]['timer']
|
||||
self.tasks[room][jid_bare]['countdown'] = asyncio.create_task(
|
||||
XmppCommands.countdown(self, seconds, room, alias, reason))
|
||||
message_to_participant = (
|
||||
'Your status message "{}" violates policies of groupchat '
|
||||
'{}.\n'
|
||||
'You have {} seconds to change your status message, in '
|
||||
'order to avoid consequent actions.'
|
||||
.format(presence_body, room, seconds))
|
||||
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
||||
elif reason and not (room in self.tasks
|
||||
and jid_bare in self.tasks[room] and
|
||||
'countdown' in self.tasks[room][jid_bare]):
|
||||
print('reason for jid: ' + jid_bare + ' at room ' + room)
|
||||
score_max = self.settings[room]['score_presence']
|
||||
score = XmppCommands.raise_score(self, room, alias, db_file, reason)
|
||||
if score > score_max:
|
||||
if self.settings[room]['action']:
|
||||
jid_bare = await XmppCommands.outcast(self, room, alias, reason)
|
||||
# admins = await XmppMuc.get_affiliation(self, room, 'admin')
|
||||
# owners = await XmppMuc.get_affiliation(self, room, 'owner')
|
||||
moderators = await XmppMuc.get_role(self, room, 'moderator')
|
||||
# Report to the moderators.
|
||||
message_to_moderators = (
|
||||
'Participant {} ({}) has been banned from '
|
||||
'groupchat {}.'.format(alias, jid_bare, room))
|
||||
for alias in moderators:
|
||||
# jid_full = presence['muc']['jid']
|
||||
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
||||
XmppMessage.send(self, jid_full, message_to_moderators, 'chat')
|
||||
# Inform the subject.
|
||||
message_to_participant = (
|
||||
'You were banned from groupchat {}. Please '
|
||||
'contact the moderators if you think this was a '
|
||||
'mistake.'.format(room))
|
||||
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
||||
else:
|
||||
await XmppCommands.devoice(self, room, alias, reason)
|
||||
elif (room in self.tasks and
|
||||
jid_bare in self.tasks[room] and
|
||||
'countdown' in self.tasks[room][jid_bare]) and not reason:
|
||||
print('cancel task for jid: ' + jid_bare + ' at room ' + room)
|
||||
print(self.tasks[room][jid_bare]['countdown'])
|
||||
if self.tasks[room][jid_bare]['countdown'].cancel():
|
||||
print(self.tasks[room][jid_bare]['countdown'])
|
||||
message_to_participant = 'Thank you for your cooperation.'
|
||||
XmppMessage.send(self, jid_bare, message_to_participant, 'chat')
|
||||
del self.tasks[room][jid_bare]['countdown']
|
||||
# Check for inactivity
|
||||
if self.settings[room]['check_inactivity']:
|
||||
roster_muc = XmppMuc.get_roster(self, room)
|
||||
for alias in roster_muc:
|
||||
if alias != self.alias:
|
||||
jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
||||
result, span = XmppModeration.moderate_last_activity(
|
||||
self, room, jid_bare, timestamp)
|
||||
if result:
|
||||
message_to_participant = None
|
||||
if 'inactivity_notice' not in self.settings[room]:
|
||||
self.settings[room]['inactivity_notice'] = []
|
||||
noticed_jids = self.settings[room]['inactivity_notice']
|
||||
if result == 'Inactivity':
|
||||
if jid_bare in noticed_jids: noticed_jids.remove(jid_bare)
|
||||
await XmppCommands.kick(self, room, alias, reason)
|
||||
message_to_participant = (
|
||||
'You were expelled from groupchat {} due to '
|
||||
'being inactive for {} days.'.format(room, span))
|
||||
elif result == 'Warning' and jid_bare not in noticed_jids:
|
||||
noticed_jids.append(jid_bare)
|
||||
time_left = int(span)
|
||||
if not time_left: time_left = 'an'
|
||||
message_to_participant = (
|
||||
'This is an inactivity-warning.\n'
|
||||
'You are expected to be expelled from '
|
||||
'groupchat {} within {} hour time.'
|
||||
.format(room, int(span) or 'an'))
|
||||
Toml.update_jid_settings(
|
||||
self, room, db_file, 'inactivity_notice', noticed_jids)
|
||||
if message_to_participant:
|
||||
XmppMessage.send(
|
||||
self, jid_bare, message_to_participant, 'chat')
|
||||
|
||||
|
||||
async def on_muc_self_presence(self, presence):
|
||||
actor = presence['muc']['item']['actor']['nick']
|
||||
alias = presence['muc']['nick']
|
||||
room = presence['muc']['room']
|
||||
if actor and alias == self.alias: XmppStatus.send_status_message(self, room)
|
||||
|
||||
|
||||
async def on_room_activity(self, presence):
|
||||
print('on_room_activity')
|
||||
print(presence)
|
||||
print('testing mix core')
|
||||
breakpoint()
|
||||
|
||||
|
||||
async def on_session_start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
# self.command_list()
|
||||
# await self.get_roster()
|
||||
await self['xep_0115'].update_caps()
|
||||
bookmarks = await XmppBookmark.get_bookmarks(self)
|
||||
print(bookmarks)
|
||||
rooms = await XmppGroupchat.autojoin(self, bookmarks)
|
||||
# See also get_joined_rooms of slixmpp.plugins.xep_0045
|
||||
for room in rooms:
|
||||
XmppStatus.send_status_message(self, room)
|
||||
self.add_event_handler("muc::%s::presence" % room, self.on_muc_presence)
|
||||
self.add_event_handler("muc::%s::self-presence" % room, self.on_muc_self_presence)
|
||||
await asyncio.sleep(5)
|
||||
self.send_presence(
|
||||
pshow='available',
|
||||
pstatus='👁️ KaikOut Moderation Chat Bot')
|
||||
|
||||
|
||||
def command_list(self):
|
||||
self['xep_0050'].add_command(node='search',
|
||||
name='🔍️ Search',
|
||||
handler=self._handle_search)
|
||||
self['xep_0050'].add_command(node='settings',
|
||||
name='⚙️ Settings',
|
||||
handler=self._handle_settings)
|
||||
self['xep_0050'].add_command(node='about',
|
||||
name='📜️ About',
|
||||
handler=self._handle_about)
|
||||
|
||||
|
||||
def _handle_cancel(self, payload, session):
|
||||
text_note = ('Operation has been cancelled.'
|
||||
'\n\n'
|
||||
'No action was taken.')
|
||||
session['notes'] = [['info', text_note]]
|
||||
return session
|
||||
|
||||
|
||||
def _handle_about(self, iq, session):
|
||||
text_note = Documentation.about()
|
||||
session['notes'] = [['info', text_note]]
|
||||
return session
|
431
kaikout/xmpp/commands.py
Normal file
431
kaikout/xmpp/commands.py
Normal file
|
@ -0,0 +1,431 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
import kaikout.config as config
|
||||
from kaikout.config import Config
|
||||
from kaikout.log import Logger
|
||||
from kaikout.database import Toml
|
||||
from kaikout.utilities import Documentation, Url
|
||||
from kaikout.version import __version__
|
||||
from kaikout.xmpp.bookmark import XmppBookmark
|
||||
from kaikout.xmpp.muc import XmppMuc
|
||||
from kaikout.xmpp.status import XmppStatus
|
||||
from kaikout.xmpp.utilities import XmppUtilities
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except:
|
||||
import tomli as tomllib
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
# for task in main_task:
|
||||
# task.cancel()
|
||||
|
||||
# Deprecated in favour of event "presence_available"
|
||||
# if not main_task:
|
||||
# await select_file()
|
||||
|
||||
class XmppCommands:
|
||||
|
||||
|
||||
async def clear_filter(self, room, db_file, key):
|
||||
value = []
|
||||
self.settings[room][key] = value
|
||||
Toml.update_jid_settings(self, room, db_file, key, value)
|
||||
message = 'Filter {} has been purged.'.format(key)
|
||||
return message
|
||||
|
||||
|
||||
async def devoice(self, room, alias, reason):
|
||||
status_message = '🚫 Committing an action (devoice) against participant {}'.format(alias)
|
||||
self.action_count += 1
|
||||
task_number = self.action_count
|
||||
if room not in self.actions: self.actions[room] = {}
|
||||
self.actions[room][task_number] = status_message
|
||||
XmppStatus.send_status_message(self, room)
|
||||
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
||||
if jid_full:
|
||||
jid_bare = jid_full.split('/')[0]
|
||||
await XmppMuc.set_role(self, room, alias, 'visitor', reason)
|
||||
await asyncio.sleep(5)
|
||||
del self.actions[room][task_number]
|
||||
XmppStatus.send_status_message(self, room)
|
||||
return jid_bare
|
||||
|
||||
|
||||
async def countdown(self, time, room, alias, reason):
|
||||
while time > 1:
|
||||
time -= 1
|
||||
print([time, room, alias], end='\r')
|
||||
await asyncio.sleep(1)
|
||||
await XmppCommands.devoice(self, room, alias, reason)
|
||||
|
||||
|
||||
|
||||
def fetch_gemini():
|
||||
message = 'Gemini and Gopher are not supported yet.'
|
||||
return message
|
||||
|
||||
|
||||
def get_interval(self, jid_bare):
|
||||
result = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'interval')
|
||||
message = str(result)
|
||||
return message
|
||||
|
||||
|
||||
async def bookmark_add(self, muc_jid):
|
||||
await XmppBookmark.add(self, jid=muc_jid)
|
||||
message = ('Groupchat {} has been added to bookmarks.'
|
||||
.format(muc_jid))
|
||||
return message
|
||||
|
||||
|
||||
async def bookmark_del(self, muc_jid):
|
||||
await XmppBookmark.remove(self, muc_jid)
|
||||
message = ('Groupchat {} has been removed from bookmarks.'
|
||||
.format(muc_jid))
|
||||
return message
|
||||
|
||||
async def invite_jid_to_muc(self, jid_bare):
|
||||
muc_jid = 'slixfeed@chat.woodpeckersnest.space'
|
||||
if await XmppUtilities.get_chat_type(self, jid_bare) == 'chat':
|
||||
self.plugin['xep_0045'].invite(muc_jid, jid_bare)
|
||||
|
||||
|
||||
async def kick(self, room, alias, reason):
|
||||
status_message = '🚫 Committing an action (kick) against participant {}'.format(alias)
|
||||
self.action_count += 1
|
||||
task_number = self.action_count
|
||||
if room not in self.actions: self.actions[room] = {}
|
||||
self.actions[room][task_number] = status_message
|
||||
XmppStatus.send_status_message(self, room)
|
||||
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
||||
if jid_full:
|
||||
jid_bare = jid_full.split('/')[0]
|
||||
await XmppMuc.set_affiliation(self, room, 'none', jid_bare, reason)
|
||||
await asyncio.sleep(5)
|
||||
del self.actions[room][task_number]
|
||||
XmppStatus.send_status_message(self, room)
|
||||
return jid_bare
|
||||
|
||||
|
||||
async def muc_join(self, command):
|
||||
if command:
|
||||
muc_jid = Url.check_xmpp_uri(command)
|
||||
if muc_jid:
|
||||
# TODO probe JID and confirm it's a groupchat
|
||||
result = await XmppMuc.join(self, muc_jid)
|
||||
# await XmppBookmark.add(self, jid=muc_jid)
|
||||
if result == 'ban':
|
||||
message = '{} is banned from {}'.format(self.alias, muc_jid)
|
||||
else:
|
||||
await XmppBookmark.add(self, muc_jid)
|
||||
message = 'Joined groupchat {}'.format(muc_jid)
|
||||
else:
|
||||
message = '> {}\nGroupchat JID appears to be invalid.'.format(muc_jid)
|
||||
else:
|
||||
message = '> {}\nGroupchat JID is missing.'
|
||||
return message
|
||||
|
||||
|
||||
async def muc_leave(self, room):
|
||||
XmppMuc.leave(self, room)
|
||||
await XmppBookmark.remove(self, room)
|
||||
|
||||
|
||||
async def outcast(self, room, alias, reason):
|
||||
status_message = '🚫 Committing an action (ban) against participant {}'.format(alias)
|
||||
self.action_count += 1
|
||||
task_number = self.action_count
|
||||
if room not in self.actions: self.actions[room] = {}
|
||||
self.actions[room][task_number] = status_message
|
||||
XmppStatus.send_status_message(self, room)
|
||||
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
||||
if jid_full:
|
||||
jid_bare = jid_full.split('/')[0]
|
||||
await XmppMuc.set_affiliation(self, room, 'outcast', jid_bare, reason)
|
||||
# else:
|
||||
# # Could "alias" ever be relevant?
|
||||
# # Being a moderator is essential to be able to outcast.
|
||||
# # JIDs are visible to moderators.
|
||||
# await XmppMuc.set_affiliation(self, room, 'outcast', alias, reason)
|
||||
await asyncio.sleep(5)
|
||||
del self.actions[room][task_number]
|
||||
XmppStatus.send_status_message(self, room)
|
||||
return jid_bare
|
||||
|
||||
|
||||
async def print_bookmarks(self):
|
||||
conferences = await XmppBookmark.get_bookmarks(self)
|
||||
message = '\nList of groupchats:\n\n```\n'
|
||||
for conference in conferences:
|
||||
message += ('Name: {}\n'
|
||||
'Room: {}\n'
|
||||
'\n'
|
||||
.format(conference['name'], conference['jid']))
|
||||
message += ('```\nTotal of {} groupchats.\n'.format(len(conferences)))
|
||||
return message
|
||||
|
||||
|
||||
def print_help():
|
||||
result = Documentation.manual('commands.toml')
|
||||
message = '\n'.join(result)
|
||||
return message
|
||||
|
||||
|
||||
def print_help_list():
|
||||
command_list = Documentation.manual('commands.toml', section='all')
|
||||
message = ('Complete list of commands:\n'
|
||||
'```\n{}\n```'.format(command_list))
|
||||
return message
|
||||
|
||||
|
||||
def print_help_specific(command_root, command_name):
|
||||
command_list = Documentation.manual('commands.toml',
|
||||
section=command_root,
|
||||
command=command_name)
|
||||
if command_list:
|
||||
command_list = ''.join(command_list)
|
||||
message = (command_list)
|
||||
else:
|
||||
message = 'KeyError for {} {}'.format(command_root, command_name)
|
||||
return message
|
||||
|
||||
|
||||
def print_help_key(command):
|
||||
command_list = Documentation.manual('commands.toml', command)
|
||||
if command_list:
|
||||
command_list = ' '.join(command_list)
|
||||
message = ('Available command `{}` keys:\n'
|
||||
'```\n{}\n```\n'
|
||||
'Usage: `help {} <command>`'
|
||||
.format(command, command_list, command))
|
||||
else:
|
||||
message = 'KeyError for {}'.format(command)
|
||||
return message
|
||||
|
||||
|
||||
def print_info_list():
|
||||
config_dir = config.get_default_config_directory()
|
||||
with open(config_dir + '/' + 'information.toml', mode="rb") as information:
|
||||
result = tomllib.load(information)
|
||||
message = '\n'.join(result)
|
||||
return message
|
||||
|
||||
|
||||
def print_info_specific(entry):
|
||||
config_dir = config.get_default_config_directory()
|
||||
with open(config_dir + '/' + 'information.toml', mode="rb") as information:
|
||||
entries = tomllib.load(information)
|
||||
if entry in entries:
|
||||
# command_list = '\n'.join(command_list)
|
||||
message = (entries[entry]['info'])
|
||||
else:
|
||||
message = 'KeyError for {}'.format(entry)
|
||||
return message
|
||||
|
||||
|
||||
def print_options(self, room):
|
||||
message = ''
|
||||
for key in self.settings[room]:
|
||||
val = self.settings[room][key]
|
||||
steps = 18 - len(key)
|
||||
pulse = ''
|
||||
for step in range(steps):
|
||||
pulse += ' '
|
||||
message += '\n' + key + pulse + ' = ' + str(val)
|
||||
return message
|
||||
|
||||
|
||||
# """You have {} unread news items out of {} from {} news sources.
|
||||
# """.format(unread_entries, entries, feeds)
|
||||
def print_statistics(db_file):
|
||||
"""
|
||||
Print statistics.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Path to database file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
msg : str
|
||||
Statistics as message.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: db_file: {}'
|
||||
.format(function_name, db_file))
|
||||
entries_unread = sqlite.get_number_of_entries_unread(db_file)
|
||||
entries = sqlite.get_number_of_items(db_file, 'entries_properties')
|
||||
feeds_active = sqlite.get_number_of_feeds_active(db_file)
|
||||
feeds_all = sqlite.get_number_of_items(db_file, 'feeds_properties')
|
||||
message = ("Statistics:"
|
||||
"\n"
|
||||
"```"
|
||||
"\n"
|
||||
"News items : {}/{}\n"
|
||||
"News sources : {}/{}\n"
|
||||
"```").format(entries_unread,
|
||||
entries,
|
||||
feeds_active,
|
||||
feeds_all)
|
||||
return message
|
||||
|
||||
|
||||
def print_support_jid():
|
||||
muc_jid = 'slixfeed@chat.woodpeckersnest.space'
|
||||
message = 'Join xmpp:{}?join'.format(muc_jid)
|
||||
return message
|
||||
|
||||
|
||||
def print_unknown():
|
||||
message = 'An unknown command. Type "help" for a list of commands.'
|
||||
return message
|
||||
|
||||
|
||||
def print_version():
|
||||
message = __version__
|
||||
return message
|
||||
|
||||
|
||||
def raise_score(self, room, alias, db_file, reason):
|
||||
"""
|
||||
Raise score by one.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Database filename.
|
||||
alias : str
|
||||
Alias.
|
||||
reason : str
|
||||
Reason.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result.
|
||||
|
||||
"""
|
||||
status_message = '✒️ Writing a score against {} for {}'.format(alias, reason)
|
||||
self.action_count += 1
|
||||
task_number = self.action_count
|
||||
if room not in self.actions: self.actions[room] = {}
|
||||
self.actions[room][task_number] = status_message
|
||||
XmppStatus.send_status_message(self, room)
|
||||
scores = self.settings[room]['scores'] if 'scores' in self.settings[room] else {}
|
||||
jid_full = XmppMuc.get_full_jid(self, room, alias)
|
||||
if jid_full:
|
||||
jid_bare = jid_full.split('/')[0]
|
||||
scores[jid_bare] = scores[jid_bare] + 1 if jid_bare in scores else 1
|
||||
Toml.update_jid_settings(self, room, db_file, 'scores', scores)
|
||||
time.sleep(5)
|
||||
del self.actions[room][task_number]
|
||||
XmppStatus.send_status_message(self, room)
|
||||
result = scores[jid_bare] if jid_full and jid_bare else 0
|
||||
return result
|
||||
|
||||
|
||||
def update_last_activity(self, room, jid_bare, db_file, timestamp):
|
||||
"""
|
||||
Update last message activity.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Database filename.
|
||||
jid_bare : str
|
||||
Jabber ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result.
|
||||
|
||||
"""
|
||||
activity = self.settings[room]['last_activity'] if 'last_activity' in self.settings[room] else {}
|
||||
activity[jid_bare] = timestamp
|
||||
Toml.update_jid_settings(self, room, db_file, 'last_activity', activity)
|
||||
|
||||
|
||||
async def restore_default(self, room, db_file, key=None):
|
||||
if key:
|
||||
value = self.defaults[key]
|
||||
self.settings[room][key] = value
|
||||
# data_dir = Toml.get_default_data_directory()
|
||||
# db_file = Toml.get_data_file(data_dir, jid_bare)
|
||||
Toml.update_jid_settings(self, room, db_file, key, value)
|
||||
message = ('Setting {} has been restored to default value.'
|
||||
.format(key))
|
||||
else:
|
||||
self.settings = self.defaults
|
||||
data_dir = Toml.get_default_data_directory()
|
||||
db_file = Toml.get_data_file(data_dir, room)
|
||||
Toml.create_settings_file(self, db_file)
|
||||
message = 'Default settings have been restored.'
|
||||
return message
|
||||
|
||||
|
||||
def set_filter(self, room, db_file, keywords, filter, axis):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
db_file : str
|
||||
Database filename.
|
||||
keywords : str
|
||||
keyword (word or phrase).
|
||||
filter : str
|
||||
'allow' or 'deny'.
|
||||
axis : boolean
|
||||
True for + (plus) and False for - (minus).
|
||||
|
||||
Returns
|
||||
-------
|
||||
None.
|
||||
|
||||
"""
|
||||
keyword_list = self.settings[room][filter] if filter in self.settings[room] else []
|
||||
new_keywords = keywords.split(',')
|
||||
processed_keywords = []
|
||||
if axis:
|
||||
for keyword in new_keywords:
|
||||
if keyword and keyword not in keyword_list:
|
||||
keyword_trim = keyword.strip()
|
||||
keyword_list.append(keyword_trim)
|
||||
processed_keywords.append(keyword_trim)
|
||||
else:
|
||||
for keyword in new_keywords:
|
||||
if keyword and keyword in keyword_list:
|
||||
keyword_trim = keyword.strip()
|
||||
keyword_list.remove(keyword_trim)
|
||||
processed_keywords.append(keyword_trim)
|
||||
Toml.update_jid_settings(self, room, db_file, filter, keyword_list)
|
||||
processed_keywords.sort()
|
||||
message = 'Keywords "{}" have been added to list "{}".'.format(', '.join(processed_keywords), filter)
|
||||
return message
|
||||
|
||||
|
||||
async def set_interval(self, db_file, jid_bare, val):
|
||||
try:
|
||||
val_new = int(val)
|
||||
val_old = Config.get_setting_value(
|
||||
self.settings, jid_bare, 'interval')
|
||||
await Config.set_setting_value(
|
||||
self.settings, jid_bare, db_file, 'interval', val_new)
|
||||
message = ('Updates will be sent every {} minutes '
|
||||
'(was: {}).'.format(val_new, val_old))
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
message = ('No action has been taken. Enter a numeric value only.')
|
||||
return message
|
||||
|
||||
|
||||
def update_setting_value(self, room, db_file, key, value):
|
||||
Toml.update_jid_settings(self, room, db_file, key, value)
|
58
kaikout/xmpp/groupchat.py
Normal file
58
kaikout/xmpp/groupchat.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
TODO
|
||||
|
||||
1) Send message to inviter that bot has joined to groupchat.
|
||||
|
||||
2) If groupchat requires captcha, send the consequent message.
|
||||
|
||||
3) If groupchat error is received, send that error message to inviter.
|
||||
|
||||
FIXME
|
||||
|
||||
1) Save name of groupchat instead of jid as name
|
||||
|
||||
"""
|
||||
from kaikout.xmpp.bookmark import XmppBookmark
|
||||
from kaikout.xmpp.muc import XmppMuc
|
||||
from kaikout.xmpp.status import XmppStatus
|
||||
from kaikout.log import Logger, Message
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class XmppGroupchat:
|
||||
|
||||
async def autojoin(self, bookmarks):
|
||||
mucs_join_success = []
|
||||
for bookmark in bookmarks:
|
||||
if bookmark["jid"] and bookmark["autojoin"]:
|
||||
if not bookmark["nick"]:
|
||||
bookmark["nick"] = self.alias
|
||||
logger.error('Alias (i.e. Nicknname) is missing for '
|
||||
'bookmark {}'.format(bookmark['name']))
|
||||
alias = bookmark["nick"]
|
||||
room = bookmark["jid"]
|
||||
Message.printer('Joining to MUC {} ...'.format(room))
|
||||
result = await XmppMuc.join(self, room, alias)
|
||||
if result == 'ban':
|
||||
await XmppBookmark.remove(self, room)
|
||||
logger.warning('{} is banned from {}'.format(self.alias, room))
|
||||
logger.warning('Groupchat {} has been removed from bookmarks'
|
||||
.format(room))
|
||||
else:
|
||||
mucs_join_success.append(room)
|
||||
logger.info('Autojoin groupchat\n'
|
||||
'Name : {}\n'
|
||||
'JID : {}\n'
|
||||
'Alias : {}\n'
|
||||
.format(bookmark["name"],
|
||||
bookmark["jid"],
|
||||
bookmark["nick"]))
|
||||
elif not bookmark["jid"]:
|
||||
logger.error('JID is missing for bookmark {}'
|
||||
.format(bookmark['name']))
|
||||
return mucs_join_success
|
80
kaikout/xmpp/message.py
Normal file
80
kaikout/xmpp/message.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from kaikout.log import Logger
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
"""
|
||||
|
||||
NOTE
|
||||
|
||||
See XEP-0367: Message Attaching
|
||||
|
||||
"""
|
||||
|
||||
class XmppMessage:
|
||||
|
||||
|
||||
# def process():
|
||||
|
||||
|
||||
def send(self, jid, message_body, chat_type):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
self.send_message(mto=jid,
|
||||
mfrom=jid_from,
|
||||
mbody=message_body,
|
||||
mtype=chat_type)
|
||||
|
||||
|
||||
def send_headline(self, jid, message_subject, message_body, chat_type):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
self.send_message(mto=jid,
|
||||
mfrom=jid_from,
|
||||
# mtype='headline',
|
||||
msubject=message_subject,
|
||||
mbody=message_body,
|
||||
mtype=chat_type,
|
||||
mnick=self.alias)
|
||||
|
||||
|
||||
# NOTE We might want to add more characters
|
||||
# def escape_to_xml(raw_string):
|
||||
# escape_map = {
|
||||
# '"' : '"',
|
||||
# "'" : '''
|
||||
# }
|
||||
# return saxutils.escape(raw_string, escape_map)
|
||||
def send_oob(self, jid, url, chat_type):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
url = saxutils.escape(url)
|
||||
# try:
|
||||
html = (
|
||||
f'<body xmlns="http://www.w3.org/1999/xhtml">'
|
||||
f'<a href="{url}">{url}</a></body>')
|
||||
message = self.make_message(mto=jid,
|
||||
mfrom=jid_from,
|
||||
mbody=url,
|
||||
mhtml=html,
|
||||
mtype=chat_type)
|
||||
message['oob']['url'] = url
|
||||
message.send()
|
||||
# except:
|
||||
# logging.error('ERROR!')
|
||||
# logging.error(jid, url, chat_type, html)
|
||||
|
||||
|
||||
# FIXME Solve this function
|
||||
def send_oob_reply_message(message, url, response):
|
||||
reply = message.reply(response)
|
||||
reply['oob']['url'] = url
|
||||
reply.send()
|
||||
|
||||
|
||||
# def send_reply(self, message, message_body):
|
||||
# message.reply(message_body).send()
|
||||
|
||||
|
||||
def send_reply(self, message, response):
|
||||
message.reply(response).send()
|
124
kaikout/xmpp/moderation.py
Normal file
124
kaikout/xmpp/moderation.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from kaikout.database import Toml
|
||||
from kaikout.log import Logger
|
||||
from kaikout.xmpp.commands import XmppCommands
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
class XmppModeration:
|
||||
|
||||
|
||||
def moderate_last_activity(self, room, jid_bare, timestamp):
|
||||
result = None
|
||||
if 'last_activity' not in self.settings[room]:
|
||||
self.settings[room]['last_activity'] = {}
|
||||
if jid_bare not in self.settings[room]['last_activity']:
|
||||
# self.settings[room]['last_activity'][jid_bare] = timestamp
|
||||
# last_activity_for_jid = self.settings[room]['last_activity'][jid_bare]
|
||||
db_file = Toml.instantiate(self, room)
|
||||
# Toml.update_jid_settings(self, room, db_file, 'last_activity', last_activity_for_jid)
|
||||
XmppCommands.update_last_activity(self, room, jid_bare, db_file, timestamp)
|
||||
else:
|
||||
jid_last_activity = self.settings[room]['last_activity'][jid_bare]
|
||||
span_inactivity = self.settings[room]['inactivity_span']
|
||||
span_inactivity_in_seconds = 60*60*24*span_inactivity
|
||||
if timestamp - jid_last_activity > span_inactivity_in_seconds:
|
||||
result = 'Inactivity'
|
||||
return result, span_inactivity
|
||||
span_inactivity_warn = self.settings[room]['inactivity_warn']
|
||||
span_inactivity_warn_in_seconds = 60*span_inactivity_warn
|
||||
if timestamp - jid_last_activity > span_inactivity_in_seconds - span_inactivity_warn_in_seconds:
|
||||
time_left = span_inactivity_in_seconds - (timestamp - jid_last_activity)
|
||||
time_left_in_seconds = time_left/60/60
|
||||
result = 'Warning'
|
||||
return result, time_left_in_seconds
|
||||
if not result:
|
||||
return None, None
|
||||
|
||||
|
||||
def moderate_message(self, message_body, room):
|
||||
# Process a message for faults and unsolicitations.
|
||||
message_body_lower = message_body.lower()
|
||||
message_body_lower_split = message_body_lower.split()
|
||||
|
||||
# Count instances.
|
||||
message_body_lower_split_sort = list(set(message_body_lower_split))
|
||||
for i in message_body_lower_split_sort:
|
||||
count = message_body_lower_split.count(i)
|
||||
count_max = self.settings[room]['count']
|
||||
if count > count_max:
|
||||
reason = 'Spam (Repetition)'
|
||||
return reason
|
||||
|
||||
# Check for denied phrases or words.
|
||||
blocklist = self.settings[room]['deny']
|
||||
for i in blocklist:
|
||||
if i in message_body_lower:
|
||||
# jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
||||
reason = 'Spam (Blocklist)'
|
||||
return reason
|
||||
|
||||
# Check for frequency.
|
||||
message_activity = self.settings[room]['activity_message']
|
||||
length = len(message_activity)
|
||||
message_frequency = self.settings[room]['frequency_messages']
|
||||
if (message_activity[length-1]['timestamp'] - message_activity[length-2]['timestamp'] < message_frequency and
|
||||
message_activity[length-1]['alias'] == message_activity[length-2]['alias'] and
|
||||
message_activity[length-1]['id'] != message_activity[length-2]['id']):
|
||||
reason = 'Spam (Flooding)'
|
||||
return reason
|
||||
|
||||
# message_activity = self.settings[room]['activity_message']
|
||||
# count = {}
|
||||
# for i in message_activity:
|
||||
# message_activity.pop()
|
||||
# for j in message_activity:
|
||||
# if (i['timestamp'] - j['timestamp'] < 3 and
|
||||
# i['alias'] == j['alias'] and
|
||||
# i['id'] != j['id']):
|
||||
# if 'alias' in count:
|
||||
# count['alias'] += 1
|
||||
# else:
|
||||
# count['alias'] = 0
|
||||
|
||||
|
||||
def moderate_status_message(self, status_message, room):
|
||||
# Process a status message for faults and unsolicitations.
|
||||
status_message_lower = status_message.lower()
|
||||
status_message_lower_split = status_message_lower.split()
|
||||
reason = None
|
||||
|
||||
# Count instances.
|
||||
message_body_lower_split_sort = list(set(status_message_lower_split))
|
||||
for i in message_body_lower_split_sort:
|
||||
count = status_message_lower_split.count(i)
|
||||
count_max = self.settings[room]['count']
|
||||
if count > count_max:
|
||||
reason = 'Spam (Repetition)'
|
||||
return reason, 1
|
||||
|
||||
# Check for denied phrases or words.
|
||||
blocklist = self.settings[room]['deny']
|
||||
for i in blocklist:
|
||||
if i in status_message_lower:
|
||||
# jid_bare = XmppMuc.get_full_jid(self, room, alias).split('/')[0]
|
||||
reason = 'Spam (Blocklist)'
|
||||
return reason, 1
|
||||
|
||||
# Check for frequency.
|
||||
presence_activity = self.settings[room]['activity_presence']
|
||||
length = len(presence_activity)
|
||||
presence_frequency = self.settings[room]['frequency_presence']
|
||||
if (presence_activity[length-1]['body'] and
|
||||
presence_activity[length-2]['body'] and
|
||||
presence_activity[length-1]['timestamp'] - presence_activity[length-2]['timestamp'] < presence_frequency and
|
||||
presence_activity[length-1]['alias'] == presence_activity[length-2]['alias'] and
|
||||
presence_activity[length-1]['id'] != presence_activity[length-2]['id'] and
|
||||
presence_activity[length-1]['body'] != presence_activity[length-2]['body']):
|
||||
reason = 'Spam (Flooding)'
|
||||
return reason, 0
|
||||
|
||||
if not reason:
|
||||
return None, None
|
171
kaikout/xmpp/muc.py
Normal file
171
kaikout/xmpp/muc.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
TODO
|
||||
|
||||
1) Send message to inviter that bot has joined to groupchat.
|
||||
|
||||
2) If groupchat requires captcha, send the consequent message.
|
||||
|
||||
3) If groupchat error is received, send that error message to inviter.
|
||||
|
||||
FIXME
|
||||
|
||||
1) Save name of groupchat instead of jid as name
|
||||
|
||||
"""
|
||||
from slixmpp.exceptions import IqError, IqTimeout, PresenceError
|
||||
from kaikout.log import Logger
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class XmppMuc:
|
||||
|
||||
|
||||
async def get_affiliation(self, room, affiliation):
|
||||
jids = await self.plugin['xep_0045'].get_affiliation_list(room, affiliation)
|
||||
return jids
|
||||
|
||||
|
||||
def get_alias(self, room, jid):
|
||||
alias = self.plugin['xep_0045'].get_nick(room, jid)
|
||||
return alias
|
||||
|
||||
|
||||
def get_full_jid(self, room, alias):
|
||||
"""
|
||||
Get full JId.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jid_full : str
|
||||
Full Jabber ID.
|
||||
"""
|
||||
jid_full = self.plugin['xep_0045'].get_jid_property(room, alias, 'jid')
|
||||
return jid_full
|
||||
|
||||
|
||||
def get_joined_rooms(self):
|
||||
rooms = self.plugin['xep_0045'].get_joined_rooms()
|
||||
return rooms
|
||||
|
||||
|
||||
async def get_role(self, room, role):
|
||||
jids = await self.plugin['xep_0045'].get_roles_list(room, role)
|
||||
return jids
|
||||
|
||||
|
||||
def get_roster(self, room):
|
||||
roster = self.plugin['xep_0045'].get_roster(room)
|
||||
return roster
|
||||
|
||||
|
||||
def is_moderator(self, room, alias):
|
||||
"""Check if given JID is a moderator"""
|
||||
role = self.plugin['xep_0045'].get_jid_property(room, alias, 'role')
|
||||
if role == 'moderator':
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
|
||||
async def join(self, jid, alias=None, password=None):
|
||||
# token = await initdb(
|
||||
# muc_jid,
|
||||
# sqlite.get_setting_value,
|
||||
# "token"
|
||||
# )
|
||||
# if token != "accepted":
|
||||
# token = randrange(10000, 99999)
|
||||
# await initdb(
|
||||
# muc_jid,
|
||||
# sqlite.update_setting_value,
|
||||
# ["token", token]
|
||||
# )
|
||||
# self.send_message(
|
||||
# mto=inviter,
|
||||
# mfrom=self.boundjid.bare,
|
||||
# mbody=(
|
||||
# "Send activation token {} to groupchat xmpp:{}?join."
|
||||
# ).format(token, muc_jid)
|
||||
# )
|
||||
logger.info('Joining groupchat\nJID : {}\n'.format(jid))
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
if not alias: alias = self.alias
|
||||
try:
|
||||
await self.plugin['xep_0045'].join_muc_wait(jid,
|
||||
alias,
|
||||
presence_options = {"pfrom" : jid_from},
|
||||
password=password,
|
||||
maxchars=0,
|
||||
maxstanzas=0,
|
||||
seconds=0,
|
||||
since=0,
|
||||
timeout=30)
|
||||
result = 'success'
|
||||
except IqError as e:
|
||||
logger.error('Error XmppIQ')
|
||||
logger.error(str(e))
|
||||
logger.error(jid)
|
||||
result = 'error'
|
||||
except IqTimeout as e:
|
||||
logger.error('Timeout XmppIQ')
|
||||
logger.error(str(e))
|
||||
logger.error(jid)
|
||||
result = 'timeout'
|
||||
except PresenceError as e:
|
||||
logger.error('Error Presence')
|
||||
logger.error(str(e))
|
||||
if (e.condition == 'forbidden' and
|
||||
e.presence['error']['code'] == '403'):
|
||||
logger.warning('{} is banned from {}'.format(self.alias, jid))
|
||||
result = 'ban'
|
||||
else:
|
||||
result = 'error'
|
||||
return result
|
||||
|
||||
|
||||
def leave(self, jid):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
message = ('This news bot will now leave this groupchat.\n'
|
||||
'The JID of this news bot is xmpp:{}?message'
|
||||
.format(self.boundjid.bare))
|
||||
status_message = ('This bot has left the group. '
|
||||
'It can be reached directly via {}'
|
||||
.format(self.boundjid.bare))
|
||||
self.send_message(mto=jid,
|
||||
mfrom=self.boundjid,
|
||||
mbody=message,
|
||||
mtype='groupchat')
|
||||
self.plugin['xep_0045'].leave_muc(jid,
|
||||
self.alias,
|
||||
status_message,
|
||||
jid_from)
|
||||
|
||||
|
||||
async def set_affiliation(self, room, affiliation, jid=None, alias=None, reason=None):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
try:
|
||||
await self.plugin['xep_0045'].set_affiliation(
|
||||
room, affiliation, jid=jid, nick=alias, reason=reason, ifrom=jid_from)
|
||||
except IqError as e:
|
||||
logger.error('Error XmppIQ')
|
||||
logger.error('Could not set affiliation at room: {}'.format(room))
|
||||
logger.error(str(e))
|
||||
logger.error(room)
|
||||
|
||||
|
||||
async def set_role(self, room, alias, role, reason=None):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
try:
|
||||
await self.plugin['xep_0045'].set_role(
|
||||
room, alias, role, reason=None, ifrom=jid_from)
|
||||
except IqError as e:
|
||||
logger.error('Error XmppIQ')
|
||||
logger.error('Could not set role of alias: {}'.format(alias))
|
||||
logger.error(str(e))
|
||||
logger.error(room)
|
21
kaikout/xmpp/presence.py
Normal file
21
kaikout/xmpp/presence.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class XmppPresence:
|
||||
|
||||
|
||||
def send(self, jid, status_message, presence_type=None, status_type=None):
|
||||
jid_from = str(self.boundjid) if self.is_component else None
|
||||
self.send_presence(pto=jid,
|
||||
pfrom=jid_from,
|
||||
pshow=status_type,
|
||||
pstatus=status_message,
|
||||
ptype=presence_type)
|
||||
|
||||
|
||||
def subscription(self, jid, presence_type):
|
||||
self.send_presence_subscription(pto=jid,
|
||||
pfrom=self.boundjid.bare,
|
||||
ptype=presence_type,
|
||||
pnick=self.alias)
|
118
kaikout/xmpp/profile.py
Normal file
118
kaikout/xmpp/profile.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
NOTE
|
||||
|
||||
The VCard XML fields that can be set are as follows:
|
||||
‘FN’, ‘NICKNAME’, ‘URL’, ‘BDAY’, ‘ROLE’, ‘NOTE’, ‘MAILER’,
|
||||
‘TZ’, ‘REV’, ‘UID’, ‘DESC’, ‘TITLE’, ‘PRODID’, ‘SORT-STRING’,
|
||||
‘N’, ‘ADR’, ‘TEL’, ‘EMAIL’, ‘JABBERID’, ‘ORG’, ‘CATEGORIES’,
|
||||
‘NOTE’, ‘PRODID’, ‘REV’, ‘SORT-STRING’, ‘SOUND’, ‘UID’, ‘URL’,
|
||||
‘CLASS’, ‘KEY’, ‘MAILER’, ‘GEO’, ‘TITLE’, ‘ROLE’,
|
||||
‘LOGO’, ‘AGENT’
|
||||
|
||||
TODO
|
||||
|
||||
1) Test XEP-0084.
|
||||
|
||||
2) Make sure to support all type of servers.
|
||||
|
||||
3) Catch IqError
|
||||
ERROR:slixmpp.basexmpp:internal-server-error: Database failure
|
||||
WARNING:slixmpp.basexmpp:You should catch IqError exceptions
|
||||
|
||||
"""
|
||||
|
||||
import glob
|
||||
from kaikout.config import Config
|
||||
import kaikout.config as config
|
||||
from kaikout.log import Logger
|
||||
from slixmpp.exceptions import IqTimeout, IqError
|
||||
import os
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
# class XmppProfile:
|
||||
|
||||
async def update(self):
|
||||
""" Update profile """
|
||||
try:
|
||||
await set_vcard(self)
|
||||
except IqTimeout as e:
|
||||
logger.error('Profile vCard: Error Timeout')
|
||||
logger.error(str(e))
|
||||
except IqError as e:
|
||||
logger.error('Profile vCard: Error XmppIQ')
|
||||
logger.error(str(e))
|
||||
try:
|
||||
await set_avatar(self)
|
||||
except IqTimeout as e:
|
||||
logger.error('Profile Photo: Error Timeout')
|
||||
logger.error(str(e))
|
||||
except IqError as e:
|
||||
logger.error('Profile Photo: Error XmppIQ')
|
||||
logger.error(str(e))
|
||||
|
||||
|
||||
async def set_avatar(self):
|
||||
config_dir = config.get_default_config_directory()
|
||||
if not os.path.isdir(config_dir):
|
||||
config_dir = '/usr/share/kaikout/'
|
||||
filename = glob.glob(config_dir + '/image.*')
|
||||
if not filename and os.path.isdir('/usr/share/kaikout/'):
|
||||
# filename = '/usr/share/kaikout/image.svg'
|
||||
filename = glob.glob('/usr/share/kaikout/image.*')
|
||||
if not filename:
|
||||
config_dir = os.path.dirname(__file__)
|
||||
config_dir = config_dir.split('/')
|
||||
config_dir.pop()
|
||||
config_dir = '/'.join(config_dir)
|
||||
filename = glob.glob(config_dir + '/assets/image.*')
|
||||
if len(filename):
|
||||
filename = filename[0]
|
||||
image_file = os.path.join(config_dir, filename)
|
||||
with open(image_file, 'rb') as avatar_file:
|
||||
avatar = avatar_file.read()
|
||||
# await self.plugin['xep_0084'].publish_avatar(avatar)
|
||||
try:
|
||||
await self.plugin['xep_0153'].set_avatar(avatar=avatar)
|
||||
except IqTimeout as e:
|
||||
logger.error('Profile Photo: Error Timeout 222')
|
||||
logger.error(str(e))
|
||||
except IqError as e:
|
||||
logger.error('Profile Photo: Error XmppIQ 222')
|
||||
logger.error(str(e))
|
||||
|
||||
|
||||
def set_identity(self, category):
|
||||
"""
|
||||
Identify for Service Descovery.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
category : str
|
||||
"client" or "service".
|
||||
|
||||
Returns
|
||||
-------
|
||||
None.
|
||||
|
||||
"""
|
||||
self['xep_0030'].add_identity(
|
||||
category=category,
|
||||
itype='news',
|
||||
name='kaikout',
|
||||
node=None,
|
||||
jid=self.boundjid.full,
|
||||
)
|
||||
|
||||
|
||||
async def set_vcard(self):
|
||||
vcard = self.plugin['xep_0054'].make_vcard()
|
||||
profile = config.get_values('accounts.toml', 'xmpp')['profile']
|
||||
for key in profile:
|
||||
vcard[key] = profile[key]
|
||||
await self.plugin['xep_0054'].publish_vcard(vcard)
|
||||
|
46
kaikout/xmpp/status.py
Normal file
46
kaikout/xmpp/status.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from kaikout.database import Toml
|
||||
from kaikout.log import Logger
|
||||
from kaikout.xmpp.presence import XmppPresence
|
||||
from kaikout.xmpp.utilities import XmppUtilities
|
||||
import sys
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class XmppStatus:
|
||||
|
||||
|
||||
def send_status_message(self, room, status_mode=None, status_text=None):
|
||||
"""
|
||||
Send status message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jid : str
|
||||
Jabber ID.
|
||||
"""
|
||||
function_name = sys._getframe().f_code.co_name
|
||||
logger.debug('{}: jid: {}'.format(function_name, room))
|
||||
if not status_mode and not status_text:
|
||||
if XmppUtilities.is_moderator(self, room, self.alias):
|
||||
if room not in self.settings:
|
||||
Toml.instantiate(self, room)
|
||||
# Toml.load_jid_settings(self, room)
|
||||
if self.settings[room]['enabled']:
|
||||
jid_task = self.actions[room] if room in self.actions else None
|
||||
if jid_task and len(jid_task):
|
||||
status_mode = 'dnd'
|
||||
status_text = jid_task[list(jid_task.keys())[0]]
|
||||
else:
|
||||
status_mode = 'available'
|
||||
status_text = '👁️ Moderating'
|
||||
else:
|
||||
status_mode = 'xa'
|
||||
status_text = '💤 Disabled'
|
||||
else:
|
||||
status_text = '⚠️ KaikOut requires moderation privileges'
|
||||
status_mode = 'away'
|
||||
XmppPresence.send(self, room, status_text, status_type=status_mode)
|
117
kaikout/xmpp/utilities.py
Normal file
117
kaikout/xmpp/utilities.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from kaikout.log import Logger
|
||||
from kaikout.xmpp.muc import XmppMuc
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
logger = Logger(__name__)
|
||||
|
||||
|
||||
class XmppUtilities:
|
||||
|
||||
|
||||
async def is_jid_of_moderators(self, room, jid_full):
|
||||
# try:
|
||||
moderators = await XmppMuc.get_role(self, room, 'moderator')
|
||||
for alias in moderators:
|
||||
# Note: You might want to compare jid_bare
|
||||
if XmppMuc.get_full_jid(self, room, alias) == jid_full:
|
||||
return alias
|
||||
# except IqError as e:
|
||||
# logger.error(e)
|
||||
return False
|
||||
|
||||
|
||||
async def get_chat_type(self, jid):
|
||||
"""
|
||||
Check chat (i.e. JID) type.
|
||||
|
||||
If iqresult["disco_info"]["features"] contains XML namespace
|
||||
of 'http://jabber.org/protocol/muc', then it is a 'groupchat'.
|
||||
|
||||
Unless it has forward slash, which would indicate that it is
|
||||
a chat which is conducted through a groupchat.
|
||||
|
||||
Otherwise, determine type 'chat'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
jid : str
|
||||
Jabber ID.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : str
|
||||
'chat' or 'groupchat' or 'error'.
|
||||
"""
|
||||
try:
|
||||
iqresult = await self["xep_0030"].get_info(jid=jid)
|
||||
features = iqresult["disco_info"]["features"]
|
||||
# identity = iqresult['disco_info']['identities']
|
||||
# if 'account' in indentity:
|
||||
# if 'conference' in indentity:
|
||||
if ('http://jabber.org/protocol/muc' in features) and not ('/' in jid):
|
||||
result = "groupchat"
|
||||
# TODO elif <feature var='jabber:iq:gateway'/>
|
||||
# NOTE Is it needed? We do not interact with gateways or services
|
||||
else:
|
||||
result = "chat"
|
||||
logger.info('Jabber ID: {}\n'
|
||||
'Chat Type: {}'.format(jid, result))
|
||||
except (IqError, IqTimeout) as e:
|
||||
logger.warning('Chat type could not be determined for {}'.format(jid))
|
||||
logger.error(e)
|
||||
result = 'error'
|
||||
# except BaseException as e:
|
||||
# logger.error('BaseException', str(e))
|
||||
# finally:
|
||||
# logger.info('Chat type is:', chat_type)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def is_access(self, jid_bare, jid_full, chat_type):
|
||||
"""Determine access privilege"""
|
||||
operator = XmppUtilities.is_operator(self, jid_bare)
|
||||
if operator:
|
||||
if chat_type == 'groupchat':
|
||||
if XmppUtilities.is_moderator(self, jid_bare, jid_full):
|
||||
access = True
|
||||
else:
|
||||
access = True
|
||||
else:
|
||||
access = False
|
||||
return access
|
||||
|
||||
|
||||
def is_operator(self, jid_bare):
|
||||
"""Check if given JID is an operator"""
|
||||
result = False
|
||||
for operator in self.operators:
|
||||
if jid_bare == operator['jid']:
|
||||
result = True
|
||||
# operator_name = operator['name']
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def is_moderator(self, room, alias):
|
||||
"""Check if given JID is a moderator"""
|
||||
role = self.plugin['xep_0045'].get_jid_property(room, alias, 'role')
|
||||
if role == 'moderator':
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
|
||||
def is_member(self, jid_bare, jid_full):
|
||||
"""Check if given JID is a member"""
|
||||
alias = jid_full[jid_full.index('/')+1:]
|
||||
affiliation = self.plugin['xep_0045'].get_jid_property(jid_bare, alias, 'affiliation')
|
||||
if affiliation == 'member':
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
return result
|
53
pyproject.toml
Normal file
53
pyproject.toml
Normal file
|
@ -0,0 +1,53 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.2"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "kaikout"
|
||||
version = "0.1"
|
||||
description = "Kaiko (Japanese: 懐古) Out is a group chat moderation chat bot"
|
||||
authors = [{name = "Schimon Zachary", email = "sch@fedora.email"}]
|
||||
license = {text = "AGPL-3.0-only"}
|
||||
classifiers = [
|
||||
"Framework :: slixmpp",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Internet :: Extensible Messaging and Presence Protocol (XMPP)",
|
||||
"Topic :: Internet :: Instant Messaging",
|
||||
"Topic :: Internet :: XMPP",
|
||||
]
|
||||
keywords = [
|
||||
"bot",
|
||||
"chat",
|
||||
"im",
|
||||
"jabber",
|
||||
"spam",
|
||||
"xmpp",
|
||||
]
|
||||
# urls = {Homepage = "https://git.xmpp-it.net/sch/kaikout"}
|
||||
dependencies = [
|
||||
"tomli", # Python 3.10
|
||||
"tomli_w",
|
||||
"slixmpp",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "http://kaikout.i2p/"
|
||||
Repository = "https://git.xmpp-it.net/sch/kaikout"
|
||||
Issues = "https://codeberg.org/sch/kaikout/issues"
|
||||
|
||||
# [project.readme]
|
||||
# text = "kaikout is a moderation chat bot"
|
||||
|
||||
[project.scripts]
|
||||
kaikout = "kaikout.__main__:main"
|
||||
|
||||
[tool.setuptools]
|
||||
platforms = ["any"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["*.toml", "*.png"]
|
Loading…
Reference in a new issue