Compare commits

..

81 Commits
v1.0 ... master

Author SHA1 Message Date
Michael
0d4b3744f1
new username 2023-09-06 12:24:35 +08:00
PC-Admin
6b7b5bf261 fix conditional error with deactivate all 2023-08-27 02:36:11 +08:00
PC-Admin
7475e38388 add example of remote media purging, create new function for sending server notices (untested). 2023-08-23 18:36:40 +08:00
PC-Admin
d986fb424e attempt to straighten text ui out 2023-08-20 19:33:52 +08:00
PC-Admin
d3e858a3ed update README.md 2023-08-20 19:29:06 +08:00
PC-Admin
7c19dfbf94 added event report functions and updated docs/ui 2023-08-20 19:10:54 +08:00
PC-Admin
adc32b2b27 add debug line 2023-08-18 21:31:53 +08:00
PC-Admin
56373d68db docs update 2023-08-18 20:48:46 +08:00
PC-Admin
1be96637ee update formatting 2023-08-18 20:04:16 +08:00
PC-Admin
827ea1d2e9 update formatting 2023-08-18 20:01:05 +08:00
PC-Admin
7b1f79cf7f update docs 2023-08-18 19:57:14 +08:00
PC-Admin
72cae1f88b update README.md 2023-08-18 18:53:14 +08:00
PC-Admin
612f67dd70 move one touch reporting section back to README.md 2023-08-18 18:46:48 +08:00
PC-Admin
c8e96bf82d add jank docs file chatgpt wrote 2023-08-18 18:44:05 +08:00
PC-Admin
f3ed15068e add basic docs files, needs expanding 2023-08-18 18:17:19 +08:00
PC-Admin
771d1b78e4 parse usernames in new rate limiting functions 2023-08-18 17:27:56 +08:00
PC-Admin
811c6d2b35 pass blank preset, fixes #4 2023-08-18 17:11:32 +08:00
PC-Admin
401206c8c5 you say defect, i say feature. script won't interactively take variables anymore. fixes #3 2023-08-18 17:02:50 +08:00
PC-Admin
e8c9f7f9a1 derp 2023-08-18 16:28:31 +08:00
PC-Admin
7afa95e888 add enviroment and better prompting 2023-08-18 16:27:28 +08:00
PC-Admin
34c3dc503a move example variable file to saner location 2023-08-18 16:12:54 +08:00
PC-Admin
a38e042b33 add new find_account_with_threepid function. fix new dynamic rdlist location sections i missed. 2023-08-07 20:16:55 +08:00
PC-Admin
2f186dffc9 variable for dynamic rdlist location. make block recommended tags output prettier. minor bug fixes. 2023-08-07 19:26:58 +08:00
PC-Admin
020289cd4d default testing mode to False, add testing mode warning for user report section 2023-08-03 20:53:16 +08:00
PC-Admin
f2f529942b neaten up incident reporting output 2023-08-03 18:30:53 +08:00
PC-Admin
82586a45bf fix for returning homeserver contact emails and whois emails 2023-08-03 16:04:30 +08:00
PC-Admin
fcf344a921 avoid incident report prompt if no remote users found. 2023-08-02 18:00:04 +08:00
PC-Admin
f5fd56860d fix bug where a single whois email is returned as an array of letters. 2023-08-02 17:09:31 +08:00
PC-Admin
df4d2ca3ab cleanup README.md 2023-08-02 01:11:29 +08:00
Michael
4e92aeff87
Update README.md with incident report pic 2023-08-02 00:30:28 +08:00
PC-Admin
0a583d7918 seperate collection of user reports and sending of incident reports for rdlist users into seperate functions. 2023-08-02 00:27:12 +08:00
PC-Admin
a48c587c84 update report details prompt. add forgotten rooms to automatically generated user reports. 2023-08-01 21:29:38 +08:00
PC-Admin
21b9653aad avoid encrypting ZIP files as pubkey encryption would be better 2023-08-01 13:20:36 +08:00
PC-Admin
c1bfa32e2d fix busted recommended tags module. generate primitive report summaries. use incident_dict like disctionary for all_local_users and all_remote_users. 2023-07-31 21:36:44 +08:00
PC-Admin
65f297f594 fix example hardcoded_variables file 2023-07-31 09:26:47 +08:00
PC-Admin
dd6d0abd15 default to not erasing user data on deactivation 2023-07-30 01:11:40 +08:00
PC-Admin
fee582cef0 update README.md 2023-07-30 00:10:47 +08:00
PC-Admin
1598d73599 dont promote rdlist bot user to server admin, there's no point yet. add matrix messaging for incident reports via matrix-nio. hard code rdlist bot password. 2023-07-29 23:46:32 +08:00
PC-Admin
81739a456d add shadow ban function. ask if admin wants to shadow ban users before mass rdlist shutdowns. move example room/user lists into seperate folder. 2023-07-28 10:36:58 +08:00
PC-Admin
115caceaf7 add export function for multiple rooms. avoid printing total_list_kicked_users twice. expand menu numbering to allow for a lot more new functions. 2023-07-28 05:15:46 +08:00
PC-Admin
acf4bffc1a add functions to examine if room is alrady blocked and to block/unblock rooms. dramatic speedup for applying rdlist by skipping already blocked rooms and just blocking unknown rooms instead of performing a full shutdown. add report_details section to user reporting to collect information from the admin about why the report was generated. add rdlist tag descriptions to allow for automated report_details generation 2023-07-27 10:00:40 +08:00
PC-Admin
ad87420546 add function to return rdlist tags associated with a room 2023-07-25 23:14:53 +08:00
PC-Admin
b0b812f3af update README.md 2023-07-25 22:32:57 +08:00
PC-Admin
dbb9821a5c add function to return list of all or just local members of a room. silence a lot of functions terminal output. adjust incident reports module to handle sending 1 email per homeserver with all offending users, rooms and tag details combined. allow automatic generation of reports for all users when blocking rdlist tags. just block unknown rooms instead of block+purge. collect list of all local users present in rdlist rooms for batch reporting/deactivation. add statistics about rdlist blocking. divide room states/details into both dms and larger rooms. add save_to_file boolean to state export module to avoid writing it to file, also pass variable to customise file location. 2023-07-25 21:52:08 +08:00
PC-Admin
a61a4e88af whoops wrong numbers 2023-07-24 07:01:41 +08:00
PC-Admin
5082482159 move lookup admin email function to report commands 2023-07-24 06:57:58 +08:00
PC-Admin
b2bbacc46d add incident report function for automatically alerting other homeserver administrators, move lookup_homeserver_admin_email() function to report_commands.py 2023-07-24 06:02:27 +08:00
PC-Admin
6454a2e9f1 add email settings to hardcoded_variables.py 2023-07-24 03:30:55 +08:00
PC-Admin
492b384a92 add generic email send function for automated reporting, hardcode matrix.org abuse email because... well I wanna stay in their good books lol 2023-07-24 02:59:49 +08:00
PC-Admin
63dc5bd6d8 add function to automatically lookup a homeserver admins email address, either the easy way (MSC1929) or the hard way (Your domain registrars whois email) 2023-07-24 01:44:41 +08:00
PC-Admin
3ad8469b5f move ipinfo functions to seperate file, add rudimentary user reporting function 2023-07-23 04:21:50 +08:00
PC-Admin
94740aceb0 add functions to collect account data, list account pushers, set account rate limiting and check if an account exists 2023-07-21 03:15:08 +08:00
PC-Admin
99fa8940a6 warn hs admin about which tags are about to be automatically blocked/purged 2023-07-21 01:24:34 +08:00
PC-Admin
2be8f69ab0 whoops 2023-07-21 01:02:30 +08:00
PC-Admin
9c935821be move recommended rdlist tags back to the hardcoded variables file for easier adjustment 2023-07-21 00:58:43 +08:00
PC-Admin
a2fcbf6ec9 update readme 2023-07-19 06:06:54 +08:00
PC-Admin
b4931e43bb update readme 2023-07-19 06:05:24 +08:00
PC-Admin
0fac83af5b update readme 2023-07-19 06:04:04 +08:00
PC-Admin
d3e8b5adc8 update readme 2023-07-19 06:02:46 +08:00
PC-Admin
c48fe4f715 update roadmap and make the menu prettier 2023-07-19 06:01:40 +08:00
PC-Admin
e4cdebc161 cleanup main script file 2023-07-15 04:55:59 +08:00
PC-Admin
93315971c9 update readme 2023-07-15 04:47:51 +08:00
PC-Admin
3d38d88488 update server functions to use request instead of curl 2023-07-15 04:45:27 +08:00
PC-Admin
664c8611d4 finish re-write of rooms functions 2023-07-15 04:36:40 +08:00
PC-Admin
07ec96334b re-write half the room functions to use requests module 2023-07-15 03:34:10 +08:00
PC-Admin
6e2e8a28d6 update readme, stop state events export function from recursing 2023-07-15 01:49:08 +08:00
PC-Admin
1aea8c7f93 replace 4 spaces with tabs, i <3 tabs! 2023-07-15 00:04:59 +08:00
PC-Admin
58ed3defee add ipinfo function for determining an accounts country of origin, add multiple whois and query account functions 2023-07-14 23:18:31 +08:00
PC-Admin
da1322dc8d re-add vars file (whoops) 2023-07-10 04:59:30 +08:00
PC-Admin
96c1d5b94e change user commands function list to use requests module instead of curl (safer) 2023-07-10 04:48:14 +08:00
PC-Admin
f1a2e58dd7 untrack file 2023-07-10 02:50:25 +08:00
PC-Admin
3fc1b8271a move recommended tags list out of variables file 2023-07-10 02:48:44 +08:00
PC-Admin
5bcc2da4e3 README.md update 2023-07-08 18:11:05 +08:00
PC-Admin
1b1564b423 add blank hardcoded_variables.py file 2023-07-08 18:08:43 +08:00
PC-Admin
715d36d267 add .gitignore file to prevent accidental leaking of token 2023-07-08 18:07:33 +08:00
PC-Admin
03d2722856 seperate functions out into multiple files 2023-07-08 17:55:48 +08:00
PC-Admin
3062080912 cleanup readme 2023-07-08 06:22:43 +08:00
PC-Admin
3c887b8ca3 cleanup readme 2023-07-08 06:19:10 +08:00
PC-Admin
93b17b5213 update default tags for rdlist and cleanup readme 2023-07-08 06:18:44 +08:00
PC-Admin
1f697a279d finish recommended tags functions. modularise reset_password() and set_user_server_admin(). distinguish whois API lookup from user account API lookup. make rdlist pulls more seamless 2023-07-08 06:08:35 +08:00
PC-Admin
ce6f2aa6ea add recommended tags functions 2023-07-08 01:09:59 +08:00
21 changed files with 3419 additions and 1133 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
hardcoded_variables.py
__pycache__

