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