193 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			193 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
# Copyright 2021 The Matrix.org Foundation C.I.C.
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
# you may not use this file except in compliance with the License.
 | 
						|
# You may obtain a copy of the License at
 | 
						|
#
 | 
						|
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
# See the License for the specific language governing permissions and
 | 
						|
# limitations under the License.
 | 
						|
 | 
						|
import argparse
 | 
						|
import sys
 | 
						|
import time
 | 
						|
from datetime import datetime
 | 
						|
from typing import List
 | 
						|
 | 
						|
import attr
 | 
						|
 | 
						|
from synapse.config._base import (
 | 
						|
    Config,
 | 
						|
    RootConfig,
 | 
						|
    find_config_files,
 | 
						|
    read_config_files,
 | 
						|
)
 | 
						|
from synapse.config.database import DatabaseConfig
 | 
						|
from synapse.storage.database import DatabasePool, LoggingTransaction, make_conn
 | 
						|
from synapse.storage.engines import create_engine
 | 
						|
 | 
						|
 | 
						|
class ReviewConfig(RootConfig):
 | 
						|
    "A config class that just pulls out the database config"
 | 
						|
    config_classes = [DatabaseConfig]
 | 
						|
 | 
						|
 | 
						|
@attr.s(auto_attribs=True)
 | 
						|
class UserInfo:
 | 
						|
    user_id: str
 | 
						|
    creation_ts: int
 | 
						|
    emails: List[str] = attr.Factory(list)
 | 
						|
    private_rooms: List[str] = attr.Factory(list)
 | 
						|
    public_rooms: List[str] = attr.Factory(list)
 | 
						|
    ips: List[str] = attr.Factory(list)
 | 
						|
 | 
						|
 | 
						|
def get_recent_users(
 | 
						|
    txn: LoggingTransaction, since_ms: int, exclude_app_service: bool
 | 
						|
) -> List[UserInfo]:
 | 
						|
    """Fetches recently registered users and some info on them."""
 | 
						|
 | 
						|
    sql = """
 | 
						|
        SELECT name, creation_ts FROM users
 | 
						|
        WHERE
 | 
						|
            ? <= creation_ts
 | 
						|
            AND deactivated = 0
 | 
						|
    """
 | 
						|
 | 
						|
    if exclude_app_service:
 | 
						|
        sql += " AND appservice_id IS NULL"
 | 
						|
 | 
						|
    txn.execute(sql, (since_ms / 1000,))
 | 
						|
 | 
						|
    user_infos = [UserInfo(user_id, creation_ts) for user_id, creation_ts in txn]
 | 
						|
 | 
						|
    for user_info in user_infos:
 | 
						|
        user_info.emails = DatabasePool.simple_select_onecol_txn(
 | 
						|
            txn,
 | 
						|
            table="user_threepids",
 | 
						|
            keyvalues={"user_id": user_info.user_id, "medium": "email"},
 | 
						|
            retcol="address",
 | 
						|
        )
 | 
						|
 | 
						|
        sql = """
 | 
						|
            SELECT room_id, canonical_alias, name, join_rules
 | 
						|
            FROM local_current_membership
 | 
						|
            INNER JOIN room_stats_state USING (room_id)
 | 
						|
            WHERE user_id = ? AND membership = 'join'
 | 
						|
        """
 | 
						|
 | 
						|
        txn.execute(sql, (user_info.user_id,))
 | 
						|
        for room_id, canonical_alias, name, join_rules in txn:
 | 
						|
            if join_rules == "public":
 | 
						|
                user_info.public_rooms.append(canonical_alias or name or room_id)
 | 
						|
            else:
 | 
						|
                user_info.private_rooms.append(canonical_alias or name or room_id)
 | 
						|
 | 
						|
        user_info.ips = DatabasePool.simple_select_onecol_txn(
 | 
						|
            txn,
 | 
						|
            table="user_ips",
 | 
						|
            keyvalues={"user_id": user_info.user_id},
 | 
						|
            retcol="ip",
 | 
						|
        )
 | 
						|
 | 
						|
    return user_infos
 | 
						|
 | 
						|
 | 
						|