265
README.md
View File

@ -1,26 +1,104 @@
# Matrix Moderation Tool # Matrix Moderation Tool
A Matrix moderation tool to make managing a Synapse server easier.
This is a script i wrote to make moderating 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.
***
## List of Functions
This tool abstracts the Synapse API so you can perform common moderation functions easier and in batch.
Here is a preview of the CLI interface:
```
##########################
# MATRIX MODERATION TOOL #
##########################
A tool for making common Synapse moderation tasks easier. Created by @PC-Admin.
----------------------------------------------
#### User Account Commands #### #### Room Commands ####
1) Deactivate a user account. 50) List details of a room.
2) Deactivate multiple user accounts. 51) List the members of a room.
3) Create a user account. 52) Export the state events of a room.
4) Create multiple user accounts. 53) Export the state events of multiple rooms.
5) Reset a users password. 54) List rooms in public directory.
6) Whois user account. 55) Remove a room from the public directory.
7) Whois multiple user accounts. 56) Remove multiple rooms from the public directory.
8) Query user account. 57) Redact a room event.
9) Query multiple user accounts. 58) List/Download all media in a room.
10) List room memberships of user. 59) Download media from multiple rooms.
11) Promote a user to server admin. 60) Quarantine all media in a room.
12) List all user accounts. 61) Shutdown a room.
13) Quarantine all media a users uploaded. 62) Shutdown multiple rooms.
14) Collect account data. 63) Delete a room.
15) List account pushers. 64) Delete multiple rooms.
16) Get rate limit of a user account. 65) Purge the event history of a room to a specific timestamp.
17) Set rate limit of a user account. 66) Purge the event history of multiple rooms to a specific timestamp.
18) Delete rate limit of a user account. 67) Get blocked status for room.
19) Check if user account exists. 68) Block a room.
20) Shadow ban a user. 69) Unblock a room.
21) Find a user by their 3PID.
#### Server Commands #### #### ipinfo.io ####
100) Delete and block a specific media. 140) Analyse a users country of origin.
101) Purge remote media repository up to a certain date. 141) Analyse multiple users country of origin.
102) Prepare database for copying events of multiple rooms.
103) Show last 10 reported events. #### Report Generation ####
104) Get all reported events. 150) Generate user report.
105) Get details of a reported event. 151) Lookup homeserver admin contact details.
152) Send a test email (to yourself).
#### rdlist - General #### 153) Send a test Matrix message (to yourself).
120) Block all rooms with specific rdlist tags. 154) Send test incident reports (to yourself).
121) Get rdlist tags for a room.
#### rdlist - Recommended Tags ####
For rdlist rooms with recommended tags, the following actions are available:
130) Collect User Reports on local accounts in rdlist rooms.
131) Send Incident Reports on remote accounts in rdlist rooms.
132) Block/Purge all rdlist rooms.
```
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 published under the MIT license, for more information on this license see here: https://opensource.org/license/mit/
*** ***
## Setup script ## Setup script
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. Firstly, you need hard code the 'server URL', 'federation port' and 'access token' into the [hardcoded_variables.py](./hardcoded_variables.py) file
```
$ cp ./hardcoded_variables.py.sample ./hardcoded_variables.py
$ nano ./hardcoded_variables.py
```
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
```
***
## Running the script
`$ python3 moderation_tool.py`
*** ***
## 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...”
@ -35,11 +113,10 @@ UPDATE 1
synapse=# synapse=#
- sign instead of = means: Note: A - sign instead of = means you didn't type a complete SQL query yet!
It means you didn't type a complete SQL query yet. (You need a semicolon (;) at the end to terminate the command.)
You need a semicolon to terminate the command.
*** ***
## Make sure /_synapse/ is mapped ## Make sure /_synapse/ is mapped
@ -62,6 +139,11 @@ 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`
*** ***
## rdlist Functionality ## rdlist Functionality
@ -69,131 +151,62 @@ You can also run the script locally on your server if you do not wish to map /_s
This script can automatically load and block/purge abusive rooms from rdlist, making it **very easy** for inexperienced administrators to block this harmful content. This script can automatically load and block/purge abusive rooms from rdlist, making it **very easy** for inexperienced administrators to block this harmful content.
To get started, just dm me at [@michael:perthchat.org](https://matrix.to/#/@michael:perthchat.org) and I can invite you to the 'Legion of Janitors' room. 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), simply run this moderation script like so: For more information on rdlist related function consult the [support document](./docs/rdlist_functions.md).
```
$ python3 modtool.py
Please select one of the following options:
...
#### rdlist ####
30) Block all rooms with specific rdlist tags.
('q' or 'e') Exit.
30
Cloning rdlist repo...
Cloning into 'rdlist'...
Username for 'https://code.glowers.club': PC-Admin
Password for 'https://PC-Admin@code.glowers.club':
remote: Enumerating objects: 563, done.
remote: Counting objects: 100% (563/563), done.
remote: Compressing objects: 100% (556/556), done.
remote: Total 563 (delta 355), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (563/563), 5.47 MiB | 800.00 KiB/s, done.
Resolving deltas: 100% (355/355), done.
Printing details about the current tags in rdlist: ***
## One-touch Reporting
# Tags CAUTION: This section is under heavy development and probably shouldn't be used by anyone!
### Greedy Tags This script can automatically generate reports about user accounts for law enforcement.
*Rooms with these tags may not exist to host such content, however do not remove it.* It collects as much data about the target user account as possible, then packages it into an encrypted ZIP file that can be shared:
Tag|Description
-|-
[`csam`][csam] | Child Sexual Abuse Material
[`cfm`][cfm] | An abundance of content which would directly appeal to those seeking csam.
[`jailbait`][jailbait] | Photos which contain underage individuals in questionable or suggestive situations.
[`tfm`][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.
### Topic Tags
*Tags here describe the room or the room's characteristics.*
Tag|Description
-|-
`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.
[csam]: https://en.wikipedia.org/wiki/Child_pornography 'Child Sexual Abuse Material. COPAINE >=2'
[cfm]: https://en.wikipedia.org/wiki/Child_erotica 'Child Fetish Material'
[jailbait]: https://en.wikipedia.org/wiki/Jailbait_images 'Jailbait'
[tfm]: about:blank 'Teen Fetish Material'
[wp:copaine-scale]: https://en.wikipedia.org/wiki/COPINE_scale 'COPINE scale - Wikipedia'
Please enter a space seperated list of tags you wish to block:
hub_room_links preban degen_misc beastiality degen_porn gore hub_room_trade snuff degen_larp hub_room_sussy bot_spam cfm 3d_loli jailbait bot_porn toddlercon loli csam tfm anarchy abandoned degen_meet stylized_3d_loli
Tag: csam
Room IDs: [***REDACTED***]
Tag: 3d_loli
Room IDs: [***REDACTED***]
Tag: jailbait
Room IDs: [***REDACTED***]
Tag: toddlercon
Room IDs: [***REDACTED***]
Tag: loli
Room IDs: [***REDACTED***]
...
Do you want to block/purge all these rooms? y/n? y
Please enter the local username that will create a 'muted violation room' for your users (Example: michael): mod_team
Please enter the room name of the muted violation room your users will be sent to: POLICY VIOLATION
Please enter the shutdown message that will be displayed to users: THIS ROOM HAS BEEN SHUTDOWN AS IT VIOLATES PERTHCHAT.ORG POLICIES!
Do you want to purge these rooms? (This deletes all the room history from your database.) y/n? y
Do you want to block these rooms? (This prevents your server users re-entering the room.) y/n? y
Number of rooms being shutdown: 320
Are you sure you want to shutdown these rooms? y/n? y
curl -kXGET 'https://matrix.perthchat.org/_synapse/admin/v1/rooms/!***REDACTED***:matrix.org/state?access_token=***REDACTED***' > ./!***REDACTED***:matrix.org_state_1688238203.json
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 50 0 50 0 0 891 0 --:--:-- --:--:-- --:--:-- 892
Exported room state events to file, this data can be useful for profiling a room after you've blocked/purged it: ./state_events!***REDACTED***:matrix.org_state_1688238203.json
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 226 0 32 100 194 341 2069 --:--:-- --:--:-- --:--:-- 2430
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 318 0 318 0 0 7095 0 --:--:-- --:--:-- --:--:-- 7227
status: complete
!***REDACTED***:matrix.org has been successfully shutdown!
``` ```
Note that this script before shutting these rooms down will save the state events to the "./state_events" folder, this data is important for law enforcement. Please collect these files and send them back to the [Legion of Janitors](https://matrix.to/#/#janitors:glowers.club) for collection and analysis. 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
```
***
## 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 - DONE
- https://github.com/matrix-org/synapse/blob/master/docs/admin_api/delete_group.md
- https://matrix-org.github.io/synapse/v1.38/admin_api/rooms.html#make-room-admin-api
- https://matrix-org.github.io/synapse/latest/admin_api/server_notices.html
- https://matrix-org.github.io/synapse/latest/admin_api/event_reports.html
- https://matrix-org.github.io/synapse/latest/usage/administration/admin_api/federation.html#destination-rooms
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
- 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) - DONE
- 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

62
bot_commands.py Normal file
View File

@ -0,0 +1,62 @@
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())

182
docs/rdlist_functions.md Normal file
View File

@ -0,0 +1,182 @@
# rdlist Functions
'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 [@PC-Admin:matrix.org](https://matrix.to/#/@PC-Admin:matrix.org) and I can invite you to the 'Legion of Janitors' room.
***
## 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/projects/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/projects/matrix-moderation-tool/reports/somecreep29330_2023-08-01_23-19-27.zip
.zip file size: 0.29578 MB
```
'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.
***
## 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
```
![A preview of an Incident Report being sent over Matrix.](https://github.com/PC-Admin/matrix-moderation-tool/assets/29645145/db5a4a56-fd66-413a-ac44-1216c7b2f1fd)
## 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
Do 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? y
Shadow banning user: @***REDACTED***:perthchat.org
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?
https://matrix.perthchat.org/_synapse/admin/v1/deactivate/@***REDACTED***:perthchat.org
Successfully deactivated account.
These accounts have been deactivated.
...
```

102
docs/room_functions.md Normal file
View File

@ -0,0 +1,102 @@
#### Room Commands ####
50) **List details of a room.**
> Shows comprehensive information about a specific room.
https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-details-api
51) **List the members of a room.**
> Displays all the members who are part of a particular room.
https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-members-api
52) **Export the state events of a room.**
> Retrieves and saves the state events (such as topic changes, member joins) of a specific room.
https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#room-state-api
53) **Export the state events of multiple rooms.**
> Gathers and saves the state events from several rooms simultaneously.
54) **List rooms in public directory.**
> Displays all the rooms that are publicly available and listed in the directory.
https://spec.matrix.org/legacy/client_server/r0.6.0.html#get-matrix-client-r0-publicrooms
55) **Remove a room from the public directory.**
> Takes a specific room off the public directory, making it less accessible to users browsing the directory.
56) **Remove multiple rooms from the public directory.**
> Removes several rooms from the public directory at once.
57) **Redact a room event.**
> Conceals specific events in a room, such as messages or media, from being visible.
58) **List/Download all media in a room.**
> Displays and offers the option to download all media files shared in a specific room.
https://matrix-org.github.io/synapse/latest/admin_api/media_admin_api.html#list-all-media-in-a-room
59) **Download media from multiple rooms.**
> Enables downloading of media files shared across several rooms.
60) **Quarantine all media in a room.**
> Isolates and restricts access to all media files in a specific room, usually due to concerns over inappropriate content.
https://matrix-org.github.io/synapse/latest/admin_api/media_admin_api.html#quarantining-media-in-a-room
61) **Shutdown a room.**
> Terminates a room, preventing further activities or messages in it.
https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#version-2-new-version
62) **Shutdown multiple rooms.**
> Closes several rooms simultaneously, halting any ongoing activity in them.
63) **Delete a room.**
> Permanently removes a room and all its content.
64) **Delete multiple rooms.**
> Erases several rooms and their content from the server at once.
65) **Purge the event history of a room to a specific timestamp.**
> Deletes all events in a room up to a certain point in time, preserving events after that timestamp.
66) **Purge the event history of multiple rooms to a specific timestamp.**
> Clears the events in several rooms up to a specified timestamp.
67) **Get blocked status for room.**
> Checks if a room is blocked and restricted from user access.
https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#get-block-status
68) **Block a room.**
> Restricts access to a room, preventing users from joining or interacting in it.
https://matrix-org.github.io/synapse/latest/admin_api/rooms.html#block-or-unblock-a-room
69) **Unblock a room.**
> Restores access to a previously blocked room, allowing users to join and participate once more.
**Note:** Managing rooms is a sensitive operation, especially when dealing with content and user privacy. Ensure you have the required permissions and handle operations with care.

