LittlePythonSheep/linuxdo.py
2024-03-28 17:52:26 +08:00

123 lines
3.6 KiB
Python

# A script to generate a user API key on a Discourse site.
# Authorized by author on https://shuiyuan.sjtu.edu.cn/t/topic/123808 to use this code under MIT license.
import base64
import json
import secrets
import urllib.parse
import uuid
import webbrowser
from collections.abc import Iterable
from dataclasses import dataclass
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
# From: https://github.com/discourse/discourse/blob/main/app/models/user_api_key_scope.rb
ALL_SCOPES = [
'read',
'write',
'message_bus',
'push',
'one_time_password',
'notifications',
'session_info',
'bookmarks_calendar',
'user_status',
]
DEFAULT_SCOPES = ['read']
@dataclass
class UserApiKeyPayload:
key: str
nonce: str
push: bool
api: int
@dataclass
class UserApiKeyRequestResult:
client_id: str
payload: UserApiKeyPayload
# Ref:
# https://meta.discourse.org/t/user-api-keys-specification/48536
# https://github.com/discourse/discourse/blob/main/app/controllers/user_api_keys_controller.rb
def generate_user_api_key(
site_url_base: str,
application_name: str, *,
client_id: str | None = None,
scopes: Iterable[str] | None = None,
) -> UserApiKeyRequestResult:
# Generate RSA key pair.
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
public_key = private_key.public_key()
public_key_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode('ascii')
# Generate a random client ID if not provided.
client_id_to_use = str(uuid.uuid4()) if client_id is None else client_id
nonce = secrets.token_urlsafe(32)
# Validate scopes.
scopes_list = DEFAULT_SCOPES if scopes is None else list(scopes)
if not set(scopes_list) <= set(ALL_SCOPES):
raise ValueError('Invalid scopes')
# Build request URL and open in browser.
params_dict: dict[str, str] = {
'application_name': application_name,
'client_id': client_id_to_use,
'scopes': ','.join(scopes_list),
'public_key': public_key_pem,
'nonce': nonce,
}
params_str = '&'.join(f'{k}={urllib.parse.quote(v)}' for k, v in params_dict.items())
webbrowser.open(f'{site_url_base}/user-api-key/new?{params_str}')
# Receive, decrypt and check response payload from server.
enc_payload = input('Paste the response payload here: ')
dec_payload = UserApiKeyPayload(**json.loads(private_key.decrypt(
base64.b64decode(enc_payload),
padding.PKCS1v15(),
)))
if dec_payload.nonce != nonce:
raise ValueError('Nonce mismatch')
# Return client ID and response payload.
return UserApiKeyRequestResult(
client_id=client_id_to_use,
payload=dec_payload,
)
def test_user_api_key(site_url_base: str, key: str) -> None:
# Get the current session information from the Discourse site.
print("key:", key)
r = requests.put(
f'https://linux.do/u/codecow/invited.json?filter=pending&offset=0',
headers={'User-Api-Key': key},
timeout=5,
)
# Expect some results.
print(r.json())
def main() -> None:
site_url_base = 'https://linux.do'
# Generate a user API key and test it.
result = generate_user_api_key(site_url_base, 'LinuxDo Invites Pool App')
print(result) # Store this somewhere
test_user_api_key(site_url_base, result.payload.key)
if __name__ == '__main__':
main()