Initial commit
This commit is contained in:
3
.editorconfig
Normal file
3
.editorconfig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
24
flake.lock
generated
Normal file
24
flake.lock
generated
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1750776420,
|
||||||
|
"narHash": "sha256-/CG+w0o0oJ5itVklOoLbdn2dGB0wbZVOoDm4np6w09A=",
|
||||||
|
"path": "/nix/store/1l4nm55xcq55cdp1xz3x5mfgr4c24058-source",
|
||||||
|
"rev": "30a61f056ac492e3b7cdcb69c1e6abdcf00e39cf",
|
||||||
|
"type": "path"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
51
flake.nix
Normal file
51
flake.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
description = "synapse-revitalization";
|
||||||
|
|
||||||
|
outputs = {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in {
|
||||||
|
devShells.x86_64-linux.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
python313Full
|
||||||
|
python313Packages.signedjson
|
||||||
|
python313Packages.requests
|
||||||
|
python313Packages.systemd
|
||||||
|
python313Packages.setuptools
|
||||||
|
black
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules = {
|
||||||
|
synapse-revitalization = import ./module.nix self;
|
||||||
|
default = self.nixosModules.synapse-revitalization;
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.x86_64-linux.synapse-revitalization = pkgs.stdenv.mkDerivation {
|
||||||
|
projectDir = ./.;
|
||||||
|
pname = "synapse-revitalization";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
installPhase =
|
||||||
|
let python = pkgs.python313.withPackages (ps: with ps; [ requests signedjson systemd ]);
|
||||||
|
script = pkgs.writeShellScript "synapse-revitalization" "${python}/bin/python $out/main.py '$1'";
|
||||||
|
in ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp $src/src/* $out
|
||||||
|
# cp ${script} $out/bin
|
||||||
|
cat > $out/bin/synapse-revitalization << EOF
|
||||||
|
#!/bin/sh
|
||||||
|
exec ${python}/bin/python $out/main.py "\$1"
|
||||||
|
EOF
|
||||||
|
chmod +x $out/bin/synapse-revitalization
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
packages.x86_64-linux.default = self.packages.x86_64-linux.synapse-revitalization;
|
||||||
|
};
|
||||||
|
}
|
63
module.nix
Normal file
63
module.nix
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
self: { pkgs, config, lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
mkOption
|
||||||
|
mkEnableOption
|
||||||
|
mkIf
|
||||||
|
types
|
||||||
|
;
|
||||||
|
|
||||||
|
cfg = config.services.synapse-revitalization;
|
||||||
|
in {
|
||||||
|
options.services.synapse-revitalization = {
|
||||||
|
enable = mkEnableOption "Enable synapse-revitalization service";
|
||||||
|
adminAuthTokenFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "File containing admin user's authentication token";
|
||||||
|
};
|
||||||
|
serverKeyFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = "Synapse server signing key file";
|
||||||
|
default = "/var/lib/matrix-synapse/homeserver.signing.key";
|
||||||
|
};
|
||||||
|
serverName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Synapse server's name";
|
||||||
|
default = config.services.matrix-synapse.settings.server_name;
|
||||||
|
};
|
||||||
|
serverFQDN = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Synapse server's fqdn";
|
||||||
|
};
|
||||||
|
package = self.packages.x86_64-linux.synapse-revitalization;
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf (cfg.enable) {
|
||||||
|
systemd.services."synapse-revitalization" =
|
||||||
|
let pkg = "${pkgs.synapse-revitalization}/bin/synapse-revitalization";
|
||||||
|
script = pkgs.writeShellScript "synapse-revitalization-script" ''
|
||||||
|
journalctl -f -u matrix-synapse -o cat |
|
||||||
|
while read -r line; do
|
||||||
|
echo "$line" | grep "as we're not in the room" && ${pkg} "$line" &
|
||||||
|
echo "$line" | grep "Ignoring PDU for unknown room_id" && ${pkg} "$line" &
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
enable = true;
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = "root";
|
||||||
|
Group = "root";
|
||||||
|
ExecStart = script;
|
||||||
|
Restart = "always";
|
||||||
|
};
|
||||||
|
environment = {
|
||||||
|
"SYNAPSE_REVITALIZATION_ADMIN_AUTH_TOKEN_FILE" = cfg.adminAuthTokenFile;
|
||||||
|
"SYNAPSE_REVITALIZATION_SERVER_KEY_FILE" = cfg.serverKeyFile;
|
||||||
|
"SYNAPSE_REVITALIZATION_SERVER_NAME" = cfg.serverName;
|
||||||
|
"SYNAPSE_REVITALIZATION_SERVER_ADDRESS" = cfg.serverFQDN;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
1
result
Symbolic link
1
result
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/nix/store/0jq0qq7dp05d494flf6a6qa1gjq2kfzf-synapse-revitalization-0.1.0
|
20
src/get_config.py
Normal file
20
src/get_config.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def read_vars():
|
||||||
|
if os.environ.get("SYNAPSE_REVITALIZATION_AUTH_TOKEN_TERM") == None:
|
||||||
|
auth_token_term = 180
|
||||||
|
else:
|
||||||
|
auth_token_term = os.environ["SYNAPSE_REVITALIZATION_AUTH_TOKEN_TERM"]
|
||||||
|
|
||||||
|
admin_auth_token_file = os.environ["SYNAPSE_REVITALIZATION_ADMIN_AUTH_TOKEN_FILE"]
|
||||||
|
|
||||||
|
with open(admin_auth_token_file, "r") as file:
|
||||||
|
admin_auth_token = file.read().split("\n")[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"auth_token_term": auth_token_term,
|
||||||
|
"admin_auth_token": admin_auth_token,
|
||||||
|
"server_key_file": os.environ["SYNAPSE_REVITALIZATION_SERVER_KEY_FILE"],
|
||||||
|
"origin_server_name": os.environ["SYNAPSE_REVITALIZATION_SERVER_NAME"],
|
||||||
|
"origin_server": os.environ["SYNAPSE_REVITALIZATION_SERVER_ADDRESS"],
|
||||||
|
}
|
35
src/main.py
Normal file
35
src/main.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from systemd import journal
|
||||||
|
from revitalize import revitalize, parse_roomid
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) != 2: # 2, because running the script looks like 'python [1]main.py [2]"..."'
|
||||||
|
print("synapse-revitalization accepts only 1 argument")
|
||||||
|
exit()
|
||||||
|
else:
|
||||||
|
message = sys.argv[1]
|
||||||
|
|
||||||
|
if "Ignoring PDU for unknown room_id" in message:
|
||||||
|
journal.send("[info] Got error message: %s" % message)
|
||||||
|
roomid = message.rsplit(maxsplit=1)[1]
|
||||||
|
x = revitalize(roomid, parse_roomid(roomid))
|
||||||
|
if x == True:
|
||||||
|
journal.send("[info] Successfully made all users rejoin {roomid} via {server}")
|
||||||
|
journal.send("[info] {roomid} is available now")
|
||||||
|
elif x == False:
|
||||||
|
journal.send("ERROR! Couldn't make {roomid} available via {parse_roomid(roomid)}")
|
||||||
|
|
||||||
|
|
||||||
|
elif "as we're not in the room" in message:
|
||||||
|
journal.send("[info] Got error message: %s" % message)
|
||||||
|
roomid = message.split("'")[1]
|
||||||
|
server = message.split("from server ")[1].split(" ")[0]
|
||||||
|
for i in [server, parse_roomid(roomid)]:
|
||||||
|
x = revitalize(roomid, i)
|
||||||
|
if x == True:
|
||||||
|
journal.send(f"[info] Successfully made all users rejoin {roomid} via {server}")
|
||||||
|
journal.send(f"[info] {roomid} is available now")
|
||||||
|
break
|
||||||
|
elif x == False:
|
||||||
|
journal.send(f"[WARNING] Couldn't make users rejoin {roomid} via {server}")
|
||||||
|
if i == parse_roomid(roomid):
|
||||||
|
journal.send(f"ERROR! Couldn't make {roomid} available via {server} nor {parse_roomid(roomid)}")
|
198
src/revitalize.py
Normal file
198
src/revitalize.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
from signedjson.key import read_signing_keys
|
||||||
|
from signedjson.sign import sign_json
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from systemd import journal
|
||||||
|
from get_config import read_vars
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# Importing configuration from environment variables
|
||||||
|
config = read_vars()
|
||||||
|
|
||||||
|
|
||||||
|
# Getting signing key
|
||||||
|
with open(config["server_key_file"]) as f:
|
||||||
|
skey = read_signing_keys(f)[0]
|
||||||
|
|
||||||
|
|
||||||
|
# Defining additional functions
|
||||||
|
def current_time():
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
return int(now.timestamp() * 1000)
|
||||||
|
|
||||||
|
def parse_roomid(roomid):
|
||||||
|
return roomid.split(":")[1]
|
||||||
|
|
||||||
|
def serveraddr(servername):
|
||||||
|
x = requests.get("https://%s/.well-known/matrix/server" % servername)
|
||||||
|
if x.status_code == 200:
|
||||||
|
j = json.loads(x.text)
|
||||||
|
return j["m.server"]
|
||||||
|
else:
|
||||||
|
journal.send(f"[WARNING] Got {x.status_code} discovering server address of {servername}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Function for getting users' access tokens
|
||||||
|
def get_access_token(userid):
|
||||||
|
time = current_time() + config["auth_token_term"] * 1000
|
||||||
|
x = requests.post(
|
||||||
|
f"https://{config['origin_server']}/_synapse/admin/v1/users/{userid}/login",
|
||||||
|
json={"valid_until_ms": time},
|
||||||
|
headers={
|
||||||
|
"Authorization": "Bearer %s" % config["admin_auth_token"],
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
).text
|
||||||
|
if "You are not a server admin" in x:
|
||||||
|
journal.send("ERROR! The given token doesn't belogn to the server admin!")
|
||||||
|
exit()
|
||||||
|
y = json.loads(x)
|
||||||
|
return y["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
# Sign Matrix federation API requests
|
||||||
|
## From Matrix protocol documentation
|
||||||
|
def authorization_headers(
|
||||||
|
destination_name, request_method, request_target, content=None
|
||||||
|
):
|
||||||
|
origin_name = config["origin_server_name"]
|
||||||
|
request_json = {
|
||||||
|
"method": request_method,
|
||||||
|
"uri": request_target,
|
||||||
|
"origin": origin_name,
|
||||||
|
"destination": destination_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if content is not None:
|
||||||
|
request_json["content"] = content
|
||||||
|
|
||||||
|
signed_json = sign_json(request_json, origin_name, skey)
|
||||||
|
|
||||||
|
for key, sig in signed_json["signatures"][origin_name].items():
|
||||||
|
return 'X-Matrix origin="%s",destination="%s",key="%s",sig="%s"' % (
|
||||||
|
origin_name,
|
||||||
|
destination_name,
|
||||||
|
key,
|
||||||
|
sig,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Request the last event
|
||||||
|
def request_last_event(destination, roomid):
|
||||||
|
servers = [destination, "matrix.org", "inex.rocks", "sibnsk.net", "kde.org"]
|
||||||
|
for i in servers:
|
||||||
|
journal.send(f"[info] Trying access event_to_timestamp via {i}")
|
||||||
|
try:
|
||||||
|
server = serveraddr(i)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
journal.send(f"[WARNING] {i}'s well-known's are unreachable")
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
journal.send(f"[WARNING] Can't request {i}'s well-known's for unknown reason. Likely {i} is unreachable or ssl certs expired")
|
||||||
|
continue
|
||||||
|
if server == False:
|
||||||
|
continue
|
||||||
|
|
||||||
|
res = current_time()
|
||||||
|
auth = authorization_headers(
|
||||||
|
i,
|
||||||
|
"GET",
|
||||||
|
f"/_matrix/federation/v1/timestamp_to_event/{roomid}?dir=b&ts={res}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
x = requests.get(
|
||||||
|
f"https://{server}/_matrix/federation/v1/timestamp_to_event/{roomid}",
|
||||||
|
params={"dir": "b", "ts": res},
|
||||||
|
headers={"Authorization": auth},
|
||||||
|
).text
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
journal.send(f"[WARNING] {server} is unreachable")
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
journal.send(f"[WARNING] Couldn't request timestamp_to_event from {server} for unknown reason. Likely {server} is unreachable or ssl certs expired")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "M_UNRECOGNIZED" in x:
|
||||||
|
journal.send(f"[WARNING] Trying request {server} timestamp_to_event for {roomid}: got M_UNRECOGNIZED error")
|
||||||
|
continue
|
||||||
|
elif "M_NOT_FOUND" in x:
|
||||||
|
journal.send(f"[WARNING] Unable to get the last event from {roomid}. {i} doesn't know room {roomid}")
|
||||||
|
journal.send(f"{i} says: '{x}'")
|
||||||
|
continue
|
||||||
|
elif "event_id" in x:
|
||||||
|
j = json.loads(x)
|
||||||
|
return (i, server, j["event_id"])
|
||||||
|
else:
|
||||||
|
journal.send(f"[WARNING] Unable to get the last event from {roomid}. Unknown error from {server}: '{x}'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
journal.send(f"ERROR! Unable to get the last event from {roomid}. All {servers} don't know room {roomid} or support timestamp_to_event")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Request states of specified room
|
||||||
|
def get_states(destination, server, roomid, last_event_id):
|
||||||
|
auth = authorization_headers(
|
||||||
|
destination, "GET", "/_matrix/federation/v1/state/%s?event_id=%s" % (roomid, last_event_id)
|
||||||
|
)
|
||||||
|
return requests.get(
|
||||||
|
f"https://{server}/_matrix/federation/v1/state/{roomid}?event_id={last_event_id}",
|
||||||
|
headers={"Authorization": auth},
|
||||||
|
).text
|
||||||
|
|
||||||
|
|
||||||
|
# Get users list from states
|
||||||
|
def get_users(states):
|
||||||
|
x = json.loads(states)
|
||||||
|
# jq .pdus.[].type
|
||||||
|
users = []
|
||||||
|
x = x["pdus"]
|
||||||
|
for i in x:
|
||||||
|
if i["type"] == "m.room.member":
|
||||||
|
users.append(i["sender"])
|
||||||
|
return users
|
||||||
|
|
||||||
|
def filter_users(users):
|
||||||
|
x = []
|
||||||
|
for i in users:
|
||||||
|
if f":{config["origin_server_name"]}" in i:
|
||||||
|
x.append(i)
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
# Make a user join a room
|
||||||
|
def mkjoins(roomid, users, server1, server2):
|
||||||
|
for i in users:
|
||||||
|
token = get_access_token(i)
|
||||||
|
x = requests.post(
|
||||||
|
f"https://{config['origin_server']}/_matrix/client/v3/join/{roomid}",
|
||||||
|
params={"via": server1, "via": server2},
|
||||||
|
headers={"Authorization": "Bearer %s" % token},
|
||||||
|
).text
|
||||||
|
if roomid in x:
|
||||||
|
journal.send(f"[info] Joined {i} to {roomid}")
|
||||||
|
journal.send(x)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
journal.send(f"[info] Failed to join {i} to {roomid}")
|
||||||
|
journal.send(x)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def revitalize(roomid, server):
|
||||||
|
journal.send(f"Got new roomid {roomid}")
|
||||||
|
fe = request_last_event(server, roomid)
|
||||||
|
if fe == False:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
x = get_states(fe[0], fe[1], roomid, fe[2])
|
||||||
|
y = get_users(x)
|
||||||
|
z = filter_users(y)
|
||||||
|
journal.send(f"Trying to add {z} to {roomid}")
|
||||||
|
if mkjoins(roomid, z, server, fe[0]):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
Reference in New Issue
Block a user