116
docs/server_functions.md Normal file
View File

@ -0,0 +1,116 @@
# Server Commands Guide
This guide provides detailed steps for server-side operations that use the database and SSH. The commands and scripts are essential for handling specific server operations related to Matrix's Synapse server.
## Table of Contents
- [1. Delete and Block Specific Media](#1-delete-and-block-specific-media)
- [2. Purge Remote Media Repository](#2-purge-remote-media-repository)
- [3. Prepare Database for Copying Events of Multiple Rooms](#3-prepare-database-for-copying-events-of-multiple-rooms)
---
100) **Delete and Block Specific Media.**
> This command allows an admin to delete a specific media on their Matrix Synapse server and block it to prevent future accesses.
#### Process Flow:
1. Take `media_id` and remote server URL from the user.
2. Use SSH to query the Synapse PostgreSQL database for the associated `filesystem_id`.
3. Locate the target media files and thumbnails on the server's file system.
4. Zero out (empty) each file and make them immutable, meaning they cannot be modified or deleted.
#### Example:
For a media with ID `eDmjusOjnHyFPOYGxlrOsULJ`, the process would involve:
```bash
$ ssh matrix.perthchat.org "... SQL query to get filesystem_id..."
$ ssh matrix.perthchat.org "... command to locate files ..."
$ ssh matrix.perthchat.org "true > ...path to file..."
$ ssh matrix.perthchat.org "chattr +i ...path to file..."
```
101) **Purge Remote Media Repository**
This command purges the remote media repository for a certain range of days.
Process Flow:
Ask the user for the range of days to purge.
Calculate the epoch timestamp for each day in the range.
Send a request to the Synapse server to purge media for that day.
Repeat for each day in the range.
Example:
```bash
$ python3 moderation_tool.py
101
Enter the number of days to purge from: 30
Enter the number of days to purge too: -2
{"deleted":0}
{"deleted":0}
{"deleted":3}
{"deleted":360}
{"deleted":469}
...
{"deleted":1020}
{"deleted":2440}
{"deleted":0}
{"deleted":0}
Done! :)
```
102) **Prepare Database for Copying Events of Multiple Rooms**
This command prepares the PostgreSQL database to export events from multiple Matrix rooms.
Process Flow:
Prompt for a list of room IDs.
Create a RAM disk on the server to store the export.
For each room ID:
Create a SQL query to extract room events.
Write the query to a file on the RAM disk.
Provide instructions for running the queries in the PostgreSQL container.
Notes:
This function is compatible with Spantaleev's Matrix deploy script.
Ensure proper permissions and consider the impact on the server when copying a large amount of data.
Example:
```bash
# As the root user on the target server:
$ mkdir /matrix/postgres/data/ramdisk
$ ... commands to set up RAM disk ...
$ ... commands to generate SQL queries for each room ...
$ docker exec -it matrix-postgres /bin/bash
bash-5.0$ ... commands to execute SQL queries ...
```
After copying the data, ensure to clean up the RAM disk:
```bash
$ rm -r /matrix/postgres/data/ramdisk/*
$ umount /matrix/postgres/data/ramdisk
```
103) **Show last 10 reported events.**
> Gets the last 10 reported events using the event reports API and returns it.
https://matrix-org.github.io/synapse/latest/admin_api/event_reports.html#show-reported-events
104) **Paginate all reported events.**
> Combines all the events into a large JSON and returns it.
105) **Show details of a specific event report**
> This API returns information about a specific event report.
https://matrix-org.github.io/synapse/latest/admin_api/event_reports.html#show-details-of-a-specific-event-report