def main() -> None:
 | 
						|
    parser = argparse.ArgumentParser()
 | 
						|
    parser.add_argument(
 | 
						|
        "-c",
 | 
						|
        "--config-path",
 | 
						|
        action="append",
 | 
						|
        metavar="CONFIG_FILE",
 | 
						|
        help="The config files for Synapse.",
 | 
						|
        required=True,
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-s",
 | 
						|
        "--since",
 | 
						|
        metavar="duration",
 | 
						|
        help="Specify how far back to review user registrations for, defaults to 7d (i.e. 7 days).",
 | 
						|
        default="7d",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-e",
 | 
						|
        "--exclude-emails",
 | 
						|
        action="store_true",
 | 
						|
        help="Exclude users that have validated email addresses.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-u",
 | 
						|
        "--only-users",
 | 
						|
        action="store_true",
 | 
						|
        help="Only print user IDs that match.",
 | 
						|
    )
 | 
						|
    parser.add_argument(
 | 
						|
        "-a",
 | 
						|
        "--exclude-app-service",
 | 
						|
        help="Exclude appservice users.",
 | 
						|
        action="store_true",
 | 
						|
    )
 | 
						|
 | 
						|
    config = ReviewConfig()
 | 
						|
 | 
						|
    config_args = parser.parse_args(sys.argv[1:])
 | 
						|
    config_files = find_config_files(search_paths=config_args.config_path)
 | 
						|
    config_dict = read_config_files(config_files)
 | 
						|
    config.parse_config_dict(
 | 
						|
        config_dict,
 | 
						|
    )
 | 
						|
 | 
						|
    since_ms = time.time() * 1000 - Config.parse_duration(config_args.since)
 | 
						|
    exclude_users_with_email = config_args.exclude_emails
 | 
						|
    exclude_users_with_appservice = config_args.exclude_app_service
 | 
						|
    include_context = not config_args.only_users
 | 
						|
 | 
						|
    for database_config in config.database.databases:
 | 
						|
        if "main" in database_config.databases:
 | 
						|
            break
 | 
						|
 | 
						|
    engine = create_engine(database_config.config)
 | 
						|
 | 
						|
    with make_conn(database_config, engine, "review_recent_signups") as db_conn:
 | 
						|
        # This generates a type of Cursor, not LoggingTransaction.
 | 
						|
        user_infos = get_recent_users(db_conn.cursor(), since_ms, exclude_users_with_appservice)  # type: ignore[arg-type]
 | 
						|
 | 
						|
    for user_info in user_infos:
 | 
						|
        if exclude_users_with_email and user_info.emails:
 | 
						|
            continue
 | 
						|
 | 
						|
        if include_context:
 | 
						|
            print_public_rooms = ""
 | 
						|
            if user_info.public_rooms:
 | 
						|
                print_public_rooms = "(" + ", ".join(user_info.public_rooms[:3])
 | 
						|
 | 
						|
                if len(user_info.public_rooms) > 3:
 | 
						|
                    print_public_rooms += ", ..."
 | 
						|
 | 
						|
                print_public_rooms += ")"
 | 
						|
 | 
						|
            print("# Created:", datetime.fromtimestamp(user_info.creation_ts))
 | 
						|
            print("# Email:", ", ".join(user_info.emails) or "None")
 | 
						|
            print("# IPs:", ", ".join(user_info.ips))
 | 
						|
            print(
 | 
						|
                "# Number joined public rooms:",
 | 
						|
                len(user_info.public_rooms),
 | 
						|
                print_public_rooms,
 | 
						|
            )
 | 
						|
            print("# Number joined private rooms:", len(user_info.private_rooms))
 | 
						|
            print("#")
 | 
						|
 | 
						|
        print(user_info.user_id)
 | 
						|
 | 
						|
        if include_context:
 | 
						|
            print()
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    main()
 |