mirror of
https://github.com/PC-Admin/matrix-moderation-tool.git
synced 2025-09-06 07:50:31 -04:00
Compare commits
2 Commits
fcf344a921
...
requests_m
Author | SHA1 | Date | |
---|---|---|---|
|
b24bebe88f | ||
|
0b6c8a0845 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
hardcoded_variables.py
|
|
675
LICENSE
675
LICENSE
@@ -1,21 +1,662 @@
|
|||||||
MIT License
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (c) 2023 Michael Collins
|
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.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
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:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU 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/>.
|
||||||
|
|
||||||
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.
|
|
258
README.md
258
README.md
@@ -1,37 +1,26 @@
|
|||||||
|
|
||||||
# Matrix Moderation Tool
|
# PC-Admin's Synapse Moderation Tool
|
||||||
|
|
||||||
A Matrix moderation tool to make managing a Synapse server easier.
|
|
||||||
|
|
||||||
Contact me at [@michael:perthchat.org](https://matrix.to/#/@michael:perthchat.org) if you get stuck or have an edit in mind.
|
This is a script i wrote to make moderating a Synapse server easier.
|
||||||
|
|
||||||
|
Contact me at: @PC-Admin:perthchat.org if you get stuck or have an edit in mind.
|
||||||
|
|
||||||
***
|
***
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
This work is published under the MIT license, for more information on this license see here: https://opensource.org/license/mit/
|
This work is licensed under GNU Affero General Public License v3, for more information on this license see here: https://www.gnu.org/licenses/agpl-3.0.txt
|
||||||
|
|
||||||
|
|
||||||
***
|
***
|
||||||
## Setup script
|
## Setup script
|
||||||
|
|
||||||
You can hard code the server URL, federation port and access token into the [hardcoded_variables.py](./hardcoded_variables.py) file for faster use, it will prompt you for these values if you don't.
|
You can hard code the server URL, federation port and access token for faster use, it will prompt you for these values if you don't.
|
||||||
|
|
||||||
Your access token can be found in Element > Settings > Help & About, your user account must first be upgraded to a server admin.
|
Your access token can be found in Element > Settings > Help & About, your user account must first be upgraded to a server admin.
|
||||||
|
|
||||||
This script also requires you to install the following PIP packages:
|
|
||||||
```
|
|
||||||
pip3 install python-whois
|
|
||||||
pip3 install requests
|
|
||||||
pip3 install matrix-nio
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
***
|
***
|
||||||
## Upgrade user to 'server admin'
|
## Upgrade user to 'server admin'
|
||||||
|
|
||||||
To use this moderation script you need an OAuth token of a "server admin" account. If you've just setup a Matrix server, you'll need to promote an account to server admin by altering your database.
|
|
||||||
|
|
||||||
https://github.com/matrix-org/synapse/tree/master/docs/admin_api
|
https://github.com/matrix-org/synapse/tree/master/docs/admin_api
|
||||||
|
|
||||||
“So first connect to the correct db and then run the UPDATE users...”
|
“So first connect to the correct db and then run the UPDATE users...”
|
||||||
@@ -46,10 +35,11 @@ UPDATE 1
|
|||||||
|
|
||||||
synapse=#
|
synapse=#
|
||||||
|
|
||||||
Note: A ‘-’ sign instead of ‘=’ means you didn't type a complete SQL query yet!
|
‘-’ sign instead of ‘=’ means:
|
||||||
|
|
||||||
(You need a semicolon (;) at the end to terminate the command.)
|
It means you didn't type a complete SQL query yet.
|
||||||
|
|
||||||
|
You need a semicolon to terminate the command.
|
||||||
|
|
||||||
***
|
***
|
||||||
## Make sure /_synapse/ is mapped
|
## Make sure /_synapse/ is mapped
|
||||||
@@ -72,235 +62,3 @@ A few of the commands will not work unless /_synapse/ is mapped to port 8008. He
|
|||||||
|
|
||||||
You can also run the script locally on your server if you do not wish to map /_synapse/.
|
You can also run the script locally on your server if you do not wish to map /_synapse/.
|
||||||
|
|
||||||
With the popular [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy) playbook you can expose this API interface by enabling 'Synapse Admin':
|
|
||||||
|
|
||||||
`matrix_synapse_admin_enabled: true`
|
|
||||||
|
|
||||||
|
|
||||||
***
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
To do:
|
|
||||||
1) Add the following functions:
|
|
||||||
- https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#find-a-user-based-on-their-id-in-an-auth-provider
|
|
||||||
- https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#find-a-user-based-on-their-third-party-id-threepid-or-3pid
|
|
||||||
- https://github.com/matrix-org/synapse/blob/master/docs/admin_api/delete_group.md
|
|
||||||
2) Add fully automated (should just return a web link and decryption password) reporting functions for users:
|
|
||||||
- Description of why the report was made (what happened), include key information
|
|
||||||
- User's ID - DONE
|
|
||||||
- Whois Data - DONE
|
|
||||||
- Account Data - DONE
|
|
||||||
- Query Data - DONE
|
|
||||||
- Pushers List - DONE
|
|
||||||
- IPs + ipinfo Data - DONE
|
|
||||||
- List of the rooms the user is participating in, divided into 1:1 conversations and larger rooms - DONE
|
|
||||||
- Any other usernames associated with that IP
|
|
||||||
- Timestamp for when illegal material was accessed
|
|
||||||
- Description of report format and contents (to guide the reader)
|
|
||||||
- Collect state event dumps of recently read rooms as well (as they may have looked at other suss rooms recently)
|
|
||||||
3) Have recommended rdlist function:
|
|
||||||
- return a list of offending accounts and the tags they accessed (for creating incident_dict's)
|
|
||||||
- add the shadowban function to prevent members alerting others after mass shutdowns - DONE
|
|
||||||
4) Only email reportID in incident report?
|
|
||||||
5) Add a room report function to create a properly formatted report for rdlist
|
|
||||||
6) Expand the incident reporting to also issue reports over Matrix
|
|
||||||
7) Automated public room joining and reminder if reporting email is not available?
|
|
||||||
8) Refine ipinfo module to also return extra details about the IP
|
|
||||||
9) Make existing functions compatible with JSON formatted inputs
|
|
||||||
|
|
||||||
|
|
||||||
***
|
|
||||||
## rdlist Functionality
|
|
||||||
|
|
||||||
'rdlist' is a comprehensive list of child abuse related rooms on Matrix, it's a safety initiative led by the [Legion of Janitors](https://matrix.to/#/#janitors:glowers.club).
|
|
||||||
|
|
||||||
This script can automatically load and block/purge abusive rooms from rdlist, making it **very easy** for inexperienced administrators to block this harmful content.
|
|
||||||
|
|
||||||
If you are running a public server, please dm me at [@michael:perthchat.org](https://matrix.to/#/@michael:perthchat.org) and I can invite you to the 'Legion of Janitors' room.
|
|
||||||
|
|
||||||
Once you have read access to the [rdlist repository](https://code.glowers.club/loj/rdlist), this script can be used for multiple rdlist related functions.
|
|
||||||
|
|
||||||
|
|
||||||
***
|
|
||||||
## Collect User Reports on local users in rdlist rooms
|
|
||||||
|
|
||||||
This script can automatically generate 'User Reports' for each one of your local users in rdlist rooms that have the 'recommended tags'.
|
|
||||||
|
|
||||||
These user reports can be given to law enforcement or shared in [#janitor-dumps](https://matrix.to/#/#janitor-dumps:glowers.club) to help us locate more abusive users/rooms.
|
|
||||||
|
|
||||||
```
|
|
||||||
130
|
|
||||||
|
|
||||||
rdlist repo already cloned...
|
|
||||||
Fetching origin
|
|
||||||
Pulling latest changes from rdlist repo...
|
|
||||||
|
|
||||||
WARNING! The following local users are current members of rooms tagged in rdlist: ['@fatweeb23838:perthchat.org', '@somecreep29330:perthchat.org']
|
|
||||||
|
|
||||||
Do you want to generate a user report file for each of these users? y/n? y
|
|
||||||
|
|
||||||
Generating user report for fatweeb23838...
|
|
||||||
Report generated successfully on user: "fatweeb23838"
|
|
||||||
|
|
||||||
You can send this .zip file when reporting a user to law enforcement.
|
|
||||||
.zip file location: /home/pcadmin/PerthchatVault/Perthchat_LIVE/matrix-moderation-tool/reports/fatweeb23838_2023-08-01_23-19-24.zip
|
|
||||||
.zip file size: 0.00966 MB
|
|
||||||
|
|
||||||
|
|
||||||
Generating user report for somecreep29330...
|
|
||||||
Report generated successfully on user: "somecreep29330"
|
|
||||||
|
|
||||||
You can send this .zip file when reporting a user to law enforcement.
|
|
||||||
.zip file location: /home/pcadmin/PerthchatVault/Perthchat_LIVE/matrix-moderation-tool/reports/somecreep29330_2023-08-01_23-19-27.zip
|
|
||||||
.zip file size: 0.29578 MB
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
***
|
|
||||||
## Send Incident Reports for remote users in rdlist rooms
|
|
||||||
|
|
||||||
This script can automatically generate 'Incident Reports' for every remote homeserver admin with users in rdlist rooms that have the 'recommended tags'.
|
|
||||||
|
|
||||||
It examines the homeserver involved to find a admin contact method via [MSC1929](https://github.com/matrix-org/matrix-spec-proposals/pull/1929). If an MXID is returned it will attempt to send the Incident Report over Matrix. If an email is provided it will send the Incident Report over email. If neither is found a whois lookup is performed and the Incident Report are sent to the domain registrar via email.
|
|
||||||
|
|
||||||
```
|
|
||||||
131
|
|
||||||
|
|
||||||
rdlist repo already cloned...
|
|
||||||
Fetching origin
|
|
||||||
Pulling latest changes from rdlist repo...
|
|
||||||
|
|
||||||
WARNING! The following remote users are current members of rooms tagged in rdlist: ['@pedobear:matrix.org', '@randomcreep:perthchat.org']
|
|
||||||
|
|
||||||
Do you want to send out incident reports for these users to every homeserver admin involved? y/n? y
|
|
||||||
|
|
||||||
Sending Incident Report for users from matrix.org to abuse@matrix.org
|
|
||||||
|
|
||||||
Sending Incident Report for users from perthchat.org to @michael:perthchat.org
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## rdlist Block/Purge all rooms with recommended rdlist tags
|
|
||||||
|
|
||||||
Finally this script can be used to shutdown rooms with the recommended rdlist tags.
|
|
||||||
|
|
||||||
This function is much larger and will ask you if you also want to create user/incident reports before the shutdowns. (Recommended) It'll also ask you if you want to shadowban the users in these rooms to prevent them from alerting others. (Recommended) Finally it'll ask if you want to shutdown the local accounts located in these rooms.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ python3 moderation_tool.py
|
|
||||||
|
|
||||||
Please select one of the following options:
|
|
||||||
...
|
|
||||||
Please enter a number from the above menu, or enter 'q' or 'e' to exit.
|
|
||||||
|
|
||||||
132
|
|
||||||
|
|
||||||
@mod_team:perthchat.org account already exists. Resetting account password.
|
|
||||||
|
|
||||||
Ensuring @mod_team:perthchat.org account is a server admin.
|
|
||||||
|
|
||||||
rdlist repo already cloned...
|
|
||||||
Fetching origin
|
|
||||||
rdlist repo is up-to-date, no need to pull changes.
|
|
||||||
|
|
||||||
Using recommended rdlist tags. Rooms matching the following tags will be purged and/or blocked:
|
|
||||||
['hub_room_links', 'hub_room_trade', 'preban', 'degen_misc', 'beastiality', 'degen_porn', 'gore', 'snuff', 'degen_larp', 'hub_room_sussy', 'bot_spam', 'cfm', 'jailbait', 'bot_porn', 'toddlercon', 'loli', 'csam', 'tfm', 'degen_meet', 'stylized_3d_loli', '3d_loli']
|
|
||||||
|
|
||||||
WARNING! The following local users are current members of rooms tagged in rdlist: ['@***REDACTED***:perthchat.org']
|
|
||||||
|
|
||||||
Do you want to generate a user report file for each of these users? y/n? n
|
|
||||||
|
|
||||||
Skipping user report generation...
|
|
||||||
|
|
||||||
WARNING! The following remote users are current members of rooms tagged in rdlist: ['@***REDACTED***:matrix.org']
|
|
||||||
|
|
||||||
Do you want to send out incident reports for these users to every homeserver admin involved? y/n? n
|
|
||||||
|
|
||||||
Skipping incident report generation...
|
|
||||||
|
|
||||||
|
|
||||||
Number of rdlist rooms being shutdown: 346
|
|
||||||
|
|
||||||
Are you sure you want to block/shutdown these rooms? y/n? y
|
|
||||||
|
|
||||||
|
|
||||||
Skipping already blocked room: !***REDACTED***:matrix.org
|
|
||||||
|
|
||||||
|
|
||||||
Skipping already blocked room: !***REDACTED***:matrix.org
|
|
||||||
|
|
||||||
|
|
||||||
Skipping already blocked room: !***REDACTED***:matrix.org
|
|
||||||
|
|
||||||
|
|
||||||
Blocking unknown room: !***REDACTED***:matrix.org
|
|
||||||
Successfully blocked room !***REDACTED***:matrix.org
|
|
||||||
|
|
||||||
|
|
||||||
Blocking unknown room: !***REDACTED***:matrix.org
|
|
||||||
Successfully blocked room !***REDACTED***:matrix.org
|
|
||||||
|
|
||||||
|
|
||||||
Skipping already blocked room: !***REDACTED***:matrix.org
|
|
||||||
|
|
||||||
|
|
||||||
Shutting down known room: !***REDACTED***:sibnsk.net
|
|
||||||
Sleeping for 2 seconds...
|
|
||||||
Sleeping for 4 seconds...
|
|
||||||
Sleeping for 8 seconds...
|
|
||||||
!***REDACTED***:sibnsk.net has been successfully shutdown!
|
|
||||||
List of kicked users:
|
|
||||||
@***REDACTED***:perthchat.org
|
|
||||||
|
|
||||||
|
|
||||||
Skipping already blocked room: !***REDACTED***:anontier.nl
|
|
||||||
|
|
||||||
|
|
||||||
Room shutdowns completed!
|
|
||||||
|
|
||||||
User login details for your moderator account:
|
|
||||||
|
|
||||||
Username: mod_team
|
|
||||||
Password: ***REDACTED***
|
|
||||||
|
|
||||||
Print rdlist statistics:
|
|
||||||
|
|
||||||
Number of rooms blocked: 4
|
|
||||||
Number of rooms purged: 2
|
|
||||||
Number of local users located in rdlist rooms and kicked: 1
|
|
||||||
|
|
||||||
The following users were current members of rooms tagged in rdlist: ['@***REDACTED***:perthchat.org']
|
|
||||||
|
|
||||||
Do you want to also deactivate all these accounts that were kicked from rdlist rooms? y/n?
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
***
|
|
||||||
## One-touch Reporting
|
|
||||||
|
|
||||||
WARNING: This section is under heavy development and shouldn't be used by anyone!!!
|
|
||||||
|
|
||||||
This script can automatically generate reports about user accounts for law enforcement.
|
|
||||||
|
|
||||||
It collects as much data about the target user account as possible, then packages it into an encrypted ZIP file that can be shared:
|
|
||||||
```
|
|
||||||
|
|
||||||
Please enter a number from the above menu, or enter 'q' or 'e' to exit.
|
|
||||||
|
|
||||||
150
|
|
||||||
|
|
||||||
Please enter the username to automatically generate a report: pedobear
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
Report generated successfully on user: "pedobear"
|
|
||||||
|
|
||||||
You can send this .zip file and password when reporting a user to law enforcement.
|
|
||||||
|
|
||||||
.zip file location: ./reports/pedobear_2023-07-23_02-21-56.zip
|
|
||||||
.zip file size: 0.503927 MB
|
|
||||||
```
|
|
||||||
|
@@ -1,62 +0,0 @@
|
|||||||
|
|
||||||
from nio import AsyncClient, RoomCreateResponse, LoginResponse
|
|
||||||
import asyncio
|
|
||||||
import hardcoded_variables
|
|
||||||
|
|
||||||
async def create_session(username: str, password: str, homeserver: str) -> AsyncClient:
|
|
||||||
client = AsyncClient(homeserver, username)
|
|
||||||
response = await client.login(password)
|
|
||||||
|
|
||||||
if isinstance(response, LoginResponse):
|
|
||||||
await client.sync() # Perform a sync after login
|
|
||||||
return client
|
|
||||||
|
|
||||||
raise Exception(f"Failed to log in: {response}")
|
|
||||||
|
|
||||||
async def create_room(client: AsyncClient, name: str) -> str:
|
|
||||||
response = await client.room_create(name=name)
|
|
||||||
|
|
||||||
if isinstance(response, RoomCreateResponse):
|
|
||||||
return response.room_id
|
|
||||||
|
|
||||||
raise Exception(f"Failed to create room: {response}")
|
|
||||||
|
|
||||||
async def invite_user(client: AsyncClient, room_id: str, user_id: str):
|
|
||||||
response = await client.room_invite(room_id, user_id)
|
|
||||||
|
|
||||||
if not response:
|
|
||||||
raise Exception(f"Failed to invite user: {response}")
|
|
||||||
|
|
||||||
async def send_message(receiver: str, message: str):
|
|
||||||
homeserver = "https://" + hardcoded_variables.homeserver_url
|
|
||||||
client = await create_session(hardcoded_variables.rdlist_bot_username, hardcoded_variables.rdlist_bot_password, homeserver)
|
|
||||||
try:
|
|
||||||
# Check if room with the receiver already exists
|
|
||||||
for room in client.rooms.values():
|
|
||||||
if receiver in room.users and len(room.users) == 2:
|
|
||||||
room_id = room.room_id
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Create new room if it doesn't exist
|
|
||||||
room_id = await create_room(client, "Incident Report")
|
|
||||||
await invite_user(client, room_id, receiver)
|
|
||||||
|
|
||||||
content = {
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": message,
|
|
||||||
}
|
|
||||||
response = await client.room_send(room_id, message_type="m.room.message", content=content)
|
|
||||||
if not response:
|
|
||||||
raise Exception(f"Failed to send message: {response}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
def test_matrix_message():
|
|
||||||
async def main():
|
|
||||||
receiver = hardcoded_variables.report_return_mxid
|
|
||||||
message = "Hello! This is a test message. Please ignore it."
|
|
||||||
await send_message(receiver, message)
|
|
||||||
print("\nMessage successfully sent.")
|
|
||||||
|
|
||||||
asyncio.get_event_loop().run_until_complete(main())
|
|
@@ -1,23 +0,0 @@
|
|||||||
[
|
|
||||||
"!yrYxlstWvCGAdoJouV:matrix.org",
|
|
||||||
"!COsuaadCTetbYqrFMU:matrix.org",
|
|
||||||
"!GArjClfoUptAWccrVS:matrix.org",
|
|
||||||
"!DSrthZwcsuuMZdeVuT:matrix.org",
|
|
||||||
"!AocvKCbVBHkvmRuCsv:matrix.org",
|
|
||||||
"!KKFvvGdehVopAioVRf:matrix.org",
|
|
||||||
"!LRxhNbCOrTsTyNImIw:matrix.org",
|
|
||||||
"!WXbpkzOYvcSUTLQymi:matrix.org",
|
|
||||||
"!vPbZHfLrCDqkgorTcG:matrix.org",
|
|
||||||
"!jUJnVLSfaTjDoolSDA:matrix.org",
|
|
||||||
"!btrXMmdLuXglyApMfF:matrix.org",
|
|
||||||
"!delydyfeWafFCcoHFx:matrix.org",
|
|
||||||
"!GRAudIVKbttnwQaNQY:matrix.org",
|
|
||||||
"!mkaytGWtZjkUvNsIiD:matrix.org",
|
|
||||||
"!emZVfYzjsfxlWAowjC:matrix.org",
|
|
||||||
"!TRhdoFPfRxzyglpOim:matrix.org",
|
|
||||||
"!oKEaimElquIAynUazT:tchncs.de",
|
|
||||||
"!qiozdRbdpCHclyYshw:matrix.org",
|
|
||||||
"!IBNZSFHWfwYfWcNJvP:matrix.org",
|
|
||||||
"!aKprLHSIAuAnaGnBcE:matrix.org",
|
|
||||||
"!WIyBceTWJxXlLdEoyY:matrix.org"
|
|
||||||
]
|
|
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
###########################################################################
|
|
||||||
# These values can be hard coded for easier usage: #
|
|
||||||
homeserver_url = "matrix.example.org" # Your homeserver URL
|
|
||||||
base_url = "example.org" # Your base URL (appears in usernames)
|
|
||||||
access_token = "" # Your homeserver admin access token
|
|
||||||
# ipinfo.io token
|
|
||||||
ipinfo_token = "" # Leave blank to disable ipinfo.io lookups
|
|
||||||
# rdlist specific
|
|
||||||
rdlist_bot_username = "mod_team" # The username to perform automated room shutdowns
|
|
||||||
rdlist_bot_username = "strong-password" # The password for this user
|
|
||||||
rdlist_recommended_tags = ['hub_room_links', 'hub_room_trade', 'preban', 'degen_misc', 'beastiality', 'degen_porn', 'gore', 'snuff', 'degen_larp', 'hub_room_sussy', 'bot_spam', 'cfm', 'jailbait', 'bot_porn', 'toddlercon', 'loli', 'csam', 'tfm', 'degen_meet', 'stylized_3d_loli', '3d_loli']
|
|
||||||
# User report generator
|
|
||||||
report_folder = "./reports" # Reports folder name
|
|
||||||
testing_mode = True # Prevents the incident report feature from messaging/emailing anyone besides you, also limits the number of room states are exported when generating user reports.
|
|
||||||
# Incident report email settings
|
|
||||||
smtp_user = "abuse@matrix.example.org"
|
|
||||||
smtp_password = "strong-stmp-password"
|
|
||||||
smtp_server = "smtp.provider.org"
|
|
||||||
smtp_port = 587
|
|
||||||
incident_report_return_email = "youradminemail@example.org"
|
|
||||||
incident_report_return_mxid = "@yourmxid:example.com"
|
|
||||||
###########################################################################
|
|
||||||
|
|
@@ -1,79 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import csv
|
|
||||||
import time
|
|
||||||
import socket
|
|
||||||
import hardcoded_variables
|
|
||||||
import user_commands
|
|
||||||
|
|
||||||
# Should use: https://github.com/ipinfo/python
|
|
||||||
|
|
||||||
def is_valid_ipv4(ip):
|
|
||||||
try:
|
|
||||||
socket.inet_pton(socket.AF_INET, ip)
|
|
||||||
except socket.error: # not a valid address
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def analyse_account_ip(preset_username):
|
|
||||||
if not preset_username:
|
|
||||||
preset_username = input("\nPlease enter a username to analyse their country of origin: ")
|
|
||||||
data = user_commands.whois_account(preset_username=preset_username)
|
|
||||||
|
|
||||||
user_id = data['user_id']
|
|
||||||
device_data = data['devices']
|
|
||||||
|
|
||||||
ip_info = {}
|
|
||||||
for device_id, device_info in device_data.items():
|
|
||||||
for session in device_info['sessions']:
|
|
||||||
for connection in session['connections']:
|
|
||||||
ip = connection['ip']
|
|
||||||
if is_valid_ipv4(ip) and len(hardcoded_variables.ipinfo_token) > 0:
|
|
||||||
res = requests.get(f"https://ipinfo.io/{ip}",
|
|
||||||
headers={"Authorization": f"Bearer {hardcoded_variables.ipinfo_token}"})
|
|
||||||
if res.status_code == 200:
|
|
||||||
country = res.json().get('country')
|
|
||||||
ip_info[ip] = country
|
|
||||||
|
|
||||||
if len(hardcoded_variables.ipinfo_token) == 0:
|
|
||||||
return {"user_id": user_id, "ip_info": "IPINFO DISABLED"}
|
|
||||||
else:
|
|
||||||
return {"user_id": user_id, "ip_info": ip_info}
|
|
||||||
|
|
||||||
def analyse_multiple_account_ips():
|
|
||||||
print("Analyse multiple user IPs selected")
|
|
||||||
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of Matrix usernames: ")
|
|
||||||
with open(user_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
print(len(data))
|
|
||||||
|
|
||||||
print("\n" + str(data))
|
|
||||||
|
|
||||||
output_file = None
|
|
||||||
if len(data) > 10:
|
|
||||||
file_confirmation = input("\nThere are more than 10 users. Would you like to save the output to a file? y/n?\n")
|
|
||||||
if file_confirmation.lower() in ("y", "yes"):
|
|
||||||
output_file = input("\nPlease enter the desired output file path:\n")
|
|
||||||
|
|
||||||
analyse_confirmation = input("\n\nAre you sure you want to analyse the IP of all of these users? y/n?\n")
|
|
||||||
|
|
||||||
if analyse_confirmation.lower() in ("y", "yes"):
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
output = analyse_account_ip(data[x][0])
|
|
||||||
|
|
||||||
# if output file is specified, append to file
|
|
||||||
if output_file:
|
|
||||||
with open(output_file, 'a') as f:
|
|
||||||
f.write(output + "\n")
|
|
||||||
x += 1
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
if analyse_confirmation.lower() in ("n", "no"):
|
|
||||||
print("\nExiting...\n")
|
|
||||||
|
|
||||||
if output_file and os.path.isfile(output_file):
|
|
||||||
print(f"Output saved to {output_file}")
|
|
@@ -1,216 +0,0 @@
|
|||||||
|
|
||||||
import json
|
|
||||||
import user_commands
|
|
||||||
import room_commands
|
|
||||||
import server_commands
|
|
||||||
import ipinfo_commands
|
|
||||||
import rdlist_commands
|
|
||||||
import report_commands
|
|
||||||
import bot_commands
|
|
||||||
import hardcoded_variables
|
|
||||||
|
|
||||||
# check if homeserver url is hard coded, if not set it
|
|
||||||
|
|
||||||
if hardcoded_variables.homeserver_url == "matrix.example.org":
|
|
||||||
homeserver_url = input("What is the URL of your server? Eg: matrix.example.org ")
|
|
||||||
|
|
||||||
# check if base url is hard coded, if not set it
|
|
||||||
|
|
||||||
if hardcoded_variables.base_url == "example.org":
|
|
||||||
base_url = input("What is the URL of your server? Eg: example.org ")
|
|
||||||
|
|
||||||
# check if access token is hard coded, if not set it
|
|
||||||
|
|
||||||
length_access_token = len(hardcoded_variables.access_token)
|
|
||||||
|
|
||||||
if length_access_token == 0:
|
|
||||||
access_token = input("Please enter access token for server admin account: ")
|
|
||||||
|
|
||||||
# loop menu for various moderation actions
|
|
||||||
|
|
||||||
pass_token = False
|
|
||||||
while pass_token == False:
|
|
||||||
print("\n##########################")
|
|
||||||
print("# MATRIX MODERATION TOOL #")
|
|
||||||
print("##########################")
|
|
||||||
print("\nA tool for making common Synapse moderation tasks easier. Created by @PC-Admin.")
|
|
||||||
print("\n----------------------------------------------")
|
|
||||||
print("\n#### User Account Commands ####\t\t\t#### Room Commands ####")
|
|
||||||
print("1) Deactivate a user account.\t\t\t50) List details of a room.")
|
|
||||||
print("2) Deactivate multiple user accounts.\t\t51) List the members of a room.")
|
|
||||||
print("3) Create a user account.\t\t\t52) Export the state events of a room.")
|
|
||||||
print("4) Create multiple user accounts.\t\t53) Export the state events of multiple rooms.")
|
|
||||||
print("5) Reset a users password.\t\t\t54) List rooms in public directory.")
|
|
||||||
print("6) Whois user account.\t\t\t\t55) Remove a room from the public directory.")
|
|
||||||
print("7) Whois multiple user accounts.\t\t56) Remove multiple rooms from the public directory.")
|
|
||||||
print("8) Query user account.\t\t\t\t57) Redact a room event.")
|
|
||||||
print("9) Query multiple user accounts.\t\t58) List/Download all media in a room.")
|
|
||||||
print("10) List room memberships of user.\t\t59) Download media from multiple rooms.")
|
|
||||||
print("11) Promote a user to server admin.\t\t60) Quarantine all media in a room.")
|
|
||||||
print("12) List all user accounts.\t\t\t61) Shutdown a room.")
|
|
||||||
print("13) Quarantine all media a users uploaded.\t62) Shutdown multiple rooms.")
|
|
||||||
print("14) Collect account data.\t\t\t63) Delete a room.")
|
|
||||||
print("15) List account pushers.\t\t\t64) Delete multiple rooms.")
|
|
||||||
print("16) Get rate limit of a user account.\t\t65) Purge the event history of a room to a specific timestamp.")
|
|
||||||
print("17) Set rate limit of a user account.\t\t66) Purge the event history of multiple rooms to a specific timestamp.")
|
|
||||||
print("18) Delete rate limit of a user account.\t67) Get blocked status for room.")
|
|
||||||
print("19) Check if user account exists.\t\t68) Block a room.")
|
|
||||||
print("20) Shadow ban a user.\t\t\t\t69) Unblock a room.")
|
|
||||||
print("\n#### Server Commands ####\t\t\t\t\t#### Report Generation ####")
|
|
||||||
print("100) Delete and block a specific media.\t\t\t\t150) Generate user report.")
|
|
||||||
print("101) Purge remote media repository up to a certain date.\t151) Lookup homeserver admin contact details.")
|
|
||||||
print("102) Prepare database for copying events of multiple rooms.\t152) Send a test email (to yourself).")
|
|
||||||
print("\t\t\t\t\t\t\t\t153) Sent a test Matrix message (to yourself).")
|
|
||||||
print("#### rdlist - General ####\t\t\t\t\t154) Send test incident reports (to yourself).")
|
|
||||||
print("120) Block all rooms with specific rdlist tags.")
|
|
||||||
print("121) Get rdlist tags for a room.")
|
|
||||||
print("\n#### rdlist - Recommended Tags ####\nFor rdlist rooms with recommended tags, the following actions are available:")
|
|
||||||
print("130) Collect User Reports on local accounts in rdlist rooms.")
|
|
||||||
print("131) Send Incident Reports on remote accounts in rdlist rooms.")
|
|
||||||
print("132) Block/Purge all rdlist rooms.")
|
|
||||||
print("\n#### ipinfo.io ####")
|
|
||||||
print("140) Analyse a users country of origin.")
|
|
||||||
print("141) Analyse multiple users country of origin.")
|
|
||||||
print("\nPlease enter a number from the above menu, or enter 'q' or 'e' to exit.\n")
|
|
||||||
menu_input = input()
|
|
||||||
if menu_input == "1":
|
|
||||||
erase_data = input("Deactivating account. Do you want to also erase the user's data? (y/n) ")
|
|
||||||
if erase_data in ["y", "Y", "yes", "Yes", "YES"]:
|
|
||||||
user_commands.deactivate_account('',True)
|
|
||||||
elif erase_data in ["n", "N", "no", "No", "NO"]:
|
|
||||||
user_commands.deactivate_account('')
|
|
||||||
else:
|
|
||||||
print("\nIncorrect input detected, please select 'y' or 'n'!\n")
|
|
||||||
elif menu_input == "2":
|
|
||||||
user_commands.deactivate_multiple_accounts()
|
|
||||||
elif menu_input == "3":
|
|
||||||
user_commands.create_account('','')
|
|
||||||
elif menu_input == "4":
|
|
||||||
user_commands.create_multiple_accounts()
|
|
||||||
elif menu_input == "5":
|
|
||||||
user_commands.reset_password('','')
|
|
||||||
elif menu_input == "6":
|
|
||||||
whois_account_dict = user_commands.whois_account('')
|
|
||||||
print(json.dumps(whois_account_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "7":
|
|
||||||
user_commands.whois_multiple_accounts()
|
|
||||||
elif menu_input == "8":
|
|
||||||
query_account_dict = user_commands.query_account()
|
|
||||||
print(json.dumps(query_account_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "9":
|
|
||||||
user_commands.query_multiple_accounts()
|
|
||||||
elif menu_input == "10":
|
|
||||||
joined_rooms_dict = user_commands.list_joined_rooms('')
|
|
||||||
print(json.dumps(joined_rooms_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "11":
|
|
||||||
set_user_server_admin_dict = user_commands.set_user_server_admin('')
|
|
||||||
print(json.dumps(set_user_server_admin_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "12":
|
|
||||||
user_commands.list_accounts()
|
|
||||||
elif menu_input == "13":
|
|
||||||
user_commands.quarantine_users_media()
|
|
||||||
elif menu_input == "14":
|
|
||||||
account_data_dict = user_commands.collect_account_data('')
|
|
||||||
print(json.dumps(account_data_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "15":
|
|
||||||
user_commands.list_account_pushers('')
|
|
||||||
elif menu_input == "16":
|
|
||||||
user_commands.get_rate_limit()
|
|
||||||
elif menu_input == "17":
|
|
||||||
user_commands.set_rate_limit()
|
|
||||||
elif menu_input == "18":
|
|
||||||
user_commands.delete_rate_limit()
|
|
||||||
elif menu_input == "19":
|
|
||||||
user_account_exists = user_commands.check_user_account_exists('')
|
|
||||||
if user_account_exists == True:
|
|
||||||
print("\nUser account exists.\n")
|
|
||||||
elif user_account_exists == False:
|
|
||||||
print("\nUser account does not exist.\n")
|
|
||||||
elif menu_input == "20":
|
|
||||||
shadow_ban_dict = user_commands.shadow_ban_account('')
|
|
||||||
print(json.dumps(shadow_ban_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "50":
|
|
||||||
room_details_dict = room_commands.get_room_details('')
|
|
||||||
print(json.dumps(room_details_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "51":
|
|
||||||
room_members_dict = room_commands.get_room_members('',False)
|
|
||||||
print(json.dumps(room_members_dict, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "52":
|
|
||||||
room_commands.export_room_state('','',True)
|
|
||||||
elif menu_input == "53":
|
|
||||||
room_commands.export_multiple_room_states()
|
|
||||||
elif menu_input == "54":
|
|
||||||
room_commands.public_directory_rooms()
|
|
||||||
elif menu_input == "55":
|
|
||||||
room_commands.remove_room_from_directory('')
|
|
||||||
elif menu_input == "56":
|
|
||||||
room_commands.remove_multiple_rooms_from_directory()
|
|
||||||
elif menu_input == "57":
|
|
||||||
room_commands.redact_room_event()
|
|
||||||
elif menu_input == "58":
|
|
||||||
room_commands.list_and_download_media_in_room('','','','./')
|
|
||||||
elif menu_input == "59":
|
|
||||||
room_commands.download_media_from_multiple_rooms()
|
|
||||||
elif menu_input == "60":
|
|
||||||
room_commands.quarantine_media_in_room()
|
|
||||||
elif menu_input == "61":
|
|
||||||
room_commands.shutdown_room('','','','','','')
|
|
||||||
elif menu_input == "62":
|
|
||||||
room_commands.shutdown_multiple_rooms()
|
|
||||||
elif menu_input == "63":
|
|
||||||
room_commands.delete_room('')
|
|
||||||
elif menu_input == "64":
|
|
||||||
room_commands.delete_multiple_rooms()
|
|
||||||
elif menu_input == "65":
|
|
||||||
room_commands.purge_room_to_timestamp('','')
|
|
||||||
elif menu_input == "66":
|
|
||||||
room_commands.purge_multiple_rooms_to_timestamp()
|
|
||||||
elif menu_input == "67":
|
|
||||||
blocked_status = room_commands.get_block_status('')
|
|
||||||
if blocked_status == True:
|
|
||||||
print("\nRoom is blocked.\n")
|
|
||||||
elif blocked_status == False:
|
|
||||||
print("\nRoom is not blocked.\n")
|
|
||||||
print(json.dumps(blocked_status, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "68":
|
|
||||||
room_commands.set_block_status('', True)
|
|
||||||
elif menu_input == "69":
|
|
||||||
room_commands.set_block_status('', False)
|
|
||||||
elif menu_input == "100":
|
|
||||||
server_commands.delete_block_media()
|
|
||||||
elif menu_input == "101":
|
|
||||||
server_commands.purge_remote_media_repo()
|
|
||||||
elif menu_input == "102":
|
|
||||||
server_commands.prepare_database_copy_of_multiple_rooms()
|
|
||||||
elif menu_input == "120":
|
|
||||||
rdlist_commands.block_all_rooms_with_rdlist_tags(False,'','','')
|
|
||||||
elif menu_input == "121":
|
|
||||||
rdlist_tags = rdlist_commands.get_rdlist_tags('')
|
|
||||||
print(json.dumps(rdlist_tags, indent=4, sort_keys=True))
|
|
||||||
elif menu_input == "130":
|
|
||||||
rdlist_commands.collect_user_reports_on_rdlist_accounts()
|
|
||||||
elif menu_input == "131":
|
|
||||||
rdlist_commands.send_incident_reports_on_rdlist_accounts()
|
|
||||||
elif menu_input == "132":
|
|
||||||
rdlist_commands.block_recommended_rdlist_tags()
|
|
||||||
elif menu_input == "140":
|
|
||||||
ipinfo_commands.analyse_account_ip('')
|
|
||||||
elif menu_input == "141":
|
|
||||||
ipinfo_commands.analyse_multiple_account_ips()
|
|
||||||
elif menu_input == "150":
|
|
||||||
report_commands.generate_user_report('','')
|
|
||||||
elif menu_input == "151":
|
|
||||||
admin_contact_dict, is_whois = report_commands.lookup_homeserver_admin('')
|
|
||||||
print(f"\nAdmin contacts: {json.dumps(admin_contact_dict, indent=4, sort_keys=True)}\nWhois: {str(is_whois)}")
|
|
||||||
elif menu_input == "152":
|
|
||||||
report_commands.test_send_email()
|
|
||||||
elif menu_input == "153":
|
|
||||||
bot_commands.test_matrix_message()
|
|
||||||
elif menu_input == "154":
|
|
||||||
report_commands.test_send_incident_reports()
|
|
||||||
elif menu_input == "q" or menu_input == "Q" or menu_input == "e" or menu_input == "E":
|
|
||||||
print("\nExiting...\n")
|
|
||||||
pass_token = True
|
|
||||||
else:
|
|
||||||
print("\nIncorrect input detected, please select a number from 1 to 154!\n")
|
|
||||||
|
|
695
modtool.py
Executable file
695
modtool.py
Executable file
@@ -0,0 +1,695 @@
|
|||||||
|
|
||||||
|
# modtool.py
|
||||||
|
# an easy moderation tool for matrix/synapse
|
||||||
|
#
|
||||||
|
# created by @PC-Admin:perthchat.org
|
||||||
|
#
|
||||||
|
# This work is licensed under AGPLv3, for more information see: https://www.gnu.org/licenses/agpl-3.0.txt
|
||||||
|
#
|
||||||
|
# To do:
|
||||||
|
# https://github.com/matrix-org/synapse/blob/master/docs/admin_api/delete_group.md
|
||||||
|
# Make the menu prettier!
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# These values can be hard coded for easier usage: #
|
||||||
|
homeserver_url = "matrix.example.org"
|
||||||
|
base_url = "example.org"
|
||||||
|
access_token = ""
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
def parse_username(username):
|
||||||
|
tail_end = ':' + base_url
|
||||||
|
username = username.replace('@','')
|
||||||
|
username = username.replace(tail_end,'')
|
||||||
|
return username
|
||||||
|
|
||||||
|
#def deactivate_account(preset_username):
|
||||||
|
# if len(preset_username) == 0:
|
||||||
|
# username = input("\nPlease enter the username to deactivate: ")
|
||||||
|
# username = parse_username(username)
|
||||||
|
# else:
|
||||||
|
# username = parse_username(preset_username)
|
||||||
|
# command_string = "curl -X POST -H \"Authorization: Bearer " + access_token + "\" 'https://" + homeserver_url + "/_synapse/admin/v1/deactivate/@" + username + ":" + base_url + "' --data '{\"erase\": true}'"
|
||||||
|
# print("\n" + command_string + "\n")
|
||||||
|
# process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
# output = process.stdout
|
||||||
|
# print(output)
|
||||||
|
|
||||||
|
|
||||||
|
def deactivate_account(preset_username):
|
||||||
|
if len(preset_username) == 0:
|
||||||
|
username = input("\nPlease enter the username to deactivate: ")
|
||||||
|
username = parse_username(username)
|
||||||
|
else:
|
||||||
|
username = parse_username(preset_username)
|
||||||
|
deactivate_url = "https://" + homeserver_url + "/_synapse/admin/v1/deactivate/@" + username + ":" + base_url
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
request_data = { "erase": True }
|
||||||
|
deactivate = requests.post(deactivate_url, headers=request_header, json=request_data)
|
||||||
|
print( '\nRequest .status_code: ' + str(deactivate.status_code) )
|
||||||
|
print( 'Request .text: ' + deactivate.text )
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" "https://matrix.perthchat.org/_synapse/admin/v1/deactivate/@billybob:perthchat.org" --data '{"erase": true}'
|
||||||
|
|
||||||
|
#def reset_password():
|
||||||
|
# username = input("\nPlease enter the username for the password reset: ")
|
||||||
|
# password = input("Please enter the password to set: ")
|
||||||
|
# username = parse_username(username)
|
||||||
|
# command_string = "curl -X POST -H 'Content-Type: application/json' -d '{\"new_password\": \"" + password + "\", \"logout_devices\": true}' https://" + homeserver_url + "/_synapse/admin/v1/reset_password/@" + username + ":" + base_url + "?access_token=" + access_token
|
||||||
|
# print("\n" + command_string + "\n")
|
||||||
|
# process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
# output = process.stdout
|
||||||
|
# print(output)
|
||||||
|
|
||||||
|
def reset_password():
|
||||||
|
username = input("\nPlease enter the username for the password reset: ")
|
||||||
|
password = input("Please enter the password to set: ")
|
||||||
|
username = parse_username(username)
|
||||||
|
reset_url = "https://" + homeserver_url + "/_synapse/admin/v1/reset_password/@" + username + ":" + base_url
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
request_data = { "new_password": password, "logout_devices": True }
|
||||||
|
reset_password = requests.post(reset_url, headers=request_header, json=request_data)
|
||||||
|
print( '\nRequest .status_code: ' + str(reset_password.status_code) )
|
||||||
|
print( 'Request .text: ' + reset_password.text )
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -X POST -H 'Content-Type: application/json' -d '{"new_password": "dogpoo", "logout_devices": true}' https://matrix.perthchat.org/_synapse/admin/v1/reset_password/@dogpoo:perthchat.org?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
#def set_user_server_admin():
|
||||||
|
# tried setting 'admin: false' here but it failed and promoted the user instead!
|
||||||
|
# print("\nBe aware that you need to set at least 1 user to server admin already by editing the database in order to use this command. See https://github.com/PC-Admin/PC-Admins-Synapse-Moderation-Tool/blob/master/README.md for details on how to do this.")
|
||||||
|
# username = input("\nPlease enter the username you want to promote to server admin: ")
|
||||||
|
# username = parse_username(username)
|
||||||
|
# passthrough = 0
|
||||||
|
# server_admin_result = "true"
|
||||||
|
|
||||||
|
# command_string = "curl -X PUT -H 'Content-Type: application/json' -d '{\"admin\": \"" + server_admin_result + "\"}' https://" + homeserver_url + "/_synapse/admin/v1/users/@" + username + ":" + base_url + "/admin?access_token=" + access_token
|
||||||
|
# print("\n" + command_string + "\n")
|
||||||
|
# process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
# output = process.stdout
|
||||||
|
# print(output)
|
||||||
|
|
||||||
|
def set_user_server_admin():
|
||||||
|
# tried setting 'admin: false' here but it failed and promoted the user instead!
|
||||||
|
print("\nBe aware that you need to set at least 1 user to server admin already by editing the database in order to use this command. See https://github.com/PC-Admin/PC-Admins-Synapse-Moderation-Tool/blob/master/README.md for details on how to do this.")
|
||||||
|
username = input("\nPlease enter the username you want to promote/demote to/from server admin: ")
|
||||||
|
server_admin_result_raw = input("\nDo you want this user to be a server admin? yes/no? ")
|
||||||
|
username = parse_username(username)
|
||||||
|
if server_admin_result_raw == "y" or server_admin_result_raw == "yes" or server_admin_result_raw == "Y" or server_admin_result_raw == "Yes" or server_admin_result_raw == "YES":
|
||||||
|
request_data = { "admin": "true" }
|
||||||
|
elif server_admin_result_raw == "n" or server_admin_result_raw == "no" or server_admin_result_raw == "N" or server_admin_result_raw == "No" or server_admin_result_raw == "NO":
|
||||||
|
request_data = { "admin": "false" }
|
||||||
|
else:
|
||||||
|
print("Input not recognised, aborting command.")
|
||||||
|
return
|
||||||
|
set_server_admin_url = "https://" + homeserver_url + "/_synapse/admin/v1/users/@" + username + ":" + base_url + "/admin"
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
set_server_admin = requests.post(set_server_admin_url, headers=request_header, json=request_data)
|
||||||
|
print( '\nRequest .status_code: ' + str(set_server_admin.status_code) )
|
||||||
|
print( 'Request .text: ' + set_server_admin.text )
|
||||||
|
|
||||||
|
#### ^ THIS ISN'T WORKING ####
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -kX POST -H 'Content-Type: application/json' -d '{"admin": "true"}' https://matrix.perthchat.org/_synapse/admin/v2/users/@dogpoo:perthchat.org?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def query_account(preset_username):
|
||||||
|
if preset_username == '':
|
||||||
|
username = input("\nPlease enter the username you wish to query: ")
|
||||||
|
elif preset_username != '':
|
||||||
|
username = preset_username
|
||||||
|
username = parse_username(username)
|
||||||
|
query_account_url = "https://" + homeserver_url + "/_matrix/client/r0/admin/whois/@" + username + ":" + base_url
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
query_account = requests.get(query_account_url, headers=request_header)
|
||||||
|
print( '\nRequest .status_code: ' + str(query_account.status_code) )
|
||||||
|
print( 'Request .text: ' + query_account.text )
|
||||||
|
|
||||||
|
# command_string = "curl -kXGET https://" + homeserver_url + "/_matrix/client/r0/admin/whois/@" + username + ":" + base_url + "?access_token=" + access_token
|
||||||
|
#print("\n" + command_string + "\n")
|
||||||
|
# process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
# output = process.stdout
|
||||||
|
# print(output + "\n")
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -kXGET https://matrix.perthchat.org/_matrix/client/r0/admin/whois/@PC-Admin:perthchat.org?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def list_joined_rooms(preset_username):
|
||||||
|
if preset_username == '':
|
||||||
|
username = input("\nPlease enter the username you wish to query: ")
|
||||||
|
elif preset_username != '':
|
||||||
|
username = preset_username
|
||||||
|
username = parse_username(username)
|
||||||
|
list_joined_rooms_url = "https://" + homeserver_url + "/_synapse/admin/v1/users/@" + username + ":" + base_url + "/joined_rooms"
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
list_joined_rooms = requests.get(list_joined_rooms_url, headers=request_header)
|
||||||
|
print( '\nRequest .status_code: ' + str(list_joined_rooms.status_code) )
|
||||||
|
print( 'Request .text: ' + list_joined_rooms.text )
|
||||||
|
|
||||||
|
# command_string = "curl -kXGET https://" + homeserver_url + "/_synapse/admin/v1/users/@" + username + ":" + base_url + "/joined_rooms?access_token=" + access_token
|
||||||
|
#print("\n" + command_string + "\n")
|
||||||
|
# process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
# output = process.stdout
|
||||||
|
# print(output + "\n")
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -kXGET https://matrix.perthchat.org/_synapse/admin/v1/users/@PC-Admin:perthchat.org/joined_rooms?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def query_multiple_accounts():
|
||||||
|
print("Query multiple user accounts selected")
|
||||||
|
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of Matrix usernames: ")
|
||||||
|
with open(user_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
print(len(data))
|
||||||
|
query_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to query all of these users? y/n?\n")
|
||||||
|
if query_confirmation == "y" or query_confirmation == "Y" or query_confirmation == "yes" or query_confirmation == "Yes":
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
print(data[x][0])
|
||||||
|
query_account(data[x][0])
|
||||||
|
list_joined_rooms(data[x][0])
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(1)
|
||||||
|
if query_confirmation == "n" or query_confirmation == "N" or query_confirmation == "no" or query_confirmation == "No":
|
||||||
|
print("\nExiting...\n")
|
||||||
|
|
||||||
|
def list_accounts():
|
||||||
|
deactivated_choice = input("Do you want to include deactivated accounts y/n? ")
|
||||||
|
guest_choice = input("Do you want to include guest accounts y/n? ")
|
||||||
|
|
||||||
|
if deactivated_choice == "y" or deactivated_choice == "Y" or deactivated_choice == "yes" or deactivated_choice == "Yes":
|
||||||
|
deactivated_string = "&deactivated=true"
|
||||||
|
elif deactivated_choice == "n" or deactivated_choice == "N" or deactivated_choice == "no" or deactivated_choice == "No":
|
||||||
|
deactivated_string = "&deactivated=false"
|
||||||
|
else:
|
||||||
|
print("Input invalid! Defaulting to false.")
|
||||||
|
deactivated_string = "&deactivated=false"
|
||||||
|
|
||||||
|
if guest_choice == "y" or guest_choice == "Y" or guest_choice == "yes" or guest_choice == "Yes":
|
||||||
|
guest_string = "&guests=true"
|
||||||
|
elif guest_choice == "n" or guest_choice == "N" or guest_choice == "no" or guest_choice == "No":
|
||||||
|
guest_string = "&guests=false"
|
||||||
|
else:
|
||||||
|
print("Input invalid! Defaulting to false.")
|
||||||
|
guest_string = "&guests=false"
|
||||||
|
|
||||||
|
list_accounts_url = "https://" + homeserver_url + "/_synapse/admin/v2/users?from=0" + guest_string + deactivated_string
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
#request_data = { "guest": guest_string, "deactivate": deactivated_string }
|
||||||
|
list_accounts = requests.get(list_accounts_url, headers=request_header)
|
||||||
|
print( '\nRequest .status_code: ' + str(list_accounts.status_code) )
|
||||||
|
print( 'Request .text: ' + list_accounts.text )
|
||||||
|
|
||||||
|
# command_string = "curl -kXGET \"https://" + homeserver_url + "/_synapse/admin/v2/users?from=0&limit=1000000&" + guest_string + "&" + deactivated_string + "&access_token=" + access_token + "\""
|
||||||
|
# print("\n" + command_string + "\n")
|
||||||
|
# process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
# output = process.stdout
|
||||||
|
list_accounts_json = list_accounts.json()
|
||||||
|
number_of_users = list_accounts_json['total']
|
||||||
|
#
|
||||||
|
print("\nTotal amount of users: " + str(number_of_users))
|
||||||
|
if number_of_users < 100:
|
||||||
|
print(output)
|
||||||
|
elif number_of_users >= 100:
|
||||||
|
accounts_output_file = input("\nThere are too many users to list here, please specify a filename to print this data too: ")
|
||||||
|
f = open(accounts_output_file, "a")
|
||||||
|
for i in range(0, number_of_users, 100):
|
||||||
|
list_accounts_url = "https://" + homeserver_url + "/_synapse/admin/v2/users?from=" + str(i) + guest_string + deactivated_string
|
||||||
|
#request_data = { "guest": guest_string, "deactivate": deactivated_string }
|
||||||
|
list_accounts = requests.get(list_accounts_url, headers=request_header)
|
||||||
|
f.write("\n")
|
||||||
|
f.write(list_accounts.text)
|
||||||
|
f.close()
|
||||||
|
print("Done!\n")
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -kXGET "https://matrix.perthchat.org/_synapse/admin/v2/users?from=0&limit=10&guests=false&access_token=ACCESS_TOKEN"
|
||||||
|
|
||||||
|
def create_account(preset_username,preset_password):
|
||||||
|
if len(preset_username) == 0 and len(preset_password) == 0:
|
||||||
|
username = input("\nPlease enter the username to create: ")
|
||||||
|
username = parse_username(username)
|
||||||
|
user_password = input("Please enter the password for this account: ")
|
||||||
|
elif len(preset_username) > 0 and len(preset_password) > 0:
|
||||||
|
username = parse_username(preset_username)
|
||||||
|
user_password = preset_password
|
||||||
|
else:
|
||||||
|
print("\nError with user/pass file data, skipping...\n")
|
||||||
|
|
||||||
|
create_account_url = "https://" + homeserver_url + "/_synapse/admin/v2/users/@" + username + ":" + base_url
|
||||||
|
request_header = { "Authorization": 'Bearer ' + access_token }
|
||||||
|
request_data = { "password": user_password ,"admin": False, "deactivated": False }
|
||||||
|
create_account = requests.get(create_account_url, headers=request_header, data=request_data)
|
||||||
|
print( '\nRequest .status_code: ' + str(create_account.status_code) )
|
||||||
|
print( 'Request .text: ' + create_account.text )
|
||||||
|
#command_string = "curl -kX PUT -H 'Content-Type: application/json' -d '{\"password\": \"" + user_password + "\"}' https://" + homeserver_url + "/_synapse/admin/v2/users/@" + username + ":" + base_url + "?access_token=" + access_token
|
||||||
|
#print("\n" + command_string + "\n")
|
||||||
|
#process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
#output = process.stdout
|
||||||
|
#print(output)
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -kX PUT -H 'Content-Type: application/json' -d '{"password": "user_password","admin": false,"deactivated": false}' https://matrix.perthchat.org/_synapse/admin/v2/users/@billybob:perthchat.org?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def create_multiple_accounts():
|
||||||
|
print("Create multiple user accounts selected")
|
||||||
|
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of Matrix usernames: ")
|
||||||
|
with open(user_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
print(len(data))
|
||||||
|
create_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to create these users? y/n?\n")
|
||||||
|
if create_confirmation == "y" or create_confirmation == "Y" or create_confirmation == "yes" or create_confirmation == "Yes":
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
print(data[x][0])
|
||||||
|
create_account(data[x][0],data[x][1])
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(10)
|
||||||
|
if create_confirmation == "n" or create_confirmation == "N" or create_confirmation == "no" or create_confirmation == "No":
|
||||||
|
print("\nExiting...\n")
|
||||||
|
|
||||||
|
def deactivate_multiple_accounts():
|
||||||
|
print("Deactivate multiple user accounts selected")
|
||||||
|
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of names: ")
|
||||||
|
with open(user_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
delete_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to deactivate these users? y/n?\n")
|
||||||
|
#print(len(data[0]))
|
||||||
|
#print(data[0][0])
|
||||||
|
if delete_confirmation == "y" or delete_confirmation == "Y" or delete_confirmation == "yes" or delete_confirmation == "Yes":
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
#print(data[0][x])
|
||||||
|
deactivate_account(data[x][0])
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(10)
|
||||||
|
if delete_confirmation == "n" or delete_confirmation == "N" or delete_confirmation == "no" or delete_confirmation == "No":
|
||||||
|
print("\nExiting...\n")
|
||||||
|
|
||||||
|
def list_room_details(preset_internal_ID):
|
||||||
|
if preset_internal_ID == '':
|
||||||
|
internal_ID = input("\nEnter the internal id of the room you wish to query (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
||||||
|
elif preset_internal_ID != '':
|
||||||
|
internal_ID = preset_internal_ID
|
||||||
|
command_string = "curl -kXGET 'https://" + homeserver_url + "/_synapse/admin/v1/rooms/" + internal_ID + "?access_token=" + access_token + "'"
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
output = process.stdout
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# $ curl -kXGET 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!OeqILBxiHahidSQQoC:matrix.org?access_token=ACCESS_TOKEN'
|
||||||
|
|
||||||
|
def list_directory_rooms():
|
||||||
|
command_string = "curl -kXGET https://" + homeserver_url + "/_matrix/client/r0/publicRooms?access_token=" + access_token
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
output = process.stdout
|
||||||
|
output = output.replace('\"room_id\":\"','\n')
|
||||||
|
output = output.replace('\",\"name','\n\",\"name')
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# $ curl -kXGET https://matrix.perthchat.org/_matrix/client/r0/publicRooms?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def remove_room_from_directory(preset_internal_ID):
|
||||||
|
if preset_internal_ID == '':
|
||||||
|
internal_ID = input("\nEnter the internal id of the room you wish to remove from the directory (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
||||||
|
elif preset_internal_ID != '':
|
||||||
|
internal_ID = preset_internal_ID
|
||||||
|
command_string = "curl -kX PUT -H \'Content-Type: application/json\' -d \'{\"visibility\": \"private\"}\' \'https://" + homeserver_url + "/_matrix/client/r0/directory/list/room/" + internal_ID + "?access_token=" + access_token + "\'"
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
output = process.stdout
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# $ curl -kX PUT -H 'Content-Type: application/json' -d '{"visibility": "private"}' 'https://matrix.perthchat.org/_matrix/client/r0/directory/list/room/!DwUPBvNapIVecNllgt:perthchat.org?access_token=ACCESS_TOKEN'
|
||||||
|
|
||||||
|
def remove_multiple_rooms_from_directory():
|
||||||
|
print("Remove multiple rooms from directory selected")
|
||||||
|
purge_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
||||||
|
with open(purge_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
print(data[x][0])
|
||||||
|
remove_room_from_directory(data[x][0])
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def list_and_download_media_in_room(preset_internal_ID,preset_print_file_list_choice,preset_download_files_choice,base_directory):
|
||||||
|
if preset_internal_ID == '':
|
||||||
|
internal_ID = input("\nEnter the internal id of the room you want to get a list of media for (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
||||||
|
elif preset_internal_ID != '':
|
||||||
|
internal_ID = preset_internal_ID
|
||||||
|
command_string = "curl -kXGET https://" + homeserver_url + "/_synapse/admin/v1/room/" + internal_ID + "/media?access_token=" + access_token
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
media_list_output = process.stdout
|
||||||
|
#print("Full media list:\n" + media_list_output)
|
||||||
|
|
||||||
|
if preset_print_file_list_choice == '':
|
||||||
|
print_file_list_choice = input("\n Do you want to write this list to a file? y/n? ")
|
||||||
|
elif preset_print_file_list_choice != '':
|
||||||
|
print_file_list_choice = preset_print_file_list_choice
|
||||||
|
|
||||||
|
if print_file_list_choice == "y" or print_file_list_choice == "Y" or print_file_list_choice == "yes" or print_file_list_choice == "Yes":
|
||||||
|
print_file_list_choice = "true"
|
||||||
|
elif print_file_list_choice == "n" or print_file_list_choice == "N" or print_file_list_choice == "no" or print_file_list_choice == "No":
|
||||||
|
print_file_list_choice = "false"
|
||||||
|
else:
|
||||||
|
print("Input invalid! Defaulting to 'No'.")
|
||||||
|
print_file_list_choice = "false"
|
||||||
|
|
||||||
|
room_dir = "./" + internal_ID
|
||||||
|
room_dir = room_dir.replace('!', '')
|
||||||
|
room_dir = room_dir.replace(':', '-')
|
||||||
|
os.mkdir(room_dir)
|
||||||
|
os.chdir(room_dir)
|
||||||
|
|
||||||
|
if print_file_list_choice == "true":
|
||||||
|
media_list_filename_location = "./media_list.txt"
|
||||||
|
media_list_filename = open(media_list_filename_location,"w+")
|
||||||
|
media_list_filename.write(media_list_output)
|
||||||
|
media_list_filename.close()
|
||||||
|
|
||||||
|
if preset_download_files_choice == '':
|
||||||
|
download_files_choice = input("\n Do you also want to download a copy of these media files? y/n? ")
|
||||||
|
if preset_download_files_choice != '':
|
||||||
|
download_files_choice = preset_download_files_choice
|
||||||
|
|
||||||
|
if download_files_choice == "y" or download_files_choice == "Y" or download_files_choice == "yes" or download_files_choice == "Yes":
|
||||||
|
download_files_choice = "true"
|
||||||
|
elif download_files_choice == "n" or download_files_choice == "N" or download_files_choice == "no" or download_files_choice == "No":
|
||||||
|
download_files_choice = "false"
|
||||||
|
else:
|
||||||
|
print("Input invalid! Defaulting to 'No'.")
|
||||||
|
download_files_choice = "false"
|
||||||
|
|
||||||
|
if download_files_choice == "true":
|
||||||
|
media_list_output = media_list_output.split('\"')
|
||||||
|
#print("New media list:\n" + str(media_list_output))
|
||||||
|
os.mkdir("./media-files")
|
||||||
|
os.chdir("./media-files")
|
||||||
|
count = 0
|
||||||
|
# Strips the newline character
|
||||||
|
for line in media_list_output:
|
||||||
|
if "mxc" in line:
|
||||||
|
#print("Line is 1: \n\n" + line + "\n")
|
||||||
|
line = line.replace('mxc://','')
|
||||||
|
download_command = "wget https://" + homeserver_url + "/_matrix/media/r0/download/" + line
|
||||||
|
print(download_command)
|
||||||
|
download_process = subprocess.run([download_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
os.chdir(base_directory)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# $ curl -kXGET https://matrix.perthchat.org/_synapse/admin/v1/room/<room_id>/media?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
# To access via web:
|
||||||
|
# https://matrix.perthchat.org/_matrix/media/r0/download/ + server_name + "/" + media_id
|
||||||
|
|
||||||
|
def download_media_from_multiple_rooms():
|
||||||
|
print("Download media from multiple rooms selected")
|
||||||
|
download_media_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
||||||
|
with open(download_media_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
preset_print_file_list_choice = input("\n Do you want to print list files of all the media in these rooms? y/n? ")
|
||||||
|
preset_download_files_choice = input("\n Do you want to download all the media in these rooms? y/n? ")
|
||||||
|
|
||||||
|
os.mkdir("./media_download")
|
||||||
|
os.chdir("./media_download")
|
||||||
|
|
||||||
|
pwd_process = subprocess.run(["pwd"], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
base_directory = pwd_process.stdout
|
||||||
|
base_directory = base_directory.replace('\n','')
|
||||||
|
print(base_directory)
|
||||||
|
|
||||||
|
print("Beginning download of media from all rooms in list...")
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
print(data[x][0])
|
||||||
|
list_and_download_media_in_room(data[x][0],preset_print_file_list_choice,preset_download_files_choice,base_directory)
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def quarantine_media_in_room():
|
||||||
|
internal_ID = input("\nEnter the internal id of the room you want to quarantine, this makes local and remote data inaccessible (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
||||||
|
command_string = "curl -X POST \'https://" + homeserver_url + "/_synapse/admin/v1/room/" + internal_ID + "/media/quarantine?access_token=" + access_token + "\'"
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
output = process.stdout
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Example
|
||||||
|
# $ curl -X POST 'https://matrix.perthchat.org/_synapse/admin/v1/room/!DwUPBvNapIVecNllgt:perthchat.org/media/quarantine?access_token=ACCESS_TOKEN'
|
||||||
|
|
||||||
|
def quarantine_users_media():
|
||||||
|
username = input("\nPlease enter the username of the user who's media you want to quarantine: ")
|
||||||
|
username = parse_username(username)
|
||||||
|
command_string = "curl -X POST \'https://" + homeserver_url + "/_synapse/admin/v1/user/@" + username + ":" + base_url + "/media/quarantine?access_token=" + access_token + "\'"
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
output = process.stdout
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -X POST https://matrix.perthchat.org/_synapse/admin/v1/user/@PC-Admin:perthchat.org/media/quarantine?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def purge_room(preset_internal_ID,preset_user_ID,preset_new_room_name,preset_message,preset_purge_choice,preset_block_choice):
|
||||||
|
if preset_internal_ID == '':
|
||||||
|
internal_ID = input("\nEnter the internal id of the room you want purge (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
||||||
|
elif preset_internal_ID != '':
|
||||||
|
internal_ID = preset_internal_ID
|
||||||
|
if preset_user_ID == '':
|
||||||
|
user_ID = input("\nPlease enter the local username that will create a 'muted violation room' for your users: ")
|
||||||
|
elif preset_user_ID != '':
|
||||||
|
user_ID = preset_user_ID
|
||||||
|
if preset_new_room_name == '':
|
||||||
|
new_room_name = input("\nPlease enter the room name of the muted violation room your users will be sent to: ")
|
||||||
|
elif preset_new_room_name != '':
|
||||||
|
new_room_name = preset_new_room_name
|
||||||
|
if preset_message == '':
|
||||||
|
message = input("\nPlease enter the shutdown message that will be displayed to users: ")
|
||||||
|
elif preset_message != '':
|
||||||
|
message = preset_message
|
||||||
|
if preset_purge_choice == '':
|
||||||
|
purge_choice = input("\n Do you want to purge the room? (This deletes all the room history from your database.) y/n? ")
|
||||||
|
elif preset_purge_choice != '':
|
||||||
|
purge_choice = preset_purge_choice
|
||||||
|
if preset_block_choice == '':
|
||||||
|
block_choice = input("\n Do you want to block the room? (This prevents your server users re-entering the room.) y/n? ")
|
||||||
|
elif preset_block_choice != '':
|
||||||
|
block_choice = preset_block_choice
|
||||||
|
|
||||||
|
username = parse_username(user_ID)
|
||||||
|
|
||||||
|
if purge_choice == "y" or purge_choice == "Y" or purge_choice == "yes" or purge_choice == "Yes":
|
||||||
|
purge_choice = "true"
|
||||||
|
elif purge_choice == "n" or purge_choice == "N" or purge_choice == "no" or purge_choice == "No":
|
||||||
|
purge_choice = "false"
|
||||||
|
else:
|
||||||
|
print("Input invalid! exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if block_choice == "y" or block_choice == "Y" or block_choice == "yes" or block_choice == "Yes":
|
||||||
|
block_choice = "true"
|
||||||
|
elif block_choice == "n" or block_choice == "N" or block_choice == "no" or block_choice == "No":
|
||||||
|
block_choice = "false"
|
||||||
|
else:
|
||||||
|
print("Input invalid! exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
command_string = "curl -X POST -H 'user-agent: anything' -d '{\"new_room_user_id\": \"@" + username + ":" + base_url + "\",\"room_name\": \"" + new_room_name + "\",\"message\": \"" + message + "\",\"block\": " + block_choice + ",\"purge\": " + purge_choice + "}' \'https://" + homeserver_url + "/_synapse/admin/v1/rooms/" + internal_ID + "/delete?access_token=" + access_token + "\'"
|
||||||
|
print("\n" + command_string + "\n")
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
output = process.stdout
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -X POST -H 'Content-Type: application/json' -d '{"new_room_user_id": "@PC-Admin:perthchat.org","room_name": "VIOLATION ROOM 2","message": "You have been very naughty!","block": true,"purge": true}' 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!mPfaFTrXqUJsgrldwu:perthchat.org/delete?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def purge_multiple_rooms():
|
||||||
|
print("Purge multiple rooms selected")
|
||||||
|
purge_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
||||||
|
with open(purge_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
preset_user_ID = input("\nPlease enter the local username that will create a 'muted violation room' for your users: ")
|
||||||
|
preset_new_room_name = input("\nPlease enter the room name of the muted violation room your users will be sent to: ")
|
||||||
|
preset_message = input("\nPlease enter the shutdown message that will be displayed to users: ")
|
||||||
|
preset_purge_choice = input("\n Do you want to purge these rooms? (This deletes all the room history from your database.) y/n? ")
|
||||||
|
preset_block_choice = input("\n Do you want to block these rooms? (This prevents your server users re-entering the room.) y/n? ")
|
||||||
|
purge_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to purge and block these rooms? y/n?\n")
|
||||||
|
#print(len(data[0]))
|
||||||
|
#print(data[0][0])
|
||||||
|
if purge_confirmation == "y" or purge_confirmation == "Y" or purge_confirmation == "yes" or purge_confirmation == "Yes":
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
print(data[x][0])
|
||||||
|
purge_room(data[x][0],preset_user_ID,preset_new_room_name,preset_message,preset_purge_choice,preset_block_choice)
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if purge_confirmation == "n" or purge_confirmation == "N" or purge_confirmation == "no" or purge_confirmation == "No":
|
||||||
|
print("\nExiting...\n")
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ curl -X POST -H 'Content-Type: application/json' -d '{"new_room_user_id": "@PC-Admin:perthchat.org","room_name": "VIOLATION ROOM 2","message": "You have been very naughty!","block": true,"purge": true}' 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!mPfaFTrXqUJsgrldwu:perthchat.org/delete?access_token=ACCESS_TOKEN
|
||||||
|
|
||||||
|
def purge_remote_media_repo():
|
||||||
|
purge_from = input("\nEnter the number of days to purge from: ")
|
||||||
|
purge_too = input("\nEnter the number of days to purge too: ")
|
||||||
|
|
||||||
|
while int(purge_from) >= int(purge_too):
|
||||||
|
epoche_command = "date --date '" + str(purge_from) + " days ago' +%s"
|
||||||
|
print(epoche_command)
|
||||||
|
epoche_time_process = subprocess.run([epoche_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
print(epoche_time_process.stdout)
|
||||||
|
epoche_time = epoche_time_process.stdout
|
||||||
|
epoche_time_stripped = epoche_time.replace("\n", "")
|
||||||
|
command_string = "curl -X POST --header \"Authorization: Bearer " + access_token + "\" 'https://" + homeserver_url + "/_synapse/admin/v1/purge_media_cache?before_ts=" + epoche_time_stripped + "000'"
|
||||||
|
print(command_string)
|
||||||
|
process = subprocess.run([command_string], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
print(process.stdout)
|
||||||
|
purge_from = int(purge_from) - 1
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# This loop is quite slow, our server was having disk issues.
|
||||||
|
print("Done! :)")
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# $ date --date '149 days ago' +%s
|
||||||
|
# 1589442217
|
||||||
|
# $ curl -X POST --header "Authorization: Bearer ACCESS_TOKEN" 'https://matrix.perthchat.org/_synapse/admin/v1/purge_media_cache?before_ts=1589439628000'
|
||||||
|
|
||||||
|
def prepare_database_copy_of_multiple_rooms():
|
||||||
|
print("Preparing database copying of events from multiple rooms selected\n")
|
||||||
|
print("This command needs to be run on the target server as root, it will setup postgres commands to download the join-leave events and all-events from a list of rooms.\n\nIt mounts a ramdisk beforehand at /matrix/postgres/data/ramdisk\n\nThis function is only compatible with Spantaleevs Matrix deploy script: https://github.com/spantaleev/matrix-docker-ansible-deploy\n")
|
||||||
|
database_copy_list_location = input("Please enter the path of the file containing a newline seperated list of room ids: ")
|
||||||
|
with open(database_copy_list_location, newline='') as f:
|
||||||
|
reader = csv.reader(f)
|
||||||
|
data = list(reader)
|
||||||
|
|
||||||
|
make_ramdisk_command = "mkdir /matrix/postgres/data/ramdisk; mount -t ramfs -o size=512m ramfs /matrix/postgres/data/ramdisk; chown -R matrix:matrix /matrix/postgres/data/ramdisk"
|
||||||
|
make_ramdisk_command_process = subprocess.run([make_ramdisk_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
print(make_ramdisk_command_process.stdout)
|
||||||
|
|
||||||
|
x = 0
|
||||||
|
while x <= (len(data) - 1):
|
||||||
|
print(data[x][0])
|
||||||
|
roomid_trimmed = data[x][0]
|
||||||
|
roomid_trimmed = roomid_trimmed.replace('!', '')
|
||||||
|
roomid_trimmed = roomid_trimmed.replace(':', '-')
|
||||||
|
os.mkdir("/matrix/postgres/data/ramdisk/" + roomid_trimmed)
|
||||||
|
touch_command = "touch /matrix/postgres/data/ramdisk/" + roomid_trimmed + "/dump_room_data.sql"
|
||||||
|
touch_command_process = subprocess.run([touch_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
print(touch_command_process.stdout)
|
||||||
|
sql_file_contents = "\set ROOMID '" + data[x][0] + "'\nCOPY (SELECT * FROM current_state_events JOIN room_memberships ON room_memberships.event_id = current_state_events.event_id WHERE current_state_events.room_id = :'ROOMID') TO '/var/lib/postgresql/data/ramdisk/" + roomid_trimmed + "/user_join-leave.csv' WITH CSV HEADER;\nCOPY (SELECT * FROM event_json WHERE room_id=:'ROOMID') TO '/var/lib/postgresql/data/ramdisk/" + roomid_trimmed + "/room_events.csv' WITH CSV HEADER;"
|
||||||
|
print(sql_file_contents)
|
||||||
|
sql_file_location = "/matrix/postgres/data/ramdisk/" + roomid_trimmed + "/dump_room_data.sql"
|
||||||
|
sql_file = open(sql_file_location,"w+")
|
||||||
|
sql_file.write(sql_file_contents)
|
||||||
|
sql_file.close()
|
||||||
|
|
||||||
|
x += 1
|
||||||
|
#print(x)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
chown_command = "chown -R matrix:matrix /matrix/postgres/data/ramdisk; docker restart matrix-postgres"
|
||||||
|
chown_command_process = subprocess.run([chown_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
print(chown_command_process.stdout)
|
||||||
|
|
||||||
|
print("\nThe sql query files have been generated, as postgres user in container run:\n# docker exec -it matrix-postgres /bin/bash\nbash-5.0$ export PGPASSWORD=your-db-password\nbash-5.0$ for f in /var/lib/postgresql/data/ramdisk/*/dump_room_data.sql; do psql --host=127.0.0.1 --port=5432 --username=synapse -w -f $f; done\n\nAfter copying the data to a cloud location law enforcement can access, clean up the ramdisk like so:\n# rm -r /matrix/postgres/data/ramdisk/*\n# umount /matrix/postgres/data/ramdisk")
|
||||||
|
|
||||||
|
|
||||||
|
# check if homeserver url is hard coded, if not set it
|
||||||
|
|
||||||
|
if homeserver_url == "matrix.example.org":
|
||||||
|
homeserver_url = input("What is the URL of your server? Eg: matrix.example.org ")
|
||||||
|
|
||||||
|
# check if base url is hard coded, if not set it
|
||||||
|
|
||||||
|
if base_url == "example.org":
|
||||||
|
base_url = input("What is the URL of your server? Eg: example.org ")
|
||||||
|
|
||||||
|
# check if access token is hard coded, if not set it
|
||||||
|
|
||||||
|
length_access_token = len(access_token)
|
||||||
|
|
||||||
|
if length_access_token == 0:
|
||||||
|
access_token = input("Please enter access token for server admin account: ")
|
||||||
|
|
||||||
|
# loop menu for various moderation actions
|
||||||
|
|
||||||
|
pass_token = False
|
||||||
|
while pass_token == False:
|
||||||
|
menu_input = input('\nPlease select one of the following options:\n#### User Account Commands ####\n1) Deactivate a user account.\n2) Create a user account.\n3) Query user account.\n4) List room memberships of user.\n5) Query multiple user accounts.\n6) Reset a users password.\n7) Promote/Demote a user to/from server admin.\n8) List all user accounts.\n9) Create multiple user accounts.\n10) Deactivate multiple user accounts.\n11) Quarantine all media a users uploaded.\n#### Room Commands ####\n12) List details of a room.\n13) List rooms in public directory.\n14) Remove a room from the public directory.\n15) Remove multiple rooms from the public directory.\n16) List/Download all media in a room.\n17) Download media from multiple rooms.\n18) Quarantine all media in a room.\n19) Purge a room.\n20) Purge multiple rooms.\n#### Server Commands ####\n21) Purge remote media repository up to a certain date.\n22) Prepare database for copying events of multiple rooms.\n(\'q\' or \'e\') Exit.\n\n')
|
||||||
|
if menu_input == "1":
|
||||||
|
deactivate_account('')
|
||||||
|
elif menu_input == "2":
|
||||||
|
create_account('','')
|
||||||
|
elif menu_input == "3":
|
||||||
|
query_account('')
|
||||||
|
elif menu_input == "4":
|
||||||
|
list_joined_rooms('')
|
||||||
|
elif menu_input == "5":
|
||||||
|
query_multiple_accounts()
|
||||||
|
elif menu_input == "6":
|
||||||
|
reset_password()
|
||||||
|
elif menu_input == "7":
|
||||||
|
set_user_server_admin()
|
||||||
|
elif menu_input == "8":
|
||||||
|
list_accounts()
|
||||||
|
elif menu_input == "9":
|
||||||
|
create_multiple_accounts()
|
||||||
|
elif menu_input == "10":
|
||||||
|
deactivate_multiple_accounts()
|
||||||
|
elif menu_input == "11":
|
||||||
|
quarantine_users_media()
|
||||||
|
elif menu_input == "12":
|
||||||
|
list_room_details('')
|
||||||
|
elif menu_input == "13":
|
||||||
|
list_directory_rooms()
|
||||||
|
elif menu_input == "14":
|
||||||
|
remove_room_from_directory('')
|
||||||
|
elif menu_input == "15":
|
||||||
|
remove_multiple_rooms_from_directory()
|
||||||
|
elif menu_input == "16":
|
||||||
|
list_and_download_media_in_room('','','','./')
|
||||||
|
elif menu_input == "17":
|
||||||
|
download_media_from_multiple_rooms()
|
||||||
|
elif menu_input == "18":
|
||||||
|
quarantine_media_in_room()
|
||||||
|
elif menu_input == "19":
|
||||||
|
purge_room('','','','','','')
|
||||||
|
elif menu_input == "20":
|
||||||
|
purge_multiple_rooms()
|
||||||
|
elif menu_input == "21":
|
||||||
|
purge_remote_media_repo()
|
||||||
|
elif menu_input == "22":
|
||||||
|
prepare_database_copy_of_multiple_rooms()
|
||||||
|
elif menu_input == "q" or menu_input == "Q" or menu_input == "e" or menu_input == "E":
|
||||||
|
print("\nExiting...\n")
|
||||||
|
pass_token = True
|
||||||
|
else:
|
||||||
|
print("\nIncorrect input detected, please select a number from 1 to 22!\n")
|
@@ -1,313 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import asyncio
|
|
||||||
import user_commands
|
|
||||||
import room_commands
|
|
||||||
import report_commands
|
|
||||||
import hardcoded_variables
|
|
||||||
|
|
||||||
def testing_mode_warning():
|
|
||||||
print("\nWARNING! Testing mode is enabled, this will:\n\n- Reduce the amount of data collected in user reports.\n- Slow down rdlist blocking/purging.\n- Prevent the deactivation of accounts.\n- Send incident reports to yourself instead of other homeserver admins.\n")
|
|
||||||
|
|
||||||
def sync_rdlist():
|
|
||||||
rdlist_dir = "./rdlist"
|
|
||||||
os.makedirs(rdlist_dir, exist_ok=True)
|
|
||||||
# Check if the rdlist repo has already been cloned
|
|
||||||
if os.path.isdir("./rdlist/.git"):
|
|
||||||
print("\nrdlist repo already cloned...")
|
|
||||||
os.chdir("./rdlist/")
|
|
||||||
# Update git remote references and get status
|
|
||||||
subprocess.run(["git", "remote", "update"], check=True)
|
|
||||||
status = subprocess.run(["git", "status", "-uno"], stdout=subprocess.PIPE, check=True)
|
|
||||||
os.chdir("..")
|
|
||||||
|
|
||||||
# If "Your branch is up to date" is not in the status, then there are changes to pull
|
|
||||||
if "Your branch is up to date" not in status.stdout.decode():
|
|
||||||
print("Pulling latest changes from rdlist repo...")
|
|
||||||
os.chdir("./rdlist/")
|
|
||||||
# Avoid pulling changes if testing mode is enabled
|
|
||||||
if hardcoded_variables.testing_mode == False:
|
|
||||||
subprocess.run(["git", "pull"], check=True)
|
|
||||||
os.chdir("..")
|
|
||||||
else:
|
|
||||||
print("rdlist repo is up-to-date, no need to pull changes.")
|
|
||||||
|
|
||||||
else:
|
|
||||||
print("Cloning rdlist repo...")
|
|
||||||
subprocess.run(["git", "clone", "ssh://gitea@code.glowers.club:1488/loj/rdlist.git"], check=True)
|
|
||||||
|
|
||||||
# A function to return the rdlist tags associated with a room
|
|
||||||
def get_rdlist_tags(preset_internal_ID):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you wish to query (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
elif preset_internal_ID != '':
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
|
|
||||||
# Git clone the rdlist repo to ./rdlist/
|
|
||||||
sync_rdlist()
|
|
||||||
|
|
||||||
# Load the summaries JSON file
|
|
||||||
summaries_path = os.path.join("rdlist", "dist", "summaries.json")
|
|
||||||
with open(summaries_path, 'r') as file:
|
|
||||||
data = json.load(file)
|
|
||||||
|
|
||||||
# Find the room with the given id and return its tags
|
|
||||||
for item in data:
|
|
||||||
if 'room' in item and 'room_id' in item['room'] and item['room']['room_id'] == internal_ID:
|
|
||||||
if 'report_info' in item and 'tags' in item['report_info']:
|
|
||||||
return item['report_info']['tags']
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_key_rdlist_info(rdlist_tags):
|
|
||||||
# Load the summaries JSON file
|
|
||||||
summaries_path = os.path.join("rdlist", "dist", "summaries.json")
|
|
||||||
with open(summaries_path, 'r') as file:
|
|
||||||
data = json.load(file)
|
|
||||||
|
|
||||||
# Create an empty dictionary to store all the room_ids for each user
|
|
||||||
all_local_users = dict()
|
|
||||||
all_remote_users = dict()
|
|
||||||
|
|
||||||
# Create a set to store all room_ids
|
|
||||||
all_room_ids = set()
|
|
||||||
|
|
||||||
# Create a dictionary to store the tags for each room
|
|
||||||
room_tags = dict()
|
|
||||||
|
|
||||||
# Iterate over the provided rdlist_tags
|
|
||||||
for tag in rdlist_tags:
|
|
||||||
# Filter the data to keep only the entries where the tag appears in the "tags" list
|
|
||||||
filtered_data = [item for item in data if 'report_info' in item and 'tags' in item['report_info'] and tag in item['report_info']['tags']]
|
|
||||||
|
|
||||||
# Store the tags for each room
|
|
||||||
for item in filtered_data:
|
|
||||||
if 'room' in item and 'room_id' in item['room']:
|
|
||||||
room_id = item['room']['room_id']
|
|
||||||
all_room_ids.add(room_id) # add the room_id to the set
|
|
||||||
if room_id not in room_tags:
|
|
||||||
room_tags[room_id] = set()
|
|
||||||
room_tags[room_id].update(item['report_info']['tags'])
|
|
||||||
|
|
||||||
# Extract the room_ids
|
|
||||||
room_ids = [item['room']['room_id'] for item in filtered_data if 'room' in item and 'room_id' in item['room']]
|
|
||||||
|
|
||||||
# Examine these room_ids for local and remote users
|
|
||||||
for room_id in room_ids:
|
|
||||||
joined_members = room_commands.get_room_members(room_id)
|
|
||||||
|
|
||||||
# For each user, add the room_id and tags to the dictionary
|
|
||||||
for user_id in joined_members:
|
|
||||||
if user_id.endswith(hardcoded_variables.base_url):
|
|
||||||
if user_id not in all_local_users:
|
|
||||||
all_local_users[user_id] = dict()
|
|
||||||
all_local_users[user_id][room_id] = list(room_tags.get(room_id, []))
|
|
||||||
else:
|
|
||||||
if user_id not in all_remote_users:
|
|
||||||
all_remote_users[user_id] = dict()
|
|
||||||
all_remote_users[user_id][room_id] = list(room_tags.get(room_id, []))
|
|
||||||
|
|
||||||
all_room_ids = list(all_room_ids) # convert the set to a list
|
|
||||||
|
|
||||||
return all_room_ids, all_local_users, all_remote_users
|
|
||||||
|
|
||||||
def collect_user_reports_on_rdlist_accounts(all_local_users=None, skip_input=False):
|
|
||||||
# Print warning if testing mode is enabled
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
testing_mode_warning()
|
|
||||||
|
|
||||||
# If all_local_users is None, then we need to generate it
|
|
||||||
if all_local_users == None:
|
|
||||||
# Git clone the rdlist repo to ./rdlist/
|
|
||||||
sync_rdlist()
|
|
||||||
all_room_ids, all_local_users, all_remote_users = get_key_rdlist_info(hardcoded_variables.rdlist_recommended_tags)
|
|
||||||
|
|
||||||
# If there's at least 1 local user detected, ask the admin if they want to generate a user report for every user found in rdlist rooms
|
|
||||||
if len(all_local_users) > 0:
|
|
||||||
print(f"\nWARNING! The following local users are current members of rooms tagged in rdlist: {list(all_local_users.keys())}")
|
|
||||||
if skip_input == False:
|
|
||||||
generate_user_report_confirmation = input("\nDo you want to generate a user report file for each of these users? y/n? ")
|
|
||||||
if generate_user_report_confirmation.lower() in ['y', 'yes', 'Y', 'Yes'] or skip_input == True:
|
|
||||||
for user_id in all_local_users:
|
|
||||||
# Generate report_dict for each user
|
|
||||||
report_content = report_commands.generate_rdlist_report_summary(all_local_users[user_id], user_id)
|
|
||||||
report_commands.generate_user_report(user_id, report_content)
|
|
||||||
elif generate_user_report_confirmation.lower() in ['n', 'no', 'N', 'No']:
|
|
||||||
print("\nSkipping user report generation...\n")
|
|
||||||
elif len(all_local_users) == 0:
|
|
||||||
print(f"\nNo local users were found in rdlist rooms.")
|
|
||||||
|
|
||||||
def send_incident_reports_on_rdlist_accounts(all_remote_users=None, skip_input=False):
|
|
||||||
# Print warning if testing mode is enabled
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
testing_mode_warning()
|
|
||||||
|
|
||||||
# If all_remote_users is None, then we need to generate it
|
|
||||||
if all_remote_users == None:
|
|
||||||
# Git clone the rdlist repo to ./rdlist/
|
|
||||||
sync_rdlist()
|
|
||||||
all_room_ids, all_local_users, all_remote_users = get_key_rdlist_info(hardcoded_variables.rdlist_recommended_tags)
|
|
||||||
|
|
||||||
# If there's at least 1 remote user detected, ask the admin if they want to generate a user report for every user found in rdlist rooms
|
|
||||||
if len(all_remote_users) > 0:
|
|
||||||
print(f"\nWARNING! The following remote users are current members of rooms tagged in rdlist: {list(all_remote_users.keys())}")
|
|
||||||
if skip_input == False:
|
|
||||||
send_incident_report_confirmation = input("\nDo you want to send out incident reports for these users to every homeserver admin involved? y/n? ")
|
|
||||||
if send_incident_report_confirmation.lower() in ['y', 'yes', 'Y', 'Yes'] or skip_input == True:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(report_commands.send_incident_reports(all_remote_users))
|
|
||||||
elif send_incident_report_confirmation.lower() in ['n', 'no', 'N', 'No']:
|
|
||||||
print("\nSkipping incident report generation...\n")
|
|
||||||
elif len(all_remote_users) == 0:
|
|
||||||
print(f"\nNo remote users were found in rdlist rooms.")
|
|
||||||
|
|
||||||
def block_all_rooms_with_rdlist_tags(rdlist_use_recommended,preset_user_ID,preset_new_room_name,preset_message):
|
|
||||||
# Git clone the rdlist repo to ./rdlist/
|
|
||||||
sync_rdlist()
|
|
||||||
|
|
||||||
if rdlist_use_recommended == True:
|
|
||||||
# Use the hardcoded recommended tags
|
|
||||||
rdlist_tags = hardcoded_variables.rdlist_recommended_tags
|
|
||||||
print(f"\nUsing recommended rdlist tags. Rooms matching the following tags will be purged and/or blocked:\n{hardcoded_variables.rdlist_recommended_tags}")
|
|
||||||
|
|
||||||
elif rdlist_use_recommended == False:
|
|
||||||
# After the git repo has been cloned/pulled, open the file and read it into a string
|
|
||||||
with open(os.path.join("rdlist", "lib", "docs", "tags.md"), 'r') as file:
|
|
||||||
data = file.readlines()
|
|
||||||
|
|
||||||
# Print ./rdlist/lib/docs/tags.md README file for the user
|
|
||||||
print("\nPrinting details about the current tags in rdlist:\n")
|
|
||||||
for line in data:
|
|
||||||
print(line, end='') # Print the contents of the file
|
|
||||||
|
|
||||||
# Take input from the user and convert it to a list
|
|
||||||
print("\nPlease enter a space seperated list of tags you wish to block:\n")
|
|
||||||
rdlist_tags = input().split()
|
|
||||||
print('')
|
|
||||||
|
|
||||||
all_room_ids, all_local_users, all_remote_users = get_key_rdlist_info(rdlist_tags)
|
|
||||||
|
|
||||||
#print(f"\nDEBUG all_local_users: {all_local_users}")
|
|
||||||
#print(f"DEBUG all_remote_users: {all_remote_users}")
|
|
||||||
#print(f"DEBUG all_room_ids: {all_room_ids}")
|
|
||||||
|
|
||||||
# If there's at least 1 local user detected, ask the admin if they want to generate a user report for every user found in rdlist rooms
|
|
||||||
|
|
||||||
collect_user_reports_on_rdlist_accounts(all_local_users, False)
|
|
||||||
|
|
||||||
# If there's at least 1 remote user detected, ask the admin if they want to generate a incident report for every user homeserver involved in rdlist rooms
|
|
||||||
|
|
||||||
send_incident_reports_on_rdlist_accounts(all_remote_users, False)
|
|
||||||
|
|
||||||
# Ask the user if they wish to block and purge all these rooms, then collect shutdown parameters
|
|
||||||
if preset_user_ID == '':
|
|
||||||
user_ID = input("\nPlease enter the local username that will create a 'muted violation room' for your users (Example: michael): ")
|
|
||||||
elif preset_user_ID != '':
|
|
||||||
user_ID = preset_user_ID
|
|
||||||
if preset_new_room_name == '':
|
|
||||||
new_room_name = input("\nPlease enter the room name of the muted violation room your users will be sent to: ")
|
|
||||||
elif preset_new_room_name != '':
|
|
||||||
new_room_name = preset_new_room_name
|
|
||||||
if preset_message == '':
|
|
||||||
message = input("\nPlease enter the shutdown message that will be displayed to users: ")
|
|
||||||
elif preset_message != '':
|
|
||||||
message = preset_message
|
|
||||||
|
|
||||||
# Ask the user if they wish to block and purge all these rooms
|
|
||||||
shutdown_confirmation = input("\nNumber of rdlist rooms being shutdown: " + str(len(all_room_ids)) + "\n\nAre you sure you want to block/shutdown these rooms? y/n? ")
|
|
||||||
|
|
||||||
total_list_kicked_users = []
|
|
||||||
num_rooms_blocked = 0
|
|
||||||
num_rooms_purged = 0
|
|
||||||
|
|
||||||
if shutdown_confirmation.lower() in ['y', 'yes', 'Y', 'Yes']:
|
|
||||||
# Ask the user if they wish to shadow ban all local users in these rooms
|
|
||||||
shadow_ban_confirmation = input("\nDo you want to also shadow ban all your local users in these rooms before performing these shutdowns? (This is recommended as it prevents them from alerting others about these mass shutdown.) y/n? ")
|
|
||||||
# Perform shadow bans if admin confirms
|
|
||||||
if shadow_ban_confirmation in ['y', 'yes', 'Y', 'Yes']:
|
|
||||||
for user in all_local_users:
|
|
||||||
print(f"\nShadow banning user: {user}")
|
|
||||||
user_commands.shadow_ban_account(user)
|
|
||||||
for room_id in all_room_ids:
|
|
||||||
blocked_status = room_commands.get_block_status(room_id)
|
|
||||||
#print(f"\nroom_details_dict: {room_details_dict}")
|
|
||||||
#print(f"\nblock_status: {blocked_status}")
|
|
||||||
# If room is already blocked, skip it
|
|
||||||
if blocked_status == False:
|
|
||||||
# Examine if unblocked room is known, if not block it
|
|
||||||
room_details_dict = room_commands.get_room_details(room_id)
|
|
||||||
if "Room not found" in room_details_dict.get('error', ''):
|
|
||||||
print(f"\n\nBlocking unknown room: {room_id}")
|
|
||||||
room_commands.set_block_status(room_id, True)
|
|
||||||
num_rooms_blocked += 1
|
|
||||||
# If unblocked room is known, perform a shutdown of the room
|
|
||||||
else:
|
|
||||||
print(f"\n\nShutting down known room: {room_id}")
|
|
||||||
list_kicked_users = room_commands.shutdown_room(room_id, user_ID, new_room_name, message, True, True)
|
|
||||||
num_rooms_purged += 1
|
|
||||||
total_list_kicked_users.extend(list_kicked_users)
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
time.sleep(5)
|
|
||||||
elif blocked_status == True:
|
|
||||||
print(f"\n\nSkipping already blocked room: {room_id}")
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
time.sleep(5)
|
|
||||||
elif shutdown_confirmation.lower() in ['n', 'no', 'N', 'No']:
|
|
||||||
print("\nSkipping blocking/shutdown of rooms...\n")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print("\nInvalid input, skipping these files...\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Deduplicate the list of all kicked users
|
|
||||||
total_list_kicked_users = list(set(total_list_kicked_users))
|
|
||||||
|
|
||||||
# Return the list of all kicked users
|
|
||||||
return num_rooms_blocked, num_rooms_purged, total_list_kicked_users
|
|
||||||
|
|
||||||
def block_recommended_rdlist_tags():
|
|
||||||
# Print warning if testing mode is enabled
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
testing_mode_warning()
|
|
||||||
|
|
||||||
# Check if user account already exists
|
|
||||||
account_query = user_commands.query_account(hardcoded_variables.rdlist_bot_username)
|
|
||||||
|
|
||||||
# If user is not found, create it
|
|
||||||
if 'User not found' in account_query:
|
|
||||||
# Create user account
|
|
||||||
user_commands.create_account(hardcoded_variables.rdlist_bot_username, hardcoded_variables.rdlist_bot_password)
|
|
||||||
else:
|
|
||||||
print(f"@{hardcoded_variables.rdlist_bot_username}:{hardcoded_variables.base_url} account already exists. Resetting account password.\n")
|
|
||||||
user_commands.reset_password(hardcoded_variables.rdlist_bot_username, hardcoded_variables.rdlist_bot_password)
|
|
||||||
|
|
||||||
# Define default valies for shutdown_room()
|
|
||||||
preset_new_room_name = 'POLICY VIOLATION'
|
|
||||||
preset_message = 'THIS ROOM VIOLATES SERVER POLICIES'
|
|
||||||
|
|
||||||
# Block all rooms with recommended tag set
|
|
||||||
num_rooms_blocked, num_rooms_purged, total_list_kicked_users = block_all_rooms_with_rdlist_tags(True, hardcoded_variables.rdlist_bot_username, preset_new_room_name, preset_message)
|
|
||||||
|
|
||||||
# Print user login details
|
|
||||||
print("\n\nRoom shutdowns completed!\n\nUser login details for your moderator account:\n")
|
|
||||||
print("Username: " + hardcoded_variables.rdlist_bot_username)
|
|
||||||
print("Password: " + hardcoded_variables.rdlist_bot_password)
|
|
||||||
|
|
||||||
# Print statistics for the admin
|
|
||||||
print(f"\nPrint rdlist statistics:")
|
|
||||||
print(f"\nNumber of rooms blocked: {num_rooms_blocked}")
|
|
||||||
print(f"Number of rooms purged: {num_rooms_purged}")
|
|
||||||
print(f"Number of local users located in rdlist rooms and kicked: {len(total_list_kicked_users)}")
|
|
||||||
print(f"\nThe following users were current members of rooms tagged in rdlist: {total_list_kicked_users}")
|
|
||||||
|
|
||||||
# Ask admin if they want to deactivate all the accounts that were kicked from rdlist rooms
|
|
||||||
deactivate_confirmation = input("\nDo you want to also deactivate all these accounts that were kicked from rdlist rooms? y/n? ")
|
|
||||||
if deactivate_confirmation.lower() in ['y', 'yes', 'Y', 'Yes']:
|
|
||||||
for user_id in total_list_kicked_users:
|
|
||||||
user_commands.deactivate_account(user_id)
|
|
||||||
print(f"\nThese accounts have been deactivated.")
|
|
||||||
elif deactivate_confirmation.lower() in ['n', 'no', 'N', 'No']:
|
|
||||||
print("\nSkipping account deactivations...\n")
|
|
@@ -1,510 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import whois
|
|
||||||
import datetime
|
|
||||||
import zipfile
|
|
||||||
import smtplib
|
|
||||||
import requests
|
|
||||||
import asyncio
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from email.mime.base import MIMEBase
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from email.utils import COMMASPACE
|
|
||||||
from email import encoders
|
|
||||||
import user_commands
|
|
||||||
import room_commands
|
|
||||||
import ipinfo_commands
|
|
||||||
import bot_commands
|
|
||||||
import hardcoded_variables
|
|
||||||
|
|
||||||
rdlist_tag_descriptions = {
|
|
||||||
"csam": "Child Sexual Abuse Material",
|
|
||||||
"cfm": "An abundance of content which would directly appeal to those seeking csam.",
|
|
||||||
"jailbait": "Photos which contain underage individuals in questionable or suggestive situations.",
|
|
||||||
"tfm": "An abundance of content which would directly appeal to those seeking jailbait.",
|
|
||||||
"beastiality": "Self explanatory.",
|
|
||||||
"3d_loli": "Pornography which depicts photorealistic underage characters.",
|
|
||||||
"stylized_3d_loli": "Pornography which depicts underage characters that are not depicted in a realistic style.",
|
|
||||||
"gore": "Self explanatory.",
|
|
||||||
"snuff": "Self explanatory.",
|
|
||||||
"degen_misc": "Other types of coomers rooms.",
|
|
||||||
"degen_larp": "Coomer larp rooms.",
|
|
||||||
"degen_meet": "Coomer socializing rooms.",
|
|
||||||
"degen_porn": "Rooms dedicated to pornography, excluding types which have dedicated tags.",
|
|
||||||
"bot_porn": "Rooms which contain bots that spam pornographic content.",
|
|
||||||
"bot_spam": "Rooms which contain bots that spam content. Primarily for malvertising and cryptospam",
|
|
||||||
"preban": "Rooms which may not contain tagged content, however have clear intent. i.e: Rooms with names like 'CP Room', 'Child Porn', etc",
|
|
||||||
"hub_room_trade": "Rooms which exist solely to trade illegal or questionable content. i.e: csam, jailbait",
|
|
||||||
"hub_room_sussy": "A room which is sussy. This tag does not have a solid definition, see existing tagged rooms",
|
|
||||||
"abandoned": "Similar to 'anarchy', primarily for rooms which have automated spam bots.",
|
|
||||||
"anarchy": "Unmoderated rooms.",
|
|
||||||
"hub_room_underage": "Rooms which contain a disproportionate amount of underage users.",
|
|
||||||
"hub_room_links": "Rooms which exist to share links to other rooms.",
|
|
||||||
"toddlercon": "Lolicon but younger.",
|
|
||||||
"loli": "Rooms which exist to host lolicon.",
|
|
||||||
}
|
|
||||||
|
|
||||||
confidentiality_warning = f"""\n\n**********************************************************************
|
|
||||||
\t\tATTENTION! CONFIDENTIALITY NOTICE!
|
|
||||||
\nThis electronic mail and any files linked to it may hold information
|
|
||||||
that is privileged, confidential, and intended exclusively for the use of
|
|
||||||
the designated recipient or entity. If you're not the expected recipient or
|
|
||||||
the individual tasked with delivering the electronic mail to the intended recipient,
|
|
||||||
be aware that you've received this mail in error. Any utilization, duplication,
|
|
||||||
distribution, forwarding, printing, or publicizing of this email or the attached files
|
|
||||||
is strictly prohibited, as is revealing the information contained within.
|
|
||||||
If you've received this email in error, please promptly inform the sender and
|
|
||||||
remove it from your electronic mailbox.
|
|
||||||
\n**********************************************************************
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_report_folder():
|
|
||||||
# Get report_folder from hardcoded_variables
|
|
||||||
report_folder = hardcoded_variables.report_folder
|
|
||||||
|
|
||||||
# If report_folder ends with a slash, remove it
|
|
||||||
if report_folder.endswith(os.sep):
|
|
||||||
report_folder = report_folder[:-1]
|
|
||||||
|
|
||||||
return report_folder
|
|
||||||
|
|
||||||
def zip_report_folder(user_report_folder, username):
|
|
||||||
# Get parent directory of user_report_folder
|
|
||||||
parent_directory = os.path.dirname(os.path.abspath(user_report_folder))
|
|
||||||
|
|
||||||
# Create the name of the .zip file including timestamp
|
|
||||||
zip_file_name = os.path.join(parent_directory, username + "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".zip")
|
|
||||||
|
|
||||||
# Create a .zip file of the specified folder
|
|
||||||
with zipfile.ZipFile(zip_file_name, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
||||||
for root, dirs, files in os.walk(user_report_folder):
|
|
||||||
for file in files:
|
|
||||||
zip_file.write(os.path.join(root, file), arcname=os.path.relpath(os.path.join(root, file), user_report_folder))
|
|
||||||
|
|
||||||
return zip_file_name
|
|
||||||
|
|
||||||
def generate_user_report(preset_username, report_details):
|
|
||||||
if len(preset_username) == 0:
|
|
||||||
username = input("\nPlease enter the username to automatically generate a report: ")
|
|
||||||
username = user_commands.parse_username(username)
|
|
||||||
else:
|
|
||||||
username = user_commands.parse_username(preset_username)
|
|
||||||
|
|
||||||
# Check if user exists
|
|
||||||
if user_commands.check_user_account_exists(username) == False:
|
|
||||||
print("\nUser does not exist, exiting report generation.")
|
|
||||||
return
|
|
||||||
elif user_commands.check_user_account_exists(username) == True:
|
|
||||||
print(f"\nGenerating user report for {username}...")
|
|
||||||
# If report_folder ends in a slash, remove it
|
|
||||||
report_folder = get_report_folder()
|
|
||||||
|
|
||||||
# Create report folders
|
|
||||||
user_report_folder = report_folder + "/" + username + "/"
|
|
||||||
|
|
||||||
if os.path.exists(report_folder) == False:
|
|
||||||
os.mkdir(report_folder)
|
|
||||||
|
|
||||||
if os.path.exists(user_report_folder) == False:
|
|
||||||
os.mkdir(user_report_folder)
|
|
||||||
|
|
||||||
# Collect and write the report details to ./report/username/report_details.txt
|
|
||||||
if report_details == '':
|
|
||||||
report_details = input("\nPlease enter the details for this report. Include as much detail as possible, including:\n- A description of what happened.\n- Timestamps of events.\n- Whether this user was a repeat offender, if so include details about previous incidents.\n- Other user or rooms involved.\n- Other evidence you've collected against this user.\n- Whether the offending users were deactivated.\n- Whether the offending rooms were shut down.\n\n")
|
|
||||||
report_details_file = open(user_report_folder + "report_details.txt", "w")
|
|
||||||
report_details_file.write(report_details)
|
|
||||||
report_details_file.close()
|
|
||||||
|
|
||||||
# Get user account data and write to ./report/username/account_data.json
|
|
||||||
account_data = user_commands.collect_account_data(username)
|
|
||||||
account_data_file = open(user_report_folder + "account_data.json", "w")
|
|
||||||
account_data_file.write(json.dumps(account_data, indent=4, sort_keys=True))
|
|
||||||
account_data_file.close()
|
|
||||||
|
|
||||||
# Get user pushers and write to ./report/username/pushers.json
|
|
||||||
pushers_data = user_commands.list_account_pushers(username)
|
|
||||||
pushers_file = open(user_report_folder + "pushers.json", "w")
|
|
||||||
pushers_file.write(json.dumps(pushers_data, indent=4, sort_keys=True))
|
|
||||||
pushers_file.close()
|
|
||||||
|
|
||||||
# Get whois data and write to ./report/username/whois.json
|
|
||||||
whois_data = user_commands.whois_account(username)
|
|
||||||
whois_file = open(user_report_folder + "whois.json", "w")
|
|
||||||
whois_file.write(json.dumps(whois_data, indent=4, sort_keys=True))
|
|
||||||
whois_file.close()
|
|
||||||
|
|
||||||
# Get query data and write to ./report/username/query.json
|
|
||||||
query_data = user_commands.query_account(username)
|
|
||||||
query_file = open(user_report_folder + "query.json", "w")
|
|
||||||
query_file.write(json.dumps(query_data, indent=4, sort_keys=True))
|
|
||||||
query_file.close()
|
|
||||||
|
|
||||||
# Get user joined rooms and write to ./report/username/joined_rooms.json
|
|
||||||
joined_rooms_dict = user_commands.list_joined_rooms(username)
|
|
||||||
joined_rooms_file = open(user_report_folder + "joined_rooms.json", "w")
|
|
||||||
joined_rooms_file.write(json.dumps(joined_rooms_dict, indent=4, sort_keys=True))
|
|
||||||
joined_rooms_file.close()
|
|
||||||
|
|
||||||
# Get user ipinfo and write to ./report/username/ipinfo.json
|
|
||||||
ipinfo = ipinfo_commands.analyse_account_ip(username)
|
|
||||||
ipinfo_file = open(user_report_folder + "ipinfo.json", "w")
|
|
||||||
ipinfo_file.write(json.dumps(ipinfo, indent=4, sort_keys=True))
|
|
||||||
ipinfo_file.close()
|
|
||||||
|
|
||||||
# Prepare folder structures
|
|
||||||
room_folder = user_report_folder + "rooms/"
|
|
||||||
dm_folder = user_report_folder + "dms/"
|
|
||||||
forgotten_folder = user_report_folder + "forgotten/"
|
|
||||||
details_folder = "details/"
|
|
||||||
states_folder = "states/"
|
|
||||||
|
|
||||||
# For each room the user is in, get the room state and write to ./report/username/rooms/states/
|
|
||||||
room_states_folder = room_folder + states_folder
|
|
||||||
if not os.path.exists(room_states_folder):
|
|
||||||
os.makedirs(room_states_folder, exist_ok=True)
|
|
||||||
|
|
||||||
# For each room the user is in, get the room details and write to ./report/username/rooms/details/
|
|
||||||
room_details_folder = room_folder + details_folder
|
|
||||||
if not os.path.exists(room_details_folder):
|
|
||||||
os.makedirs(room_details_folder, exist_ok=True)
|
|
||||||
|
|
||||||
# For DM, get the state and write to ./report/username/dms/states/
|
|
||||||
dm_states_folder = dm_folder + states_folder
|
|
||||||
if not os.path.exists(dm_states_folder):
|
|
||||||
os.makedirs(dm_states_folder, exist_ok=True)
|
|
||||||
|
|
||||||
# For DM, get the details and write to ./report/username/dms/details/
|
|
||||||
dm_details_folder = dm_folder + details_folder
|
|
||||||
if not os.path.exists(dm_details_folder):
|
|
||||||
os.makedirs(dm_details_folder, exist_ok=True)
|
|
||||||
|
|
||||||
# For forgotten rooms, get the state and write to ./report/username/forgotten/states/
|
|
||||||
forgotten_states_folder = forgotten_folder + states_folder
|
|
||||||
if not os.path.exists(forgotten_states_folder):
|
|
||||||
os.makedirs(forgotten_states_folder, exist_ok=True)
|
|
||||||
|
|
||||||
# For forgotten rooms, get the details and write to ./report/username/forgotten/details/
|
|
||||||
forgotten_details_folder = forgotten_folder + details_folder
|
|
||||||
if not os.path.exists(forgotten_details_folder):
|
|
||||||
os.makedirs(forgotten_details_folder, exist_ok=True)
|
|
||||||
|
|
||||||
room_list = list(account_data['account_data']['rooms'].keys())
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for room in room_list:
|
|
||||||
count += 1
|
|
||||||
room = room.split(" ")[0]
|
|
||||||
room_details = room_commands.get_room_details(room)
|
|
||||||
|
|
||||||
# Check the room conditions to select the proper output folders
|
|
||||||
if room_details['forgotten'] == True:
|
|
||||||
room_details_file = open(forgotten_details_folder + room + ".json", "w")
|
|
||||||
room_commands.export_room_state(room, forgotten_states_folder, True)
|
|
||||||
elif room_details['joined_members'] == 2 and room_details['public'] == False:
|
|
||||||
room_details_file = open(dm_details_folder + room + ".json", "w")
|
|
||||||
room_commands.export_room_state(room, dm_states_folder, True)
|
|
||||||
else:
|
|
||||||
room_details_file = open(room_details_folder + room + ".json", "w")
|
|
||||||
room_commands.export_room_state(room, room_states_folder, True)
|
|
||||||
|
|
||||||
room_details_file.write(json.dumps(room_details, indent=4, sort_keys=True))
|
|
||||||
room_details_file.close()
|
|
||||||
|
|
||||||
if count > 10 and hardcoded_variables.testing_mode == True:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Generate a random password, then encrypt the ./report/username/ folder to a timestamped .zip file
|
|
||||||
zip_file_name = zip_report_folder(user_report_folder, username)
|
|
||||||
|
|
||||||
# Measure the size of the encrypted .zip file in MB
|
|
||||||
zip_file_size = os.path.getsize(zip_file_name) / 1000000
|
|
||||||
|
|
||||||
# Print the password and the encrypted .zip file name
|
|
||||||
print("Report generated successfully on user: \"" + username + "\"\n\nYou can send this .zip file when reporting a user to law enforcement.\n")
|
|
||||||
print(".zip file location: " + zip_file_name)
|
|
||||||
print(".zip file size: " + str(zip_file_size) + " MB\n")
|
|
||||||
|
|
||||||
return zip_file_name
|
|
||||||
|
|
||||||
def lookup_homeserver_admin(preset_baseurl):
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
baseurl = hardcoded_variables.base_url
|
|
||||||
elif preset_baseurl == '':
|
|
||||||
baseurl = input("\nEnter the base URL to collect the admin contact details (Example: matrix.org): ")
|
|
||||||
elif preset_baseurl != '':
|
|
||||||
baseurl = preset_baseurl
|
|
||||||
|
|
||||||
# If baseurl is matrix.org, return 'abuse@matrix.org' as a hardcoded response
|
|
||||||
if baseurl == "matrix.org":
|
|
||||||
print("\nAdmin contact email(s) for " + baseurl + " are: abuse@matrix.org")
|
|
||||||
return {"admins": {"email_address": "abuse@matrix.org"}}, False
|
|
||||||
|
|
||||||
# Check target homserver for MSC1929 support email
|
|
||||||
url = f"https://{baseurl}/.well-known/matrix/support"
|
|
||||||
try:
|
|
||||||
response = requests.get(url)
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"Error: Unable to connect to server {baseurl}. Trying WHOIS data...")
|
|
||||||
response = None
|
|
||||||
|
|
||||||
# If the request was successful, the status code will be 200
|
|
||||||
if response.status_code == 200 and ( "email_address" in response.text or "matrix_id" in response.text ):
|
|
||||||
# Parse the response as JSON
|
|
||||||
data = json.loads(response.text)
|
|
||||||
|
|
||||||
#print("\nAdmin contact details for " + baseurl + " are: " + str(data))
|
|
||||||
|
|
||||||
return data, False
|
|
||||||
else:
|
|
||||||
print(f"Error: Unable to collect admin contact details from server {baseurl}")
|
|
||||||
print("Attempting to collect admin email from WHOIS data...")
|
|
||||||
|
|
||||||
# Get WHOIS data
|
|
||||||
try:
|
|
||||||
w = whois.whois(baseurl)
|
|
||||||
if w.emails:
|
|
||||||
# Check if the emails field is a list
|
|
||||||
if isinstance(w.emails, list):
|
|
||||||
return {baseurl: list(w.emails)}, True
|
|
||||||
# If it's not a list, it must be a single string. So, we wrap it in a list
|
|
||||||
else:
|
|
||||||
return {baseurl: [w.emails]}, True
|
|
||||||
else:
|
|
||||||
print(f"\t\tError: Unable to collect admin email from WHOIS data for {baseurl}")
|
|
||||||
return None, False
|
|
||||||
except:
|
|
||||||
print(f"\t\tError: Unable to collect WHOIS data for {baseurl}")
|
|
||||||
return None, False
|
|
||||||
|
|
||||||
def send_email(email_address, email_subject, email_content, email_attachments):
|
|
||||||
assert isinstance(email_attachments, list)
|
|
||||||
|
|
||||||
msg = MIMEMultipart() # Create a multipart message
|
|
||||||
msg['From'] = hardcoded_variables.smtp_user
|
|
||||||
msg['To'] = COMMASPACE.join([email_address])
|
|
||||||
msg['Subject'] = email_subject
|
|
||||||
|
|
||||||
msg.attach(MIMEText(email_content)) # Attach the email body
|
|
||||||
|
|
||||||
# Attach files
|
|
||||||
for file in email_attachments:
|
|
||||||
part = MIMEBase('application', "octet-stream")
|
|
||||||
with open(file, 'rb') as f:
|
|
||||||
part.set_payload(f.read())
|
|
||||||
encoders.encode_base64(part)
|
|
||||||
part.add_header('Content-Disposition', 'attachment', filename=os.path.basename(file))
|
|
||||||
msg.attach(part)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the email via SMTP server
|
|
||||||
smtp = smtplib.SMTP(hardcoded_variables.smtp_server, hardcoded_variables.smtp_port)
|
|
||||||
smtp.starttls()
|
|
||||||
smtp.login(hardcoded_variables.smtp_user, hardcoded_variables.smtp_password)
|
|
||||||
smtp.sendmail(hardcoded_variables.smtp_user, email_address, msg.as_string())
|
|
||||||
smtp.close()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to send email: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_send_email():
|
|
||||||
# Ask the user for the destination email address
|
|
||||||
email_address = hardcoded_variables.incident_report_return_email
|
|
||||||
|
|
||||||
# Example email parameters
|
|
||||||
email_subject = "Incident Report"
|
|
||||||
email_content = "Hello! This is a test email. Please ignore it."
|
|
||||||
email_attachments = ["./test_data/evil_clown.jpeg"] # List of file paths. Adjust this to the actual files you want to attach.
|
|
||||||
|
|
||||||
# Try to send the email
|
|
||||||
if send_email(email_address, email_subject, email_content, email_attachments):
|
|
||||||
print("\nEmail successfully sent.")
|
|
||||||
else:
|
|
||||||
print("\nFailed to send email.")
|
|
||||||
|
|
||||||
def generate_rdlist_report_summary(room_dict, user_id):
|
|
||||||
#print(f"user_dict: {room_dict}")
|
|
||||||
report_content = f"""\n~~~User Report~~~\n\nUsername: {user_id}\n"""
|
|
||||||
|
|
||||||
for room_id, rdlist_tags in room_dict.items():
|
|
||||||
report_content += f"\nWas a member of this room: {room_id}\nThis room has been flagged with the following rdlist tags:\n"
|
|
||||||
for tag in rdlist_tags:
|
|
||||||
tag_description = rdlist_tag_descriptions.get(tag, "No description available.")
|
|
||||||
report_content += f" - {tag} ({tag_description})\n"
|
|
||||||
|
|
||||||
report_content
|
|
||||||
|
|
||||||
return report_content
|
|
||||||
|
|
||||||
def prepare_email_content(user_dict, from_whois, baseurl):
|
|
||||||
email_content = f"""Dear Administrator,
|
|
||||||
|
|
||||||
We regret to inform you that there have been incidents involving the following users in your homeserver:
|
|
||||||
"""
|
|
||||||
|
|
||||||
for full_username, room_dict in user_dict.items():
|
|
||||||
email_content += f"\nUser: {full_username}\n"
|
|
||||||
for room_id, rdlist_tags in room_dict.items():
|
|
||||||
email_content += f"\n Is in the room: {room_id}\n This room has been flagged with the following rdlist tags:\n"
|
|
||||||
for tag in rdlist_tags:
|
|
||||||
tag_description = rdlist_tag_descriptions.get(tag, "No description available.")
|
|
||||||
email_content += f" - {tag} ({tag_description})\n"
|
|
||||||
|
|
||||||
email_content += f"""
|
|
||||||
|
|
||||||
We request your immediate attention to this matter. It is recommended that you:
|
|
||||||
- Generate a report on these users' accounts and send it to law enforcement.
|
|
||||||
- Block and purge these rooms from your homeserver.
|
|
||||||
- Deactivate these users' accounts, or retain them for further observation.
|
|
||||||
|
|
||||||
All of these actions can be done automatically using this moderation tool:
|
|
||||||
https://github.com/PC-Admin/matrix-moderation-tool
|
|
||||||
|
|
||||||
**********************************************************************
|
|
||||||
\tTHIS EMAIL IS UNMONITORED, PLEASE DO NOT REPLY TO IT
|
|
||||||
**********************************************************************
|
|
||||||
|
|
||||||
To contact us please email {hardcoded_variables.incident_report_return_email}.
|
|
||||||
|
|
||||||
Thank you for helping us make Matrix safer.
|
|
||||||
|
|
||||||
Best regards,
|
|
||||||
Abuse Management Team
|
|
||||||
https://{hardcoded_variables.base_url}
|
|
||||||
"""
|
|
||||||
if from_whois:
|
|
||||||
email_content += f"""\n\n**********************************************************************
|
|
||||||
\tATTENTION DOMAIN REGISTRAR, YOUR ACTION IS REQUIRED HERE
|
|
||||||
**********************************************************************
|
|
||||||
\nThis email is intended for the owner of the domain {baseurl}, but they have not published their email address.
|
|
||||||
\nAs the recipient of this email, you have a legal obligation to ensure that this email reaches them.
|
|
||||||
\nTo avoid receiving these emails in the future, please ask them to configure MSC1929 for their Matrix server:
|
|
||||||
https://github.com/matrix-org/matrix-spec-proposals/pull/1929
|
|
||||||
"""
|
|
||||||
|
|
||||||
email_content += confidentiality_warning
|
|
||||||
return email_content
|
|
||||||
|
|
||||||
def prepare_message_content(user_dict, baseurl):
|
|
||||||
message_content = f"""Dear Administrator,
|
|
||||||
|
|
||||||
We regret to inform you that there have been incidents involving the following users in your homeserver:
|
|
||||||
"""
|
|
||||||
|
|
||||||
for full_username, room_dict in user_dict.items():
|
|
||||||
message_content += f"\nUser: {full_username}\n"
|
|
||||||
for room_id, rdlist_tags in room_dict.items():
|
|
||||||
message_content += f"\n Is in the room: {room_id}\n This room has been flagged with the following rdlist tags:\n"
|
|
||||||
for tag in rdlist_tags:
|
|
||||||
tag_description = rdlist_tag_descriptions.get(tag, "No description available.")
|
|
||||||
message_content += f" - {tag} ({tag_description})\n"
|
|
||||||
|
|
||||||
message_content += f"""
|
|
||||||
|
|
||||||
We request your immediate attention to this matter. It is recommended that you:
|
|
||||||
- Generate a report on these users' accounts and send it to law enforcement.
|
|
||||||
- Block and purge these rooms from your homeserver.
|
|
||||||
- Deactivate these users' accounts, or retain them for further observation.
|
|
||||||
|
|
||||||
All of these actions can be done automatically using this moderation tool:
|
|
||||||
https://github.com/PC-Admin/matrix-moderation-tool
|
|
||||||
|
|
||||||
**********************************************************************
|
|
||||||
\tTHIS ACCOUNT IS UNMONITORED, PLEASE DO NOT REPLY TO IT
|
|
||||||
**********************************************************************
|
|
||||||
|
|
||||||
To contact us please message {hardcoded_variables.incident_report_return_mxid}.
|
|
||||||
|
|
||||||
Thank you for helping us make Matrix safer.
|
|
||||||
|
|
||||||
Best regards,
|
|
||||||
Abuse Management Team
|
|
||||||
https://{hardcoded_variables.base_url}
|
|
||||||
"""
|
|
||||||
|
|
||||||
return message_content
|
|
||||||
|
|
||||||
async def send_incident_reports(incidents_dict):
|
|
||||||
success = True
|
|
||||||
homeserver_dict = {}
|
|
||||||
|
|
||||||
# Aggregate incidents by homeserver.
|
|
||||||
for full_username, room_dict in incidents_dict.items():
|
|
||||||
baseurl = full_username.split(":")[1]
|
|
||||||
|
|
||||||
if baseurl not in homeserver_dict:
|
|
||||||
homeserver_dict[baseurl] = {}
|
|
||||||
homeserver_dict[baseurl][full_username] = room_dict
|
|
||||||
|
|
||||||
# Prepare and send one incident report per homeserver, including all users and rooms.
|
|
||||||
for baseurl, user_dict in homeserver_dict.items():
|
|
||||||
|
|
||||||
admin_contact_dict, from_whois = lookup_homeserver_admin(baseurl)
|
|
||||||
|
|
||||||
if not admin_contact_dict or "admins" not in admin_contact_dict:
|
|
||||||
print(f"Unable to find any admin emails for {baseurl}")
|
|
||||||
success = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Prepare and send one message or email per homeserver, including all users and rooms.
|
|
||||||
for admin in admin_contact_dict["admins"]:
|
|
||||||
#print(f"DEBUG: {type(admin)}")
|
|
||||||
#print(f"DEBUG: {admin}") # this will print the content of each admin dict
|
|
||||||
if "matrix_id" in admin: # If matrix_id exists
|
|
||||||
message_content = prepare_message_content(user_dict, baseurl)
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(f"\nSending Incident Report for users from {baseurl} to {admin['matrix_id']}")
|
|
||||||
await bot_commands.send_message(admin["matrix_id"], message_content)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to send message to {admin['matrix_id']}: {str(e)}")
|
|
||||||
success = False
|
|
||||||
# If email_address exists, or if message send failed, send Incident report via email
|
|
||||||
elif "email_address" in admin or success == False:
|
|
||||||
email_address = admin.get("email_address")
|
|
||||||
if email_address: # If email_address exists
|
|
||||||
email_subject = f"Incident Report for users from {baseurl}"
|
|
||||||
email_content = prepare_email_content(user_dict, from_whois, baseurl)
|
|
||||||
|
|
||||||
email_attachments = []
|
|
||||||
print(f"Sending Incident Report for users from {baseurl} to {admin['email_address']}")
|
|
||||||
if not send_email(email_address, email_subject, email_content, email_attachments):
|
|
||||||
print(f"Failed to send email to {email_address}")
|
|
||||||
success = False
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
def test_send_incident_reports():
|
|
||||||
incidents_dict = {
|
|
||||||
f"@billybob:matrix.org": {
|
|
||||||
"!dummyid1:matrix.org": ["csam", "loli", "beastiality"],
|
|
||||||
"!dummyid2:matrix.org": ["csam", "anarchy"]
|
|
||||||
},
|
|
||||||
f"@johndoe:matrix.org": {
|
|
||||||
"!dummyid3:matrix.org": ["csam", "loli", "toddlercon"],
|
|
||||||
"!dummyid4:matrix.org": ["anarchy", "terrorism"]
|
|
||||||
},
|
|
||||||
f"@pedobear:perthchat.org": {
|
|
||||||
"!dummyid5:matrix.org": ["csam", "loli", "jailbait"],
|
|
||||||
"!dummyid6:matrix.org": ["csam", "hublinks"]
|
|
||||||
},
|
|
||||||
f"@randomcreep:perthchat.org": {
|
|
||||||
"!dummyid7:matrix.org": ["csam", "jailbait"],
|
|
||||||
"!dummyid8:matrix.org": ["csam", "preban"]
|
|
||||||
},
|
|
||||||
f"@fatweeb:grin.hu": {
|
|
||||||
"!dummyid9:matrix.org": ["csam", "loli"],
|
|
||||||
"!dummyid10:matrix.org": ["csam", "degen_porn"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
if hardcoded_variables.testing_mode == True:
|
|
||||||
print("\nNOTE: Testing mode is enabled, sending Incident Reports to you! :)\n")
|
|
||||||
if asyncio.run(send_incident_reports(incidents_dict)):
|
|
||||||
print("\nIncident reports successfully sent.")
|
|
||||||
else:
|
|
||||||
print("\nFailed to send the incident reports.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nFailed to send incident reports: {e}")
|
|
654
room_commands.py
654
room_commands.py
@@ -1,654 +0,0 @@
|
|||||||
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import csv
|
|
||||||
import requests
|
|
||||||
import hardcoded_variables
|
|
||||||
|
|
||||||
def parse_username(username):
|
|
||||||
tail_end = ':' + hardcoded_variables.base_url
|
|
||||||
username = username.replace('@','')
|
|
||||||
username = username.replace(tail_end,'')
|
|
||||||
return username
|
|
||||||
|
|
||||||
def get_room_details(preset_internal_ID):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you wish to query (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
elif preset_internal_ID != '':
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/rooms/{internal_ID}"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, headers=headers, verify=True)
|
|
||||||
|
|
||||||
room_details_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return room_details_dict
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -kXGET 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!OeqILBxiHahidSQQoC:matrix.org?access_token=ACCESS_TOKEN'
|
|
||||||
|
|
||||||
def get_room_members(preset_internal_ID, local_only=False):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you wish to query (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
elif preset_internal_ID != '':
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/rooms/{internal_ID}/members"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, verify=True)
|
|
||||||
|
|
||||||
room_members_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
# Print room_members_dict for debugging
|
|
||||||
#print("room_members_dict: " + json.dumps(room_members_dict, indent=4, sort_keys=True))
|
|
||||||
|
|
||||||
# Check if the 'members' key is in the response
|
|
||||||
if 'members' in room_members_dict:
|
|
||||||
# List of all members
|
|
||||||
room_members = room_members_dict['members']
|
|
||||||
|
|
||||||
if local_only:
|
|
||||||
# Filter to get only local members
|
|
||||||
room_members = [member for member in room_members if member.split(':')[1] == hardcoded_variables.base_url]
|
|
||||||
|
|
||||||
else:
|
|
||||||
# If 'members' key is not found, return an empty dictionary
|
|
||||||
room_members = {}
|
|
||||||
|
|
||||||
# Print room_members for debugging
|
|
||||||
#print("room_members: " + str(room_members))
|
|
||||||
return room_members
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -kXGET 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!OeqILBxiHahidSQQoC:matrix.org/members?access_token=ACCESS_TOKEN'
|
|
||||||
|
|
||||||
# This function returns the state of a room as output and optionally writes it to a json file
|
|
||||||
def export_room_state(preset_internal_ID, preset_directory, save_to_file):
|
|
||||||
# record the current directory location
|
|
||||||
current_directory = os.getcwd()
|
|
||||||
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room with which to export the 'state' of (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
elif preset_internal_ID != '':
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
|
|
||||||
if preset_directory == '':
|
|
||||||
room_dir = os.path.join(current_directory, "state_events")
|
|
||||||
elif preset_directory != '':
|
|
||||||
room_dir = preset_directory
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/rooms/{internal_ID}/state"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, headers=headers, verify=True)
|
|
||||||
|
|
||||||
state_events_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
# If save_to_file is True, write the output to a file
|
|
||||||
if save_to_file == True:
|
|
||||||
if "Room not found" not in state_events_dict.get('error', ''):
|
|
||||||
# If save_to_file is True, create the directory if it doesn't exist
|
|
||||||
os.makedirs(room_dir, exist_ok=True)
|
|
||||||
# Define the filename and write to it
|
|
||||||
unix_time = int(time.time())
|
|
||||||
filename = os.path.join(room_dir, f"{internal_ID}_state_{unix_time}.json")
|
|
||||||
#print(f"Writing room state events to {filename}")
|
|
||||||
with open(filename, 'w') as f:
|
|
||||||
f.write(json.dumps(state_events_dict, indent=4, sort_keys=True))
|
|
||||||
elif "Room not found" in state_events_dict.get('error', ''):
|
|
||||||
print("Room not found, skipping write to file...")
|
|
||||||
|
|
||||||
return state_events_dict
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -kXGET 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!OeqILBxiHahidSQQoC:matrix.org/state?access_token=ACCESS_TOKEN'
|
|
||||||
|
|
||||||
# See
|
|
||||||
# https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-state-api
|
|
||||||
|
|
||||||
def export_multiple_room_states(preset_internal_IDs=None, preset_directory=''):
|
|
||||||
print("Export multiple room states selected")
|
|
||||||
|
|
||||||
if preset_internal_IDs is None:
|
|
||||||
room_list_location = input("\nPlease enter the path of the file containing a JSON array of rooms: ")
|
|
||||||
with open(room_list_location, 'r') as f:
|
|
||||||
room_ids = json.load(f)
|
|
||||||
else:
|
|
||||||
room_ids = preset_internal_IDs
|
|
||||||
|
|
||||||
print("\n" + str(room_ids) + "\n")
|
|
||||||
export_confirmation = input("\nAre you sure you want to export the state of all of these rooms? y/n?\n")
|
|
||||||
|
|
||||||
if export_confirmation in ["y", "yes", "Y", "Yes"]:
|
|
||||||
for room_id in room_ids:
|
|
||||||
export_room_state(room_id, preset_directory, True)
|
|
||||||
elif export_confirmation in ["n", "no", "N", "No"]:
|
|
||||||
print("Export canceled by user.")
|
|
||||||
|
|
||||||
def public_directory_rooms():
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_matrix/client/r0/publicRooms"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, headers=headers, verify=True)
|
|
||||||
output = response.text
|
|
||||||
|
|
||||||
output = output.replace('\"room_id\":\"','\n')
|
|
||||||
output = output.replace('\",\"name','\n\",\"name')
|
|
||||||
|
|
||||||
print(json.dumps(output, indent=4, sort_keys=True))
|
|
||||||
|
|
||||||
public_room_directories_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return public_room_directories_dict
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -kXGET https://matrix.perthchat.org/_matrix/client/r0/publicRooms?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def remove_room_from_directory(preset_internal_ID):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you wish to remove from the directory (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
elif preset_internal_ID != '':
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_matrix/client/r0/directory/list/room/{internal_ID}"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
data = {"visibility": "private"}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.put(url, headers=headers, json=data, verify=True)
|
|
||||||
|
|
||||||
print(response.text)
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -kX PUT -H 'Content-Type: application/json' -d '{"visibility": "private"}' 'https://matrix.perthchat.org/_matrix/client/r0/directory/list/room/!DwUPBvNapIVecNllgt:perthchat.org?access_token=ACCESS_TOKEN'
|
|
||||||
|
|
||||||
def remove_multiple_rooms_from_directory():
|
|
||||||
print("Remove multiple rooms from directory selected")
|
|
||||||
purge_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
|
||||||
with open(purge_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
print(data[x][0])
|
|
||||||
remove_room_from_directory(data[x][0])
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def list_and_download_media_in_room(preset_internal_ID, preset_print_file_list_choice, preset_download_files_choice, base_directory):
|
|
||||||
headers = {
|
|
||||||
'Authorization': f"Bearer {hardcoded_variables.access_token}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you want to get a list of media for (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
else:
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/room/{internal_ID}/media"
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, verify=True)
|
|
||||||
media_list_output = response.text
|
|
||||||
print("Full media list:\n" + media_list_output)
|
|
||||||
|
|
||||||
if preset_print_file_list_choice == '':
|
|
||||||
print_file_list_choice = input("\nDo you want to write this list to a file? y/n? ")
|
|
||||||
else:
|
|
||||||
print_file_list_choice = preset_print_file_list_choice
|
|
||||||
|
|
||||||
if print_file_list_choice.lower() in ["y", "yes", "Y", "Yes"]:
|
|
||||||
print_file_list_choice = "true"
|
|
||||||
elif print_file_list_choice.lower() in ["n", "no", "N", "No"]:
|
|
||||||
print_file_list_choice = "false"
|
|
||||||
else:
|
|
||||||
print("Input invalid! Defaulting to 'No'.")
|
|
||||||
print_file_list_choice = "false"
|
|
||||||
|
|
||||||
room_dir = os.path.join(base_directory, internal_ID.replace('!', '').replace(':', '-'))
|
|
||||||
os.makedirs(room_dir, exist_ok=True)
|
|
||||||
|
|
||||||
if print_file_list_choice == "true":
|
|
||||||
media_list_filename_location = os.path.join(room_dir, "media_list.txt")
|
|
||||||
with open(media_list_filename_location,"w+") as media_list_filename:
|
|
||||||
media_list_filename.write(media_list_output)
|
|
||||||
|
|
||||||
if preset_download_files_choice == '':
|
|
||||||
download_files_choice = input("\nDo you also want to download a copy of these media files? y/n? ")
|
|
||||||
else:
|
|
||||||
download_files_choice = preset_download_files_choice
|
|
||||||
|
|
||||||
if download_files_choice.lower() in ["y", "yes", "Y", "Yes"]:
|
|
||||||
download_files_choice = "true"
|
|
||||||
elif download_files_choice.lower() in ["n", "no", "N", "No"]:
|
|
||||||
download_files_choice = "false"
|
|
||||||
else:
|
|
||||||
print("Input invalid! Defaulting to 'No'.")
|
|
||||||
download_files_choice = "false"
|
|
||||||
|
|
||||||
if download_files_choice == "true":
|
|
||||||
media_files_dir = os.path.join(room_dir, "media-files")
|
|
||||||
os.makedirs(media_files_dir, exist_ok=True)
|
|
||||||
|
|
||||||
media_list_output = json.loads(media_list_output)
|
|
||||||
for key in ['local', 'remote']:
|
|
||||||
for media in media_list_output.get(key, []):
|
|
||||||
media_url = media.replace('mxc://', f"https://{hardcoded_variables.homeserver_url}/_matrix/media/r0/download/")
|
|
||||||
media_response = requests.get(media_url, stream=True, headers=headers, verify=True)
|
|
||||||
|
|
||||||
if media_response.status_code == 200:
|
|
||||||
media_file_path = os.path.join(media_files_dir, media.split('/')[-1])
|
|
||||||
with open(media_file_path, 'wb') as media_file:
|
|
||||||
media_file.write(media_response.content)
|
|
||||||
print(f"Downloaded {media_url} to {media_file_path}")
|
|
||||||
else:
|
|
||||||
print(f"Failed to download {media_url}, status code: {media_response.status_code}")
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -kXGET https://matrix.perthchat.org/_synapse/admin/v1/room/<room_id>/media?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
# To access via web:
|
|
||||||
# https://matrix.perthchat.org/_matrix/media/r0/download/ + server_name + "/" + media_id
|
|
||||||
|
|
||||||
def download_media_from_multiple_rooms():
|
|
||||||
print("Download media from multiple rooms selected")
|
|
||||||
download_media_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
|
||||||
with open(download_media_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
preset_print_file_list_choice = input("\n Do you want to print list files of all the media in these rooms? y/n? ")
|
|
||||||
preset_download_files_choice = input("\n Do you want to download all the media in these rooms? y/n? ")
|
|
||||||
|
|
||||||
os.mkdir("./media_download")
|
|
||||||
os.chdir("./media_download")
|
|
||||||
|
|
||||||
pwd_process = subprocess.run(["pwd"], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
base_directory = pwd_process.stdout
|
|
||||||
base_directory = base_directory.replace('\n','')
|
|
||||||
print(base_directory)
|
|
||||||
|
|
||||||
print("Beginning download of media from all rooms in list...")
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
print(data[x][0])
|
|
||||||
list_and_download_media_in_room(data[x][0],preset_print_file_list_choice,preset_download_files_choice,base_directory)
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def redact_room_event():
|
|
||||||
internal_ID = input("\nEnter the internal id of the room the event is in (Example: !rapAelwZkajRyeZIpm:perthchat.org): ")
|
|
||||||
event_ID = input("\nEnter the event id of the event you wish to redact (Example: $lQT7NYYyVvwoVpZWcj7wceYQqeOzsJg1N6aXIecys4s): ")
|
|
||||||
redaction_reason = input("\nEnter the reason you're redacting this content: ")
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_matrix/client/v3/rooms/{internal_ID}/redact/{event_ID}"
|
|
||||||
print(f"\nRequesting: {url}\n")
|
|
||||||
output = requests.post(url, headers={'Authorization': f"Bearer {hardcoded_variables.access_token}"}, json={'reason': redaction_reason}, verify=True).text
|
|
||||||
print(output)
|
|
||||||
|
|
||||||
# $ curl -X POST --header "Authorization: Bearer syt_..." --data-raw '{"reason": "Indecent material"}' 'https://matrix.perthchat.org/_matrix/client/v3/rooms/!fuYHAYyXqNLDxlKsWP:perthchat.org/redact/$nyjgZguQGadRRy8MdYtIgwbAeFcUAPqOPiaj_E60XZs'
|
|
||||||
# {"event_id":"$_m1gFtPg-5DiTyCvGfeveAX2xaA8gAv0BYLpjC8xe64"}
|
|
||||||
|
|
||||||
def quarantine_media_in_room():
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you want to quarantine, this makes local and remote data inaccessible (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/room/{internal_ID}/media/quarantine"
|
|
||||||
print(f"\nRequesting: {url}\n")
|
|
||||||
headers = {'Authorization': f'Bearer {hardcoded_variables.access_token}'}
|
|
||||||
response = requests.post(url, headers=headers, verify=True)
|
|
||||||
print(response.text)
|
|
||||||
|
|
||||||
# Example
|
|
||||||
# $ curl -X POST 'https://matrix.perthchat.org/_synapse/admin/v1/room/!DwUPBvNapIVecNllgt:perthchat.org/media/quarantine?access_token=ACCESS_TOKEN'
|
|
||||||
|
|
||||||
def shutdown_room(preset_internal_ID,preset_user_ID,preset_new_room_name,preset_message,preset_purge_choice,preset_block_choice):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you want shutdown (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
elif preset_internal_ID != '':
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
if preset_user_ID == '':
|
|
||||||
user_ID = input("\nPlease enter the local username that will create a 'muted violation room' for your users (Example: michael): ")
|
|
||||||
elif preset_user_ID != '':
|
|
||||||
user_ID = preset_user_ID
|
|
||||||
if preset_new_room_name == '':
|
|
||||||
new_room_name = input("\nPlease enter the room name of the muted violation room your users will be sent to: ")
|
|
||||||
elif preset_new_room_name != '':
|
|
||||||
new_room_name = preset_new_room_name
|
|
||||||
if preset_message == '':
|
|
||||||
message = input("\nPlease enter the shutdown message that will be displayed to users: ")
|
|
||||||
elif preset_message != '':
|
|
||||||
message = preset_message
|
|
||||||
if preset_purge_choice == '':
|
|
||||||
purge_choice = input("\nDo you want to purge the room? (This deletes all the room history from your database.) y/n? ")
|
|
||||||
elif preset_purge_choice != '':
|
|
||||||
purge_choice = preset_purge_choice
|
|
||||||
if preset_block_choice == '':
|
|
||||||
block_choice = input("\nDo you want to block the room? (This prevents your server users re-entering the room.) y/n? ")
|
|
||||||
elif preset_block_choice != '':
|
|
||||||
block_choice = preset_block_choice
|
|
||||||
|
|
||||||
username = parse_username(user_ID)
|
|
||||||
|
|
||||||
if purge_choice == "y" or purge_choice == "Y" or purge_choice == "yes" or purge_choice == "Yes" or purge_choice == True:
|
|
||||||
purge_choice = "true"
|
|
||||||
elif purge_choice == "n" or purge_choice == "N" or purge_choice == "no" or purge_choice == "No" or purge_choice == False:
|
|
||||||
purge_choice = "false"
|
|
||||||
else:
|
|
||||||
print("Input invalid! exiting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if block_choice == "y" or block_choice == "Y" or block_choice == "yes" or block_choice == "Yes" or block_choice == True:
|
|
||||||
block_choice = "true"
|
|
||||||
elif block_choice == "n" or block_choice == "N" or block_choice == "no" or block_choice == "No" or block_choice == False:
|
|
||||||
block_choice = "false"
|
|
||||||
else:
|
|
||||||
print("Input invalid! exiting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
data = {
|
|
||||||
"new_room_user_id": f"@{username}:{hardcoded_variables.base_url}",
|
|
||||||
"room_name": new_room_name,
|
|
||||||
"message": message,
|
|
||||||
"block": bool(block_choice),
|
|
||||||
"purge": bool(purge_choice)
|
|
||||||
}
|
|
||||||
delete_room_url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/rooms/{internal_ID}"
|
|
||||||
response = requests.delete(delete_room_url, headers=headers, json=data, verify=True)
|
|
||||||
#print(response.text)
|
|
||||||
|
|
||||||
status = "null"
|
|
||||||
count = 0
|
|
||||||
sleep_time = 1
|
|
||||||
list_kicked_users = []
|
|
||||||
|
|
||||||
while status != "complete" and count < 8:
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
count += 1
|
|
||||||
sleep_time *= 2
|
|
||||||
check_status_url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/rooms/{internal_ID}/delete_status"
|
|
||||||
status_response = requests.get(check_status_url, headers=headers, verify=True)
|
|
||||||
#print(f"status_response: {status_response.text}")
|
|
||||||
output_json = status_response.json()
|
|
||||||
#print(f"output_json: {output_json}")
|
|
||||||
status = output_json["results"][0]["status"]
|
|
||||||
#print(f"status: {status}")
|
|
||||||
if status != "complete":
|
|
||||||
print(f"Sleeping for {sleep_time} seconds...")
|
|
||||||
|
|
||||||
if status == "complete":
|
|
||||||
print(f"{internal_ID} has been successfully shutdown!")
|
|
||||||
if str(output_json["results"][0]["shutdown_room"]["kicked_users"]) != '[]':
|
|
||||||
print("List of kicked users:")
|
|
||||||
for entry in output_json["results"][0]["shutdown_room"]["kicked_users"]:
|
|
||||||
list_kicked_users.append(entry)
|
|
||||||
print(entry)
|
|
||||||
else:
|
|
||||||
print(f"Failed to shutdown {internal_ID}!")
|
|
||||||
|
|
||||||
return list_kicked_users
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
#$ curl -H "Authorization: Bearer ACCESS_TOKEN" --data '{ "new_room_user_id": "@PC-Admin:perthchat.org", "room_name": "VIOLATION ROOM", "message": "YOU HAVE BEEN NAUGHTY!", "block": true, "purge": true }' -X DELETE 'https://matrix.perthchat.org/_synapse/admin/v2/rooms/!yUykDcYIEtrbSxOyPD:perthchat.org'
|
|
||||||
# {"delete_id":"efphJOtAxlBNtkGD"}
|
|
||||||
|
|
||||||
# Then check with:
|
|
||||||
# $ curl -H "Authorization: Bearer ACCESS_TOKEN" -kX GET 'https://matrix.perthchat.org/_synapse/admin/v2/rooms/!yUykDcYIEtrbSxOyPD:perthchat.org/delete_status'
|
|
||||||
# {"results":[{"delete_id":"yRjYjwoTOXOnRQPa","status":"complete","shutdown_room":{"kicked_users":["@michael:perthchat.org"],"failed_to_kick_users":[],"local_aliases":[],"new_room_id":"!AXTUBcSlehQuCidiZu:perthchat.org"}}]}
|
|
||||||
|
|
||||||
def shutdown_multiple_rooms():
|
|
||||||
print("Shutdown multiple rooms selected")
|
|
||||||
purge_list_location = input("\nPlease enter the path of the file or directory containing a newline seperated list of room ids: ")
|
|
||||||
file_list = []
|
|
||||||
# check if the input path is a directory or a file
|
|
||||||
if os.path.isdir(purge_list_location):
|
|
||||||
# iterate over all files in the directory
|
|
||||||
for filename in os.listdir(purge_list_location):
|
|
||||||
# construct full file path
|
|
||||||
file_path = os.path.join(purge_list_location, filename)
|
|
||||||
# add it to the list
|
|
||||||
file_list.append(file_path)
|
|
||||||
else:
|
|
||||||
# it's a single file
|
|
||||||
file_list.append(purge_list_location)
|
|
||||||
preset_user_ID = input("\nPlease enter the local username that will create a 'muted violation room' for your users (Example: michael): ")
|
|
||||||
preset_new_room_name = input("\nPlease enter the room name of the muted violation room your users will be sent to: ")
|
|
||||||
preset_message = input("\nPlease enter the shutdown message that will be displayed to users: ")
|
|
||||||
preset_purge_choice = input("\n Do you want to purge these rooms? (This deletes all the room history from your database.) y/n? ")
|
|
||||||
if preset_purge_choice.lower() in ["y", "yes", "Y", "Yes"]:
|
|
||||||
preset_purge_choice = True
|
|
||||||
elif preset_purge_choice.lower() in ["n", "no", "N", "No"]:
|
|
||||||
preset_purge_choice = False
|
|
||||||
preset_block_choice = input("\n Do you want to block these rooms? (This prevents your server users re-entering the room.) y/n? ")
|
|
||||||
if preset_block_choice.lower() in ["y", "yes", "Y", "Yes"]:
|
|
||||||
preset_block_choice = True
|
|
||||||
elif preset_block_choice.lower() in ["n", "no", "N", "No"]:
|
|
||||||
preset_block_choice = False
|
|
||||||
# Get the directory of the current script
|
|
||||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
room_list_data = []
|
|
||||||
for file in file_list:
|
|
||||||
print("Processing file: " + file)
|
|
||||||
# Change the current working directory
|
|
||||||
os.chdir(script_dir)
|
|
||||||
with open(file, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
room_list_data = room_list_data + data
|
|
||||||
# Deduplicate the room_list_data
|
|
||||||
room_list_data = [list(item) for item in set(tuple(row) for row in room_list_data)]
|
|
||||||
shutdown_confirmation = input("\n" + str(room_list_data) + "\n\nNumber of rooms being shutdown: " + str(len(room_list_data)) + "\n\nAre you sure you want to shutdown these rooms? y/n? ")
|
|
||||||
if shutdown_confirmation.lower() in ["y", "yes"]:
|
|
||||||
for room_id in room_list_data:
|
|
||||||
shutdown_room(room_id[0], preset_user_ID, preset_new_room_name, preset_message, preset_purge_choice, preset_block_choice)
|
|
||||||
time.sleep(10)
|
|
||||||
elif shutdown_confirmation.lower() in ["n", "no"]:
|
|
||||||
print("\nSkipping these files...\n")
|
|
||||||
else:
|
|
||||||
print("\nInvalid input, skipping these files...\n")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# See shutdown_room()
|
|
||||||
|
|
||||||
def delete_room(preset_internal_ID):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you want to delete (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
else:
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
headers = {"Authorization": "Bearer " + hardcoded_variables.access_token}
|
|
||||||
data = {"block": False, "purge": True}
|
|
||||||
url = f'https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/rooms/{internal_ID}'
|
|
||||||
response = requests.delete(url, headers=headers, data=json.dumps(data))
|
|
||||||
print("\n", response.text, "\n")
|
|
||||||
|
|
||||||
status = "null"
|
|
||||||
count = 0
|
|
||||||
sleep_time = 0.5
|
|
||||||
|
|
||||||
while status != "complete" and count < 8:
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
count += 1
|
|
||||||
sleep_time *= 2
|
|
||||||
url_status = f'https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/rooms/{internal_ID}/delete_status'
|
|
||||||
response = requests.get(url_status, headers=headers, verify=True)
|
|
||||||
response_json = response.json()
|
|
||||||
status = response_json["results"][0]["status"]
|
|
||||||
print("status: " + status)
|
|
||||||
if status != "complete":
|
|
||||||
print(f"Sleeping for {sleep_time} seconds...")
|
|
||||||
|
|
||||||
if status == "complete":
|
|
||||||
print(f"{internal_ID} has been successfully deleted!")
|
|
||||||
kicked_users = response_json["results"][0]["shutdown_room"]["kicked_users"]
|
|
||||||
if kicked_users:
|
|
||||||
print("List of kicked users:")
|
|
||||||
for user in kicked_users:
|
|
||||||
print(user)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
#$ curl -H "Authorization: Bearer ACCESS_TOKEN" --data '{ "block": false, "purge": true }' -X DELETE 'https://matrix.perthchat.org/_synapse/admin/v2/rooms/!yUykDcYIEtrbSxOyPD:perthchat.org'
|
|
||||||
# {"delete_id":"efphJOtAxlBNtkGD"}
|
|
||||||
|
|
||||||
# Then check with:
|
|
||||||
# $ curl -H "Authorization: Bearer ACCESS_TOKEN" -kX GET 'https://matrix.perthchat.org/_synapse/admin/v2/rooms/!yUykDcYIEtrbSxOyPD:perthchat.org/delete_status'
|
|
||||||
# {"results":[{"delete_id":"efphJOtAxlBNtkGD","status":"complete","shutdown_room":{"kicked_users":[],"failed_to_kick_users":[],"local_aliases":[],"new_room_id":null}}]}
|
|
||||||
|
|
||||||
def delete_multiple_rooms():
|
|
||||||
print("Delete multiple rooms selected")
|
|
||||||
purge_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
|
||||||
with open(purge_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
delete_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to delete these rooms? y/n? ")
|
|
||||||
#print("len(data[0]) - " + str(len(data[0])))
|
|
||||||
#print("data[0][0] - " + data[0][0])
|
|
||||||
if delete_confirmation == "y" or delete_confirmation == "Y" or delete_confirmation == "yes" or delete_confirmation == "Yes":
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
print("data[x][0] - " + data[x][0])
|
|
||||||
delete_room(data[x][0])
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
#time.sleep(2) # deleting a room is quicker then a full shutdown
|
|
||||||
|
|
||||||
if delete_confirmation == "n" or delete_confirmation == "N" or delete_confirmation == "no" or delete_confirmation == "No":
|
|
||||||
print("\nExiting...\n")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# See delete_room()
|
|
||||||
|
|
||||||
def purge_room_to_timestamp(preset_internal_ID, preset_timestamp):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of the room you want to delete (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
else:
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
|
|
||||||
if preset_timestamp == '':
|
|
||||||
timestamp = input("\nEnter the epoch timestamp in microseconds (Example: 1661058683000): ")
|
|
||||||
else:
|
|
||||||
timestamp = preset_timestamp
|
|
||||||
|
|
||||||
headers = {"Authorization": "Bearer " + hardcoded_variables.access_token, "Content-Type": "application/json"}
|
|
||||||
data = {"delete_local_events": False, "purge_up_to_ts": int(timestamp)}
|
|
||||||
url = f'https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/purge_history/{internal_ID}'
|
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, data=json.dumps(data))
|
|
||||||
print("\n", response.text, "\n")
|
|
||||||
|
|
||||||
response_json = response.json()
|
|
||||||
if "errcode" in response_json:
|
|
||||||
print(f"Error: {response_json['error']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
purge_id = response_json.get("purge_id")
|
|
||||||
status = "null"
|
|
||||||
count = 0
|
|
||||||
sleep_time = 0.5
|
|
||||||
|
|
||||||
while status != "complete" and count < 8:
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
count += 1
|
|
||||||
sleep_time *= 2
|
|
||||||
|
|
||||||
url_status = f'https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/purge_history_status/{purge_id}'
|
|
||||||
response = requests.get(url_status, headers=headers)
|
|
||||||
response_json = response.json()
|
|
||||||
|
|
||||||
status = response_json.get("status")
|
|
||||||
print("status: " + status)
|
|
||||||
|
|
||||||
if status != "complete":
|
|
||||||
print(f"Sleeping for {sleep_time} seconds...")
|
|
||||||
|
|
||||||
if status == "complete":
|
|
||||||
print(f"{internal_ID} has successfully had its history purged!")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
#$ curl --header "Authorization: Bearer syt_bW..." -X POST -H "Content-Type: application/json" -d '{ "delete_local_events": false, "purge_up_to_ts": 1661058683000 }' 'https://matrix.perthchat.org/_synapse/admin/v1/purge_history/!OnWgVbeuALuOEZowed:perthchat.org'
|
|
||||||
#{"purge_id":"rfWgHeCWWyDoOJZn"}
|
|
||||||
|
|
||||||
# Then check with:
|
|
||||||
#$ curl -H "Authorization: Bearer syt_bW..." -kX GET 'https://matrix.perthchat.org/_synapse/admin/v1/purge_history_status/rfWgHeCWWyDoOJZn'
|
|
||||||
#{"status":"complete"}
|
|
||||||
|
|
||||||
def purge_multiple_rooms_to_timestamp():
|
|
||||||
print("Purge the event history of multiple rooms to a specific timestamp selected")
|
|
||||||
purge_list_location = input("\nPlease enter the path of the file containing a newline seperated list of room ids: ")
|
|
||||||
with open(purge_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
preset_timestamp = input("\nPlease enter the epoche timestamp in milliseconds you wish to purge too (for example 1661058683000): ")
|
|
||||||
purge_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to purge the history of these rooms? y/n? ")
|
|
||||||
print("len(data[0]) - " + str(len(data[0])))
|
|
||||||
print("data[0][0] - " + data[0][0])
|
|
||||||
if purge_confirmation == "y" or purge_confirmation == "Y" or purge_confirmation == "yes" or purge_confirmation == "Yes":
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
print("data[x][0] - " + data[x][0])
|
|
||||||
purge_room_to_timestamp(data[x][0], preset_timestamp)
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
|
|
||||||
if purge_confirmation == "n" or purge_confirmation == "N" or purge_confirmation == "no" or purge_confirmation == "No":
|
|
||||||
print("\nExiting...\n")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# See purge_room_to_timestamp()
|
|
||||||
|
|
||||||
def get_block_status(preset_internal_ID):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of a room to examine if it's blocked (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
else:
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/rooms/{internal_ID}/block"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}"}
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, verify=True)
|
|
||||||
|
|
||||||
# Ensure the request was successful
|
|
||||||
if response.status_code == 200:
|
|
||||||
block_status = json.loads(response.text)['block']
|
|
||||||
else:
|
|
||||||
print(f"Error: Unable to fetch block status for room {internal_ID}")
|
|
||||||
block_status = None
|
|
||||||
|
|
||||||
return block_status
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X GET -H 'Authorization: Bearer ACCESS_TOKEN' 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!IdieserRBwPdaCGYuKk:matrix.org/block'
|
|
||||||
# {"block":false}
|
|
||||||
|
|
||||||
def set_block_status(preset_internal_ID, block):
|
|
||||||
if preset_internal_ID == '':
|
|
||||||
internal_ID = input("\nEnter the internal id of a room to block/unblock (Example: !OLkDvaYjpNrvmwnwdj:matrix.org): ")
|
|
||||||
else:
|
|
||||||
internal_ID = preset_internal_ID
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/rooms/{internal_ID}/block"
|
|
||||||
headers = {"Authorization": f"Bearer {hardcoded_variables.access_token}", "Content-Type": "application/json"}
|
|
||||||
data = {"block": block}
|
|
||||||
|
|
||||||
response = requests.put(url, headers=headers, json=data, verify=True)
|
|
||||||
|
|
||||||
# Ensure the request was successful
|
|
||||||
if response.status_code == 200:
|
|
||||||
block_status = json.loads(response.text)['block']
|
|
||||||
if block_status == block and block == True:
|
|
||||||
print(f"Successfully blocked room {internal_ID}")
|
|
||||||
elif block_status == block and block == False:
|
|
||||||
print(f"Successfully unblocked room {internal_ID}")
|
|
||||||
else:
|
|
||||||
print(f"Failed to set block status for room {internal_ID} to {block}")
|
|
||||||
else:
|
|
||||||
print(f"Error: Unable to set block status for room {internal_ID}")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
#$ curl -X PUT -H 'Authorization: Bearer ACCESS_TOKEN' -H 'Content-Type: application/json' -d '{"block": true}' 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!UQEvAyhSqHxohkIyvm:perthchat.org/block'
|
|
||||||
#{"block":true}
|
|
@@ -1,138 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import csv
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
import datetime
|
|
||||||
import hardcoded_variables
|
|
||||||
|
|
||||||
def delete_block_media():
|
|
||||||
# Take media_id from user
|
|
||||||
media_id = input("\nEnter the media_id of the media you would like to delete and block on your server. (Example: For this media https://matrix.perthchat.org/_matrix/media/r0/download/matrix.org/eDmjusOjnHyFPOYGxlrOsULJ the media_id is 'eDmjusOjnHyFPOYGxlrOsULJ'): ")
|
|
||||||
remote_server = input("\nEnter the remote servers URL without the 'https://' (Example: matrix.org): ")
|
|
||||||
# find filesystem_id from database
|
|
||||||
command_collect_filesystem_id = "ssh " + hardcoded_variables.homeserver_url + """ "/matrix/postgres/bin/cli-non-interactive --dbname=synapse -t -c 'SELECT DISTINCT filesystem_id FROM remote_media_cache WHERE media_id = '\\''""" + media_id + """'\\'" | xargs"""
|
|
||||||
print(command_collect_filesystem_id)
|
|
||||||
process_collect_filesystem_id = subprocess.run([command_collect_filesystem_id], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
filesystem_id = process_collect_filesystem_id.stdout
|
|
||||||
print(process_collect_filesystem_id.stdout)
|
|
||||||
# list the target files on disk
|
|
||||||
command_collect_thumbnails = "ssh " + hardcoded_variables.homeserver_url + ' "find /matrix/synapse/storage/media-store/remote_thumbnail/' + remote_server + '/' + filesystem_id[:2] + "/" + filesystem_id[2:4] + "/" + filesystem_id[4:].rstrip() + """ -type f -printf '%p\\n'\""""
|
|
||||||
print(command_collect_thumbnails)
|
|
||||||
process_collect_thumbnails = subprocess.run([command_collect_thumbnails], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
remote_thumbnails_list = process_collect_thumbnails.stdout
|
|
||||||
print(remote_thumbnails_list)
|
|
||||||
command_content_location = "ssh " + hardcoded_variables.homeserver_url + ' "ls /matrix/synapse/storage/media-store/remote_content/' + remote_server + '/' + filesystem_id[:2] + "/" + filesystem_id[2:4] + "/" + filesystem_id[4:].rstrip() + '"'
|
|
||||||
print(command_content_location)
|
|
||||||
process_content_location = subprocess.run([command_content_location], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
remote_content_location = process_content_location.stdout
|
|
||||||
print(remote_content_location)
|
|
||||||
# Zero the target files on disk then chattr +i them
|
|
||||||
for line in remote_thumbnails_list.split('\n'):
|
|
||||||
if line:
|
|
||||||
command_zero_thumbnails = 'ssh ' + hardcoded_variables.homeserver_url + ' "true > ' + line + '"'
|
|
||||||
print(command_zero_thumbnails)
|
|
||||||
process_zero_thumbnails = subprocess.run(command_zero_thumbnails, shell=True)
|
|
||||||
print(process_zero_thumbnails.stdout)
|
|
||||||
command_make_thumbnail_immutable = 'ssh ' + hardcoded_variables.homeserver_url + ' "chattr +i ' + line + '"'
|
|
||||||
print(command_make_thumbnail_immutable)
|
|
||||||
process_make_thumbnail_immutable = subprocess.run(command_make_thumbnail_immutable, shell=True)
|
|
||||||
print(process_make_thumbnail_immutable.stdout)
|
|
||||||
command_zero_media = 'ssh ' + hardcoded_variables.homeserver_url + ' "true > ' + remote_content_location.rstrip() + '"'
|
|
||||||
print(command_zero_media)
|
|
||||||
process_remove_media = subprocess.run(command_zero_media, shell=True)
|
|
||||||
print(process_remove_media.stdout)
|
|
||||||
command_make_content_immutable = 'ssh ' + hardcoded_variables.homeserver_url + ' "chattr +i ' + remote_content_location.rstrip() + '"'
|
|
||||||
print(command_make_content_immutable)
|
|
||||||
process_make_content_immutable = subprocess.run(command_make_content_immutable, shell=True)
|
|
||||||
print(process_make_content_immutable.stdout)
|
|
||||||
|
|
||||||
# Example, first use the media_id to find the filesystem_id:
|
|
||||||
# $ ssh matrix.perthchat.org "/matrix/postgres/bin/cli-non-interactive --dbname=synapse -t -c 'SELECT DISTINCT filesystem_id FROM remote_media_cache WHERE media_id = '\''eDmjusOjnHyFPOYGxlrOsULJ'\'" | xargs
|
|
||||||
# ehckzWWeUkDhhPfNFkcfCFNv
|
|
||||||
|
|
||||||
# Then use that filesystem_id to locate the remote file and all it's thumbnails:
|
|
||||||
# $ ssh matrix.perthchat.org "find /matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv -type f -printf '%p\n'"
|
|
||||||
#/matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv/32-32-image-jpeg-crop
|
|
||||||
#/matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv/640-480-image-jpeg-scale
|
|
||||||
# ...
|
|
||||||
# $ ssh matrix.perthchat.org "ls /matrix/synapse/storage/media-store/remote_content/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv"
|
|
||||||
# /matrix/synapse/storage/media-store/remote_content/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv
|
|
||||||
|
|
||||||
# Then zero each file and make it immutable:
|
|
||||||
# $ ssh matrix.perthchat.org "true > /matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv/32-32-image-jpeg-crop"
|
|
||||||
# $ ssh matrix.perthchat.org "chattr +i /matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv/32-32-image-jpeg-crop"
|
|
||||||
# $ ssh matrix.perthchat.org "true > /matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv/640-480-image-jpeg-scale"
|
|
||||||
# $ ssh matrix.perthchat.org "chattr +i /matrix/synapse/storage/media-store/remote_thumbnail/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv/640-480-image-jpeg-scale"
|
|
||||||
# ...
|
|
||||||
# $ ssh matrix.perthchat.org "true > /matrix/synapse/storage/media-store/remote_content/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv"
|
|
||||||
# $ ssh matrix.perthchat.org "chattr +i /matrix/synapse/storage/media-store/remote_content/matrix.org/eh/ck/zWWeUkDhhPfNFkcfCFNv"
|
|
||||||
|
|
||||||
def purge_remote_media_repo():
|
|
||||||
purge_from = int(input("\nEnter the number of days to purge from: "))
|
|
||||||
purge_too = int(input("\nEnter the number of days to purge too: "))
|
|
||||||
|
|
||||||
while purge_from >= purge_too:
|
|
||||||
# Calculate the epoch timestamp for 'purge_from' days ago
|
|
||||||
epoch_time = int((datetime.datetime.now() - datetime.timedelta(days=purge_from)).timestamp())
|
|
||||||
# Convert to milliseconds (as per your original code)
|
|
||||||
epoch_time_millis = epoch_time * 1000
|
|
||||||
|
|
||||||
# Make the request
|
|
||||||
headers = {"Authorization": "Bearer " + hardcoded_variables.access_token}
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/purge_media_cache"
|
|
||||||
params = {"before_ts": epoch_time_millis}
|
|
||||||
response = requests.post(url, headers=headers, params=params)
|
|
||||||
|
|
||||||
print(response.text)
|
|
||||||
|
|
||||||
purge_from -= 1
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# This loop is quite slow, our server was having disk issues.
|
|
||||||
print("Done! :)")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ date --date '149 days ago' +%s
|
|
||||||
# 1589442217
|
|
||||||
# $ curl -X POST --header "Authorization: Bearer ACCESS_TOKEN" 'https://matrix.perthchat.org/_synapse/admin/v1/purge_media_cache?before_ts=1589439628000'
|
|
||||||
|
|
||||||
def prepare_database_copy_of_multiple_rooms():
|
|
||||||
print("Preparing database copying of events from multiple rooms selected\n")
|
|
||||||
print("This command needs to be run on the target server as root, it will setup postgres commands to download the join-leave events and all-events from a list of rooms.\n\nIt mounts a ramdisk beforehand at /matrix/postgres/data/ramdisk\n\nThis function is only compatible with Spantaleevs Matrix deploy script: https://github.com/spantaleev/matrix-docker-ansible-deploy\n")
|
|
||||||
database_copy_list_location = input("Please enter the path of the file containing a newline seperated list of room ids: ")
|
|
||||||
with open(database_copy_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
|
|
||||||
make_ramdisk_command = "mkdir /matrix/postgres/data/ramdisk; mount -t ramfs -o size=512m ramfs /matrix/postgres/data/ramdisk; chown -R matrix:matrix /matrix/postgres/data/ramdisk"
|
|
||||||
make_ramdisk_command_process = subprocess.run([make_ramdisk_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
print(make_ramdisk_command_process.stdout)
|
|
||||||
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
print(data[x][0])
|
|
||||||
roomid_trimmed = data[x][0]
|
|
||||||
roomid_trimmed = roomid_trimmed.replace('!', '')
|
|
||||||
roomid_trimmed = roomid_trimmed.replace(':', '-')
|
|
||||||
os.mkdir("/matrix/postgres/data/ramdisk/" + roomid_trimmed)
|
|
||||||
touch_command = "touch /matrix/postgres/data/ramdisk/" + roomid_trimmed + "/dump_room_data.sql"
|
|
||||||
touch_command_process = subprocess.run([touch_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
print(touch_command_process.stdout)
|
|
||||||
sql_file_contents = "\set ROOMID '" + data[x][0] + "'\nCOPY (SELECT * FROM current_state_events JOIN room_memberships ON room_memberships.event_id = current_state_events.event_id WHERE current_state_events.room_id = :'ROOMID') TO '/var/lib/postgresql/data/ramdisk/" + roomid_trimmed + "/user_join-leave.csv' WITH CSV HEADER;\nCOPY (SELECT * FROM event_json WHERE room_id=:'ROOMID') TO '/var/lib/postgresql/data/ramdisk/" + roomid_trimmed + "/room_events.csv' WITH CSV HEADER;"
|
|
||||||
print(sql_file_contents)
|
|
||||||
sql_file_location = "/matrix/postgres/data/ramdisk/" + roomid_trimmed + "/dump_room_data.sql"
|
|
||||||
sql_file = open(sql_file_location,"w+")
|
|
||||||
sql_file.write(sql_file_contents)
|
|
||||||
sql_file.close()
|
|
||||||
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
chown_command = "chown -R matrix:matrix /matrix/postgres/data/ramdisk; docker restart matrix-postgres"
|
|
||||||
chown_command_process = subprocess.run([chown_command], shell=True, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
print(chown_command_process.stdout)
|
|
||||||
|
|
||||||
print("\nThe sql query files have been generated, as postgres user in container run:\n# docker exec -it matrix-postgres /bin/bash\nbash-5.0$ export PGPASSWORD=your-db-password\nbash-5.0$ for f in /var/lib/postgresql/data/ramdisk/*/dump_room_data.sql; do psql --host=127.0.0.1 --port=5432 --username=synapse -w -f $f; done\n\nAfter copying the data to a cloud location law enforcement can access, clean up the ramdisk like so:\n# rm -r /matrix/postgres/data/ramdisk/*\n# umount /matrix/postgres/data/ramdisk")
|
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
539
user_commands.py
539
user_commands.py
@@ -1,539 +0,0 @@
|
|||||||
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import csv
|
|
||||||
import hardcoded_variables
|
|
||||||
import socket
|
|
||||||
|
|
||||||
def parse_username(username):
|
|
||||||
tail_end = ':' + hardcoded_variables.base_url
|
|
||||||
username = username.replace('@','')
|
|
||||||
username = username.replace(tail_end,'')
|
|
||||||
return username
|
|
||||||
|
|
||||||
def deactivate_account(preset_username, erase=False):
|
|
||||||
if preset_username == '':
|
|
||||||
username = input("\nPlease enter the username to deactivate: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
else:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/deactivate/@{username}:{hardcoded_variables.base_url}"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {hardcoded_variables.access_token}"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"erase": erase
|
|
||||||
}
|
|
||||||
|
|
||||||
print("\n" + url + "\n")
|
|
||||||
response = requests.post(url, headers=headers, data=json.dumps(data), verify=True)
|
|
||||||
|
|
||||||
if response.status_code in [200,201]:
|
|
||||||
print("Successfully deactivated account.")
|
|
||||||
else:
|
|
||||||
print(f"Error deactivating account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" "https://matrix.perthchat.org/_synapse/admin/v1/deactivate/@billybob:perthchat.org" --data '{"erase": true}'
|
|
||||||
|
|
||||||
def deactivate_multiple_accounts():
|
|
||||||
print("Deactivate multiple user accounts selected")
|
|
||||||
user_list_location = input("\nPlease enter the path of the file containing a csv list of names: ")
|
|
||||||
with open(user_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
print(data)
|
|
||||||
delete_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to deactivate these users? y/n?\n")
|
|
||||||
erase_confirmation = input("\nDo you want to also erase all these users' data? y/n?\n")
|
|
||||||
if erase_confirmation in ["y", "Y", "yes", "Yes", "YES"]:
|
|
||||||
erase = True
|
|
||||||
elif erase_confirmation in ["n", "N", "no", "No", "NO"]:
|
|
||||||
erase = False
|
|
||||||
else:
|
|
||||||
print("\nIncorrect input detected, please select 'y' or 'n'!\n")
|
|
||||||
return
|
|
||||||
#print(len(data[0]))
|
|
||||||
#print(data[0][0])
|
|
||||||
if delete_confirmation ["y", "Y", "yes", "Yes", "YES"]:
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
#print(data[0][x])
|
|
||||||
status = deactivate_account(data[x][0], erase)
|
|
||||||
#print(status)
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
time.sleep(5)
|
|
||||||
elif delete_confirmation in ["n", "N", "no", "No", "NO"]:
|
|
||||||
print("\nExiting...\n")
|
|
||||||
else:
|
|
||||||
print("\nIncorrect input detected, please select 'y' or 'n'!\n")
|
|
||||||
|
|
||||||
def create_account(preset_username, preset_password):
|
|
||||||
if len(preset_username) == 0 and len(preset_password) == 0:
|
|
||||||
username = input("\nPlease enter the username to create: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
user_password = input("Please enter the password for this account: ")
|
|
||||||
elif len(preset_username) > 0 and len(preset_password) > 0:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
user_password = preset_password
|
|
||||||
else:
|
|
||||||
print("\nError with user/pass file data, skipping...\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/users/@{username}:{hardcoded_variables.base_url}"
|
|
||||||
url += f"?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"password": user_password
|
|
||||||
}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.put(url, headers=headers, data=json.dumps(data), verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 201:
|
|
||||||
print("Successfully created account.")
|
|
||||||
elif response.status_code == 200:
|
|
||||||
print("Account already exists.")
|
|
||||||
else:
|
|
||||||
print(f"Error creating account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
create_account_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return create_account_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -kX PUT -H 'Content-Type: application/json' -d '{"password": "user_password","admin": false,"deactivated": false}' https://matrix.perthchat.org/_synapse/admin/v2/users/@billybob:perthchat.org?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def create_multiple_accounts():
|
|
||||||
print("Create multiple user accounts selected")
|
|
||||||
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of Matrix usernames: ")
|
|
||||||
with open(user_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
print(len(data))
|
|
||||||
create_confirmation = input("\n" + str(data) + "\n\nAre you sure you want to create these users? y/n?\n")
|
|
||||||
if create_confirmation == "y" or create_confirmation == "Y" or create_confirmation == "yes" or create_confirmation == "Yes":
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
print(data[x][0])
|
|
||||||
create_account(data[x][0],data[x][1])
|
|
||||||
x += 1
|
|
||||||
#print(x)
|
|
||||||
time.sleep(10)
|
|
||||||
if create_confirmation == "n" or create_confirmation == "N" or create_confirmation == "no" or create_confirmation == "No":
|
|
||||||
print("\nExiting...\n")
|
|
||||||
|
|
||||||
def reset_password(preset_username, preset_password):
|
|
||||||
if len(preset_username) == 0 and len(preset_password) == 0:
|
|
||||||
username = input("\nPlease enter the username for the password reset: ")
|
|
||||||
password = input("Please enter the password to set: ")
|
|
||||||
else:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
password = preset_password
|
|
||||||
|
|
||||||
username = parse_username(username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/reset_password/@" + username + f":{hardcoded_variables.base_url}?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
data = {'new_password': password, 'logout_devices': True}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, data=json.dumps(data), verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error resetting password: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
reset_password_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return reset_password_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X POST -H 'Content-Type: application/json' -d '{"new_password": "dogpoo", "logout_devices": true}' https://matrix.perthchat.org/_synapse/admin/v1/reset_password/@dogpoo:perthchat.org?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def set_user_server_admin(preset_username):
|
|
||||||
if len(preset_username) == 0:
|
|
||||||
print("\nBe aware that you need to set at least 1 user to server admin already by editing the database in order to use this command. See https://github.com/PC-Admin/PC-Admins-Synapse-Moderation-Tool/blob/master/README.md for details on how to do this.")
|
|
||||||
username = input("\nPlease enter the username you want to promote to server admin: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
elif len(preset_username) > 0:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
|
|
||||||
# tried setting 'admin: false' here but it failed and promoted the user instead!
|
|
||||||
server_admin_result = "true"
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/admin"
|
|
||||||
url += f"?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"admin": server_admin_result
|
|
||||||
}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.put(url, headers=headers, data=json.dumps(data), verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error setting user as server admin: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
set_user_server_admin_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return set_user_server_admin_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -kX POST -H 'Content-Type: application/json' -d '{"admin": "true"}' https://matrix.perthchat.org/_synapse/admin/v2/users/@dogpoo:perthchat.org?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def whois_account(preset_username):
|
|
||||||
if preset_username == '':
|
|
||||||
username = input("\nPlease enter the username you wish to whois: ")
|
|
||||||
elif preset_username != '':
|
|
||||||
username = preset_username
|
|
||||||
username = parse_username(username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_matrix/client/r0/admin/whois/@{username}:{hardcoded_variables.base_url}"
|
|
||||||
url += f"?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error retrieving account info: {response.status_code}, {response.text}\n")
|
|
||||||
|
|
||||||
whois_account_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return whois_account_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -kXGET https://matrix.perthchat.org/_matrix/client/r0/admin/whois/@dogpoo:perthchat.org?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def whois_multiple_accounts():
|
|
||||||
print("Whois multiple user accounts selected")
|
|
||||||
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of Matrix usernames: ")
|
|
||||||
with open(user_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
print(len(data))
|
|
||||||
|
|
||||||
print("\n" + str(data))
|
|
||||||
|
|
||||||
output_file = None
|
|
||||||
if len(data) > 10:
|
|
||||||
file_confirmation = input("\nThere are more than 10 users. Would you like to save the output to a file? y/n?\n")
|
|
||||||
if file_confirmation.lower() in ("y", "yes"):
|
|
||||||
output_file = input("\nPlease enter the desired output file path:\n")
|
|
||||||
|
|
||||||
whois_confirmation = input("\n\nAre you sure you want to whois all of these users? y/n?\n")
|
|
||||||
|
|
||||||
if whois_confirmation.lower() in ("y", "yes"):
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
output = whois_account(data[x][0])
|
|
||||||
|
|
||||||
# if output file is specified, append to file
|
|
||||||
if output_file:
|
|
||||||
with open(output_file, 'a') as f:
|
|
||||||
f.write(output + "\n")
|
|
||||||
x += 1
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
if whois_confirmation.lower() in ("n", "no"):
|
|
||||||
print("\nExiting...\n")
|
|
||||||
|
|
||||||
if output_file and os.path.isfile(output_file):
|
|
||||||
print(f"Output saved to {output_file}")
|
|
||||||
|
|
||||||
def list_joined_rooms(preset_username):
|
|
||||||
if preset_username == '':
|
|
||||||
username = input("\nPlease enter the username you wish to query: ")
|
|
||||||
elif preset_username != '':
|
|
||||||
username = preset_username
|
|
||||||
|
|
||||||
username = parse_username(username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@" + username + f":{hardcoded_variables.base_url}/joined_rooms?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error querying joined rooms: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
joined_rooms_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return joined_rooms_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -kXGET https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/joined_rooms?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def list_accounts():
|
|
||||||
deactivated_choice = input("Do you want to include deactivated accounts y/n? ")
|
|
||||||
guest_choice = input("Do you want to include guest accounts y/n? ")
|
|
||||||
|
|
||||||
deactivated_string = "deactivated=false" if deactivated_choice.lower() not in ["y", "yes"] else "deactivated=true"
|
|
||||||
guest_string = "guest=false" if guest_choice.lower() not in ["y", "yes"] else "guest=true"
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/users?from=0&limit=1000000&{guest_string}&{deactivated_string}&access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
users = response.json()
|
|
||||||
number_of_users = len(users)
|
|
||||||
print("\nTotal amount of users: " + str(number_of_users))
|
|
||||||
|
|
||||||
if number_of_users < 100:
|
|
||||||
print(users)
|
|
||||||
else:
|
|
||||||
accounts_output_file = input("\nThere are too many users to list here, please specify a filename to print this data too: ")
|
|
||||||
with open(accounts_output_file, "w") as f:
|
|
||||||
json.dump(users, f, indent=4)
|
|
||||||
else:
|
|
||||||
print(f"Error retrieving users list: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -kXGET "https://matrix.perthchat.org/_synapse/admin/v2/users?from=0&limit=10&guests=false&access_token=ACCESS_TOKEN"
|
|
||||||
|
|
||||||
# NEED A MENU OPTION FOR THIS:
|
|
||||||
|
|
||||||
def query_account(preset_username):
|
|
||||||
if len(preset_username) == 0:
|
|
||||||
username = input("\nPlease enter the username to query: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
elif len(preset_username) > 0:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v2/users/@{username}:{hardcoded_variables.base_url}?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
query_account_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return query_account_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -kX GET https://matrix.perthchat.org/_synapse/admin/v2/users/@billybob:perthchat.org?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def query_multiple_accounts():
|
|
||||||
print("Query multiple user accounts selected")
|
|
||||||
user_list_location = input("\nPlease enter the path of the file containing a newline seperated list of Matrix usernames: ")
|
|
||||||
with open(user_list_location, newline='') as f:
|
|
||||||
reader = csv.reader(f)
|
|
||||||
data = list(reader)
|
|
||||||
print(len(data))
|
|
||||||
|
|
||||||
print("\n" + str(data))
|
|
||||||
|
|
||||||
output_file = None
|
|
||||||
if len(data) > 10:
|
|
||||||
file_confirmation = input("\nThere are more than 10 users. Would you like to save the output to a file? y/n?\n")
|
|
||||||
if file_confirmation.lower() in ("y", "yes"):
|
|
||||||
output_file = input("\nPlease enter the desired output file path:\n")
|
|
||||||
|
|
||||||
query_confirmation = input("\n\nAre you sure you want to query all of these users? y/n?\n")
|
|
||||||
|
|
||||||
if query_confirmation.lower() in ("y", "yes"):
|
|
||||||
x = 0
|
|
||||||
while x <= (len(data) - 1):
|
|
||||||
output = query_account(data[x][0])
|
|
||||||
|
|
||||||
# if output file is specified, append to file
|
|
||||||
if output_file:
|
|
||||||
with open(output_file, 'a') as f:
|
|
||||||
f.write(output + "\n")
|
|
||||||
x += 1
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
if query_confirmation.lower() in ("n", "no"):
|
|
||||||
print("\nExiting...\n")
|
|
||||||
|
|
||||||
if output_file and os.path.isfile(output_file):
|
|
||||||
print(f"Output saved to {output_file}")
|
|
||||||
|
|
||||||
def quarantine_users_media():
|
|
||||||
username = input("\nPlease enter the username of the user who's media you want to quarantine: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/user/@{username}:{hardcoded_variables.base_url}/media/quarantine?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
print("\n" + url + "\n")
|
|
||||||
response = requests.post(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
print(response.text)
|
|
||||||
else:
|
|
||||||
print(f"Error quarantining user's media: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X POST https://matrix.perthchat.org/_synapse/admin/v1/user/@dogpoo:perthchat.org/media/quarantine?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def collect_account_data(preset_username):
|
|
||||||
if len(preset_username) == 0:
|
|
||||||
username = input("\nPlease enter the username to collect account data: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
elif len(preset_username) > 0:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/accountdata?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
account_data_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return account_data_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X GET https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/accountdata?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def list_account_pushers(preset_username):
|
|
||||||
if len(preset_username) == 0:
|
|
||||||
username = input("\nPlease enter the username to list all pushers: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
elif len(preset_username) > 0:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/pushers?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
pusher_data_dict = json.loads(response.text)
|
|
||||||
|
|
||||||
return pusher_data_dict
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X GET https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/pushers
|
|
||||||
|
|
||||||
def get_rate_limit():
|
|
||||||
username = input("\nPlease enter the username to get its ratelimiting: ")
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/override_ratelimit?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
print(response.text)
|
|
||||||
else:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X GET https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/override_ratelimit?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def set_rate_limit():
|
|
||||||
username = input("\nPlease enter the username to adjust its ratelimiting: ")
|
|
||||||
|
|
||||||
messages_per_second = input("\nPlease enter the desired messages per second: ")
|
|
||||||
burst_count = input("\nPlease enter the desired burst count: ")
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/override_ratelimit?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
data = {
|
|
||||||
"messages_per_second": int(messages_per_second),
|
|
||||||
"burst_count": int(burst_count)
|
|
||||||
}
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, data=json.dumps(data), verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
print(response.text)
|
|
||||||
else:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X POST -H "Content-Type: application/json" -d '{"messages_per_second": 0,"burst_count": 0}' https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/override_ratelimit?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def delete_rate_limit():
|
|
||||||
username = input("\nPlease enter the username to delete its ratelimiting: ")
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/override_ratelimit?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.delete(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
print(response.text)
|
|
||||||
else:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X DELETE https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/override_ratelimit?access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def check_user_account_exists(preset_username):
|
|
||||||
if len(preset_username) == 0:
|
|
||||||
username = input("\nPlease enter the username to check if it exists: ")
|
|
||||||
username = parse_username(username)
|
|
||||||
elif len(preset_username) > 0:
|
|
||||||
username = parse_username(preset_username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/username_available?username={username}&access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
#print("\n" + url + "\n")
|
|
||||||
response = requests.get(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
#print("User ID is available.")
|
|
||||||
return False
|
|
||||||
elif response.status_code == 400:
|
|
||||||
#print(f"User ID already exists.")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f"Error querying account: {response.status_code}, {response.text}")
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# $ curl -X GET /_synapse/admin/v1/username_available?username=dogpoo&access_token=ACCESS_TOKEN
|
|
||||||
|
|
||||||
def shadow_ban_account(preset_username):
|
|
||||||
if preset_username == '':
|
|
||||||
username = input("\nPlease enter the username you wish to shadow ban: ")
|
|
||||||
elif preset_username != '':
|
|
||||||
username = preset_username
|
|
||||||
username = parse_username(username)
|
|
||||||
|
|
||||||
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/users/@{username}:{hardcoded_variables.base_url}/shadow_ban"
|
|
||||||
url += f"?access_token={hardcoded_variables.access_token}"
|
|
||||||
|
|
||||||
response = requests.post(url, verify=True)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
print(f"Error shadow banning account: {response.status_code}, {response.text}\n")
|
|
||||||
|
|
||||||
return json.loads(response.text)
|
|
||||||
|
|
||||||
# Example:
|
|
||||||
# curl -XPOST -H "Content-Type: application/json" 'https://matrix.perthchat.org/_synapse/admin/v1/users/@dogpoo:perthchat.org/shadow_ban?access_token=ACCESS_TOKEN'
|
|
Reference in New Issue
Block a user