118
docs/user_functions.md Normal file
View File

@ -0,0 +1,118 @@
#### User Account Commands ####
1) **Deactivate a user account.**
> This function disables a specific user's account, making it unusable for the owner.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#deactivate-account
2) **Deactivate multiple user accounts.**
> Allows you to disable several user accounts at once. Requires a room list file with room_ids separated by newlines, see the example [./examples/room_list.txt](./examples/room_list.txt) file.
3) **Create a user account.**
> Use this to generate a new user account.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#create-or-modify-account
4) **Create multiple user accounts.**
> This facilitates the creation of several user accounts simultaneously. Requires a user list file with user_ids separated by newlines, see the example [./examples/user_list.txt](./examples/user_list.txt) file.
5) **Reset a user's password.**
> If a user forgets their password, this function helps set a new one.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#reset-password
6) **Whois user account.**
> This API returns information about the active sessions for a specific user.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#query-current-sessions-for-a-user
7) **Whois multiple user accounts.**
> Retrieves detailed information for multiple user accounts at once. Requires a user list file with user_ids separated by newlines, see the example [./examples/user_list.txt](./examples/user_list.txt) file.
8) **Query user account.**
> Allows you to get specific details or attributes of a user account.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#query-user-account
9) **Query multiple user accounts.**
> Retrieve specific details for several user accounts simultaneously. Requires a user list file with user_ids separated by newlines, see the example [./examples/user_list.txt](./examples/user_list.txt) file.
10) **List room memberships of user.**
> Displays the list of rooms that a user is a part of.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#list-room-memberships-of-a-user
11) **Promote a user to server admin.**
> Elevates a user's privileges, making them an administrator on the server.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#change-whether-a-user-is-a-server-administrator-or-not
12) **List all user accounts.**
> Displays or prints to file a comprehensive list of all user accounts on the server.
https://matrix-org.github.io/synapse/v1.38/admin_api/user_admin_api.html#list-accounts
13) **Quarantine all media a user uploaded.**
> This API quarantines all local media that a local user has uploaded. That is to say, if you would like to quarantine media uploaded by a user on a remote homeserver, you should instead use one of the other APIs. Useful for potential harmful or inappropriate content.
https://matrix-org.github.io/synapse/latest/admin_api/media_admin_api.html#quarantining-all-media-of-a-user
14) **Collect account data.**
> Retrieves all available data associated with a user's account.
15) **List account pushers.**
> Shows devices and services that have push access to a user's account.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#list-all-pushers
16) **Get rate limit of a user account.**
> Displays the frequency at which a user can make requests or actions.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#get-status-of-ratelimit
17) **Set rate limit of a user account.**
> Adjusts the frequency rate at which a user can make requests or actions.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#set-ratelimit
18) **Delete rate limit of a user account.**
> Removes any rate limits set on a user's account, granting them unrestricted action frequency.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#delete-ratelimit
19) **Check if user account exists.**
> Verifies the existence of a specific user_id, for example "@johndoe:example.org" on the homeserver.
20) **Shadow ban a user.**
> Shadow-banning is a useful tool for moderating malicious or egregiously abusive users. A shadow-banned users receives successful responses to their client-server API requests, but the events are not propagated into rooms. This can be an effective tool as it (hopefully) takes longer for the user to realise they are being moderated before pivoting to another account.
> Shadow-banning a user should be used as a tool of last resort and may lead to confusing or broken behaviour for the client. A shadow-banned user will not receive any notification and it is generally more appropriate to ban or kick abusive users. A shadow-banned user will be unable to contact anyone on the server.
https://matrix-org.github.io/synapse/latest/admin_api/user_admin_api.html#controlling-whether-a-user-is-shadow-banned
21) **Find a user by their 3PID (Third-party ID).**
> Allows you to locate a user based on their third-party identifiers, such as email or phone number.
**Note:** All these commands utilize the Synapse API. Always exercise caution and ensure you have the necessary permissions when accessing and modifying user accounts.

