Skip to content

Commit

Permalink
Add list rooms & list users
Browse files Browse the repository at this point in the history
  • Loading branch information
nexy7574 committed Nov 14, 2023
1 parent 6264ce1 commit 94c13f4
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 2 deletions.
12 changes: 12 additions & 0 deletions .idea/dataSources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
click>=8.1.7
rich>=13.6.0
click>=8.1.7,<9
rich>=13.6.0,<14
httpx[http2,brotli]>=0.25.0
toml>=0.10.2
psycopg>=3.1.12,<4
135 changes: 135 additions & 0 deletions src/dendritecli/_sql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import functools
import logging
import sqlite3
import typing
from pathlib import Path

import psycopg
from urllib.parse import urlparse, parse_qs


__all__ = (
"SQLHandler",
)


class SQLHandler:
def __init__(self, uri: str):
url = urlparse(uri)
if url.scheme.lower() not in ("postgres", "postgresql", "sqlite", "sqlite3"):
raise ValueError("Invalid URI scheme (not of postgres:// or sqlite3://)")
if len(url.path) <= 1:
raise ValueError("Invalid URI path (no database name)")

self.username = url.username or None
self.password = url.password or None
self.host = url.hostname or None
self.port = url.port or 5432
self.database = url.path[1:]

if url.query:
query = parse_qs(url.query)
self.sslmode = query.get("sslmode", ["prefer"])[0]
else:
self.sslmode = "prefer"
if url.scheme.lower() in ("sqlite", "sqlite3"):
path = Path(url.path).absolute()
if not path.exists():
raise FileNotFoundError(f"Database file {path} does not exist.")
self.driver = functools.partial(sqlite3.connect, str(path))
else:
self.driver = self._psycopg_connect()

self.connection: typing.Optional[typing.Union[psycopg.Connection, sqlite3.Connection]] = None
self.log = logging.getLogger("dendritecli.sql")

def __enter__(self) -> "SQLHandler":
self.log.info("Connecting to database.")
self.connection = self.driver()
self.log.debug("Connection to database established.")
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
self.log.info("Closing database connection")
self.connection.close()
self.log.debug("Database connection closed")
self.connection = None
self.log.debug("Database connection destroyed.")

def _psycopg_connect(self):
return functools.partial(
psycopg.connect,
user=self.username,
password=self.password,
host=self.host,
port=self.port,
dbname=self.database,
sslmode=self.sslmode,
)

def list_accounts(self) -> typing.Generator[dict[str, str | int | None], None, None]:
"""
Lists all accounts registered in the database.
This function is a generator.
"""
with self as instance:
with instance.connection.cursor() as cursor:
self.log.info("Querying userapi_accounts and userapi_profiles tables.")
cursor.execute(
"""
SELECT
userapi_accounts.localpart,
userapi_accounts.server_name,
created_ts,
appservice_id,
is_deactivated,
account_type,
display_name,
avatar_url
FROM userapi_accounts
INNER JOIN userapi_profiles
ON userapi_accounts.localpart = userapi_profiles.localpart;
"""
)
for row in cursor:
self.log.debug("Yielding row %r", row)
yield {
"localpart": row[0],
"server_name": row[1],
"created_ts": row[2],
"appservice_id": row[3],
"is_deactivated": row[4],
"account_type": row[5],
"display_name": row[6],
"avatar_url": row[7],
}

def list_rooms(self) -> typing.Generator[dict[str, str | int | None], None, None]:
"""
Lists all rooms registered in the database.
Also fetches and known room aliases.
"""
with self as instance:
with instance.connection.cursor() as cursor:
self.log.info("Querying roomserver_room_aliases and roomserver_rooms tables via left join.")
cursor.execute(
"""
SELECT
roomserver_room_aliases.alias,
roomserver_rooms.room_id,
roomserver_rooms.room_version
FROM roomserver_rooms
LEFT JOIN roomserver_room_aliases
ON roomserver_room_aliases.room_id = roomserver_rooms.room_id;
"""
)
for row in cursor:
self.log.debug("Yielding row %r", row)
yield {
"alias": row[0],
"room_id": row[1],
"room_version": row[2],
}
11 changes: 11 additions & 0 deletions src/dendritecli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import typing
from urllib.parse import quote
from pathlib import Path
from ._sql import SQLHandler