23
examples/room_list.json Normal file
View File

@ -0,0 +1,23 @@
[
"!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"
]

View File

@ -0,0 +1,25 @@
###########################################################################
# 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_dir = "~/rdlist" # The directory where rdlist is pulled to
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 = False # 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"
###########################################################################

79
ipinfo_commands.py Normal file
View File

@ -0,0 +1,79 @@
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}")

250
moderation_tool.py Executable file
View File

@ -0,0 +1,250 @@
#!/bin/env python3
import os
import json
# Check if ./hardcoded_variables.py file exists
if not os.path.exists("./hardcoded_variables.py"):
print("ERROR: The file './hardcoded_variables.py' does not exist. It must be configured before using this script.")
exit()
import user_commands
import room_commands
import server_commands
import ipinfo_commands
import rdlist_commands
import report_commands
import bot_commands
# Importing the module only after verifying its existence
import hardcoded_variables
# If it does exist... check if the variables are configured:
# check if homeserver url is the default
if hardcoded_variables.homeserver_url == "matrix.example.org":
print("ERROR: homeserver_url not configured, please configure your './hardcoded_variables.py' file!")
exit()
# check if base url is the default
if hardcoded_variables.base_url == "example.org":
print("ERROR: base_url not configured, please configure your './hardcoded_variables.py' file!")
exit()
# check if access token is the default
length_access_token = len(hardcoded_variables.access_token)
if length_access_token == 0:
print("ERROR: access_token not configured, please configure your './hardcoded_variables.py' file!")
exit()
# 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("21) Find a user by their 3PID.")
print("\n#### Server Commands ####\t\t\t\t\t#### ipinfo.io ####")
print("100) Delete and block a specific media.\t\t\t\t140) Analyse a users country of origin.")
print("101) Purge remote media repository up to a certain date.\t141) Analyse multiple users country of origin.")
print("102) Prepare database for copying events of multiple rooms.")
print("103) Show last 10 reported events.\t\t\t\t#### Report Generation ####")
print("104) Get all reported events.\t\t\t\t\t150) Generate user report.")
print("105) Get details of a reported event.\t\t\t\t151) Lookup homeserver admin contact details.")
print("106) Send a server notice.\t\t\t\t\t152) Send a test email (to yourself).")
print("\t\t\t\t\t\t\t\t\t153) Send a test Matrix message (to yourself).")
print("\n#### 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 ####")
print("For 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("\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 == "21":
user_dict = user_commands.find_account_with_threepid()
print(f"\n{json.dumps(user_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 == "103":
reported_events = server_commands.get_reported_events(10)
print(json.dumps(reported_events, indent=4, sort_keys=True))
elif menu_input == "104":
all_reported_events = server_commands.paginate_reported_events() # Again assuming default values are set
print(json.dumps(all_reported_events, indent=4, sort_keys=True))
elif menu_input == "105":
report_details = server_commands.get_event_report_details()
print(json.dumps(report_details, indent=4, sort_keys=True))
elif menu_input == "106":
server_commands.send_server_notice()
print("\nServer notice sent.\n")
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")

1007
modtool.py

File diff suppressed because it is too large Load Diff

314
rdlist_commands.py Normal file
View File

@ -0,0 +1,314 @@
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 = os.path.expanduser(hardcoded_variables.rdlist_dir)
os.makedirs(rdlist_dir, exist_ok=True)
# Check if the rdlist repo has already been cloned
if os.path.isdir(os.path.join(rdlist_dir, ".git")):
print("\nrdlist repo already cloned...")
# Update git remote references and get status
subprocess.run(["git", "-C", rdlist_dir, "remote", "update"], check=True)
status = subprocess.run(["git", "-C", rdlist_dir, "status", "-uno"], stdout=subprocess.PIPE, check=True)
# 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...")
subprocess.run(["git", "-C", rdlist_dir, "pull"], check=True)
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", rdlist_dir], 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 specified directory
sync_rdlist()
# Expand the user in the path and load the summaries JSON file
summaries_dir = os.path.expanduser(hardcoded_variables.rdlist_dir)
summaries_path = os.path.join(summaries_dir, "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):
# Expand the user in the path and load the summaries JSON file
summaries_dir = os.path.expanduser(hardcoded_variables.rdlist_dir)
summaries_path = os.path.join(summaries_dir, "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()
print("\nCalculating local and remote users in rdlist rooms... (This may take a while, please wait.)")
# 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 specified directory
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...")
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 specified directory
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...")
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 specified directory
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:
# Expand the user in the path and read the file into a string
rdlist_dir = os.path.expanduser(hardcoded_variables.rdlist_dir)
rdlist_path = os.path.join(rdlist_dir, "lib", "docs", "tags.md")
with open(rdlist_path, '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 0, 0, []
else:
print("\nInvalid input, skipping these files...\n")
return 0, 0, []
# 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.")
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 if any rooms were shutdown
if total_list_kicked_users != []:
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"\nPrinting 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)}")
if 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")

520
report_commands.py Normal file
View File

@ -0,0 +1,520 @@
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 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 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):
# Print warning if testing mode is enabled
if hardcoded_variables.testing_mode == True:
testing_mode_warning()
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
print(response.text)
data = json.loads(response.text)
#print("\nAdmin contact details for " + baseurl + " are: " + str(data))
return data, False
else:
print(f"\nError: 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):
# Create a list of dictionaries, each containing one email address
emails_dict_list = [{"email_address": email} for email in w.emails]
return {"admins": emails_dict_list}, True
# If it's not a list, it must be a single string. So, we wrap it in a list
else:
return {"admins": [{"email_address": w.emails}]}, True
else:
print(f"Error: Unable to collect admin email from WHOIS data for {baseurl}")
return None, False
except:
print(f"Error: 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 Normal file
View File

@ -0,0 +1,654 @@
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}

266
server_commands.py Normal file
View File

@ -0,0 +1,266 @@
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")
def get_reported_events(limit=100, _from=0, dir='b', user_id=None, room_id=None):
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/event_reports"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {hardcoded_variables.access_token}"
}
params = {
'limit': limit,
'from': _from,
'dir': dir
}
if user_id:
params['user_id'] = user_id
if room_id:
params['room_id'] = room_id
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
return response.json()
else:
print(f"Error fetching reported events: {response.status_code}, {response.text}")
return None
def paginate_reported_events(limit=100, dir='b', user_id=None, room_id=None):
_from = 0
all_reports = []
while True:
reports = get_reported_events(limit=limit, _from=_from, dir=dir, user_id=user_id, room_id=room_id)
if not reports or "event_reports" not in reports:
break
all_reports.extend(reports["event_reports"])
if "next_token" in reports:
_from = reports["next_token"]
else:
break
return all_reports
def get_event_report_details(preset_report_id=''):
if preset_report_id == '':
report_id = input("\nEnter the report_id of the report you wish to query (Example: 56): ")
elif preset_report_id != '':
report_id = preset_report_id
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/event_reports/{report_id}"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {hardcoded_variables.access_token}"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"Error fetching event report details: {response.status_code}, {response.text}")
return None
def send_server_notice(preset_user_id='', preset_message='', txnId=None, event_type="m.room.message", state_key=None):
"""
Sends a server notice to a given user.
Args:
- user_id (str): The Matrix ID of the user to send the notice to, e.g. "@target_user:server_name".
- message (str): The message to be sent as a notice.
- txnId (str, optional): A unique transaction ID. If provided, retransmissions with the same txnId will be ignored.
- event_type (str, optional): The type of event. Defaults to "m.room.message".
- state_key (str, optional): Setting this will result in a state event being sent.
Returns:
- dict: A dictionary containing the response from the server.
"""
# Take user_id from user if not provided
if preset_user_id == '':
user_id = input("\nEnter the user_id of the user you would like to send the server notice to: ")
elif preset_user_id != '':
user_id = preset_user_id
# Take message from user if not provided
if preset_message == '':
message = input("\nEnter the message you would like to send to the user: ")
elif preset_message != '':
message = preset_message
# Construct the URL based on whether a txnId is provided
if txnId:
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/send_server_notice/{txnId}"
else:
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/send_server_notice"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {hardcoded_variables.access_token}"
}
# Construct the request body
data = {
"user_id": user_id,
"content": {
"msgtype": "m.text",
"body": message
}
}
if event_type:
data["type"] = event_type
if state_key:
data["state_key"] = state_key
# Send the request
response = requests.put(url, headers=headers, json=data) if txnId else requests.post(url, headers=headers, json=data)
if response.status_code == 200:
return response.json()
else:
print(f"Error sending server notice: {response.status_code}, {response.text}")
return None

BIN
test_data/evil_clown.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

567
user_commands.py Normal file
View File

@ -0,0 +1,567 @@
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 in ["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: ")
username = parse_username(username)
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: ")
username = parse_username(username)
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: ")
username = parse_username(username)
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'
def find_account_with_threepid(medium="", address=""):
# prompt user to enter values if they're not provided
if medium == "":
print("\nPlease enter the medium (either 'email' or 'msisdn' for mobile number): ")
medium = input()
if address == "":
print("\nPlease enter the address (the email or mobile number): ")
address = input()
url = f"https://{hardcoded_variables.homeserver_url}/_synapse/admin/v1/threepid/{medium}/users/{address}?access_token={hardcoded_variables.access_token}"
response = requests.get(url, verify=True)
if response.status_code == 200:
# User exists
return response.json()
elif response.status_code == 404:
# User not found
return {"errcode":"M_NOT_FOUND", "error":"User not found"}
else:
print(f"Error querying account: {response.status_code}, {response.text}")
# Example:
# $ curl -X GET 'https://matrix.perthchat.org/_synapse/admin/v1/threepid/email/users/dogpoo@protonmail.com?access_token=ACCESS_TOKEN'