import httpx
import toml
Expand Down Expand Up @@ -424,3 +425,13 @@ def deactivate(self, user_id: str) -> None:
response.raise_for_status()
log.info("Done deactivating user %s", user_id)
return

def list_accounts(self, uri: str):
"""
Lists all accounts registered in the database.
:return: A list of user accounts and their profiles in one dictionary
"""
with SQLHandler(uri) as sql:
users = list(sql.list_accounts())
return users
95 changes: 95 additions & 0 deletions src/dendritecli/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import getpass
import logging
import pathlib
Expand All @@ -7,6 +8,7 @@
import rich
from rich.logging import RichHandler
from rich.prompt import Confirm, Prompt
from rich.table import Table

from . import api

Expand All @@ -30,6 +32,7 @@
@click.option(
"--log-level",
"-l",
"-L",
default="INFO",
help="Log level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
Expand Down Expand Up @@ -265,3 +268,95 @@ def deactivate(http: api.HTTPAPIManager, user_id: str):
with console.status("Deactivating user..."):
http.deactivate(user_id)
console.print(f"[green]Deactivated {user_id}.")


@main.command(name="list-accounts")
@click.option(
"--database-uri",
"--uri",
"--database",
"-D",
help="The database URI to connect to (postgres[ql]:// or sqlite[3]://)",
default=None
)
@click.pass_obj
def list_accounts(http: api.HTTPAPIManager, database_uri: str | None):
"""
List all accounts registered in the database.
DATABASE_URI should be the database URI to connect to (postgres:// or sqlite3://).
"""
if database_uri is None:
config = http.read_config()
database_uri = config.get("database_uri")
if database_uri is None:
console.print("[yellow]No database URI provided.")
database_uri = Prompt.ask("Database URI (postgres:// or sqlite3://)")

table = Table(
"localpart",
"display name",
"created at",
"appservice ID",
"is deactivated",
"account type",
"avatar URL"
)
sql = api.SQLHandler(database_uri)
accounts = list(sql.list_accounts())
log.debug("Found accounts: %r", accounts)
accounts.sort(key=lambda x: x["created_ts"])
for account in accounts:
if account["created_ts"] is None:
user_created_at = '\u200b'
else:
user_created_at = datetime.datetime.fromtimestamp(
account["created_ts"] / 1000,
datetime.timezone.utc
).strftime("%Y-%m-%d %H:%M:%S %Z")
deactivated = account["is_deactivated"]
table.add_row(
account["localpart"],
account["display_name"],
user_created_at,
str(account["appservice_id"] or '\u200b'),
"[%s]%s[/]" % ("green" if deactivated else "red", deactivated),
str(account["account_type"]),
account["avatar_url"] or '\u200b'
)
console.print(table)


@main.command(name="list-rooms")
@click.option(
"--database-uri",
"--uri",
"--database",
"-D",
help="The database URI to connect to (postgres[ql]:// or sqlite[3]://)",
default=None
)
@click.pass_obj
def list_rooms(http: api.HTTPAPIManager, database_uri: str | None):
if database_uri is None:
config = http.read_config()
database_uri = config.get("database_uri")
if database_uri is None:
console.print("[yellow]No database URI provided.")
database_uri = Prompt.ask("Database URI (postgres:// or sqlite3://)")

table = Table(
"Alias",
"Room ID",
"Version"
)
sql = api.SQLHandler(database_uri)
rooms = list(sql.list_rooms())
log.debug("Found rooms: %r", rooms)
for _room in rooms:
table.add_row(
_room["alias"] or '\u200b',
_room["room_id"],
str(_room["room_version"])
)
console.print(table)

0 comments on commit 94c13f4

Please sign in to comment.