├── tests └── __init__.py ├── psychonaut ├── __init__.py ├── api │ ├── __init__.py │ └── lexicons │ │ ├── app │ │ ├── __init__.py │ │ └── bsky │ │ │ ├── __init__.py │ │ │ ├── actor │ │ │ ├── __init__.py │ │ │ ├── profile.py │ │ │ ├── get_profile.py │ │ │ ├── search_actors_typeahead.py │ │ │ ├── get_suggestions.py │ │ │ ├── get_profiles.py │ │ │ └── search_actors.py │ │ │ ├── embed │ │ │ ├── __init__.py │ │ │ ├── record_with_media.py │ │ │ ├── images.py │ │ │ ├── external.py │ │ │ └── record.py │ │ │ ├── feed │ │ │ ├── __init__.py │ │ │ ├── like.py │ │ │ ├── repost.py │ │ │ ├── get_posts.py │ │ │ ├── get_timeline.py │ │ │ ├── get_post_thread.py │ │ │ ├── get_author_feed.py │ │ │ ├── get_reposted_by.py │ │ │ ├── post.py │ │ │ └── get_likes.py │ │ │ ├── graph │ │ │ ├── __init__.py │ │ │ ├── block.py │ │ │ ├── follow.py │ │ │ ├── mute_actor.py │ │ │ ├── unmute_actor.py │ │ │ ├── get_mutes.py │ │ │ ├── get_blocks.py │ │ │ ├── get_follows.py │ │ │ └── get_followers.py │ │ │ ├── richtext │ │ │ ├── __init__.py │ │ │ └── facet.py │ │ │ ├── notification │ │ │ ├── __init__.py │ │ │ ├── update_seen.py │ │ │ ├── get_unread_count.py │ │ │ └── list_notifications.py │ │ │ └── unspecced │ │ │ ├── __init__.py │ │ │ └── get_popular.py │ │ └── com │ │ ├── __init__.py │ │ └── atproto │ │ ├── __init__.py │ │ ├── admin │ │ ├── __init__.py │ │ ├── get_moderation_action.py │ │ ├── get_moderation_report.py │ │ ├── get_repo.py │ │ ├── disable_invite_codes.py │ │ ├── get_record.py │ │ ├── reverse_moderation_action.py │ │ ├── update_account_handle.py │ │ ├── resolve_moderation_reports.py │ │ ├── update_account_email.py │ │ ├── get_invite_codes.py │ │ ├── search_repos.py │ │ ├── get_moderation_actions.py │ │ ├── get_moderation_reports.py │ │ └── take_moderation_action.py │ │ ├── label │ │ ├── __init__.py │ │ ├── subscribe_labels.py │ │ ├── query_labels.py │ │ └── defs.py │ │ ├── repo │ │ ├── __init__.py │ │ ├── strong_ref.py │ │ ├── upload_blob.py │ │ ├── describe_repo.py │ │ ├── delete_record.py │ │ ├── get_record.py │ │ ├── create_record.py │ │ ├── apply_writes.py │ │ └── put_record.py │ │ ├── sync │ │ ├── __init__.py │ │ ├── request_crawl.py │ │ ├── notify_of_update.py │ │ ├── get_blocks.py │ │ ├── get_head.py │ │ ├── get_blob.py │ │ ├── get_checkout.py │ │ ├── get_record.py │ │ ├── list_repos.py │ │ ├── get_repo.py │ │ ├── list_blobs.py │ │ ├── get_commit_path.py │ │ └── subscribe_repos.py │ │ ├── identity │ │ ├── __init__.py │ │ ├── update_handle.py │ │ └── resolve_handle.py │ │ ├── moderation │ │ ├── __init__.py │ │ ├── defs.py │ │ └── create_report.py │ │ └── server │ │ ├── __init__.py │ │ ├── delete_session.py │ │ ├── request_account_delete.py │ │ ├── revoke_app_password.py │ │ ├── request_password_reset.py │ │ ├── reset_password.py │ │ ├── delete_account.py │ │ ├── defs.py │ │ ├── create_invite_code.py │ │ ├── create_app_password.py │ │ ├── get_session.py │ │ ├── get_account_invite_codes.py │ │ ├── refresh_session.py │ │ ├── list_app_passwords.py │ │ ├── describe_server.py │ │ ├── create_invite_codes.py │ │ ├── create_account.py │ │ └── create_session.py ├── cli │ ├── __init__.py │ ├── group.py │ ├── main.py │ ├── config.py │ ├── util.py │ ├── graph.py │ └── useful_queries.py ├── firehose │ ├── types.py │ ├── __init__.py │ ├── segment_ops.py │ ├── events_test.py │ ├── exponential_backoff.py │ └── simple_reader.py ├── common_web │ ├── __init__.py │ └── ipld_test.py ├── identifier │ ├── __init__.py │ ├── resolve_test.py │ ├── resolve.py │ └── did.py ├── lexicon │ ├── __init__.py │ ├── constants.py │ ├── fixtures │ │ └── pydantic_model_lex_xrpc_parameters.txt │ ├── NOTES.md │ ├── defs.py │ ├── tools.py │ ├── ctx.py │ └── codegen_test.py ├── nsid │ └── __init__.py ├── util.py └── client │ └── cursors.py ├── docs ├── requirements.txt └── index.md ├── mkdocs.yml ├── psychonaut_logo.png ├── .gitignore ├── lexicons ├── com │ └── atproto │ │ ├── server │ │ ├── deleteSession.json │ │ ├── requestAccountDelete.json │ │ ├── revokeAppPassword.json │ │ ├── requestPasswordReset.json │ │ ├── getSession.json │ │ ├── resetPassword.json │ │ ├── deleteAccount.json │ │ ├── refreshSession.json │ │ ├── createInviteCode.json │ │ ├── describeServer.json │ │ ├── listAppPasswords.json │ │ ├── defs.json │ │ ├── getAccountInviteCodes.json │ │ ├── createAppPassword.json │ │ ├── createSession.json │ │ ├── createInviteCodes.json │ │ └── createAccount.json │ │ ├── repo │ │ ├── strongRef.json │ │ ├── uploadBlob.json │ │ ├── describeRepo.json │ │ ├── getRecord.json │ │ ├── deleteRecord.json │ │ ├── listRecords.json │ │ └── createRecord.json │ │ ├── sync │ │ ├── requestCrawl.json │ │ ├── notifyOfUpdate.json │ │ ├── getBlob.json │ │ ├── getBlocks.json │ │ ├── getCheckout.json │ │ ├── getHead.json │ │ ├── getRecord.json │ │ ├── getRepo.json │ │ ├── listBlobs.json │ │ ├── listRepos.json │ │ └── getCommitPath.json │ │ ├── identity │ │ ├── updateHandle.json │ │ └── resolveHandle.json │ │ ├── admin │ │ ├── getRepo.json │ │ ├── getModerationAction.json │ │ ├── getModerationReport.json │ │ ├── updateAccountHandle.json │ │ ├── getRecord.json │ │ ├── disableInviteCodes.json │ │ ├── updateAccountEmail.json │ │ ├── reverseModerationAction.json │ │ ├── resolveModerationReports.json │ │ ├── getModerationActions.json │ │ ├── searchRepos.json │ │ ├── getModerationReports.json │ │ ├── getInviteCodes.json │ │ └── takeModerationAction.json │ │ ├── label │ │ ├── subscribeLabels.json │ │ ├── defs.json │ │ └── queryLabels.json │ │ └── moderation │ │ ├── defs.json │ │ └── createReport.json └── app │ └── bsky │ ├── feed │ ├── repost.json │ ├── like.json │ ├── getPosts.json │ ├── getTimeline.json │ ├── getPostThread.json │ ├── getAuthorFeed.json │ ├── getRepostedBy.json │ └── getLikes.json │ ├── graph │ ├── block.json │ ├── follow.json │ ├── muteActor.json │ ├── unmuteActor.json │ ├── getMutes.json │ ├── getBlocks.json │ ├── getFollows.json │ └── getFollowers.json │ ├── actor │ ├── getProfile.json │ ├── searchActorsTypeahead.json │ ├── getProfiles.json │ ├── profile.json │ ├── getSuggestions.json │ └── searchActors.json │ ├── notification │ ├── updateSeen.json │ ├── getUnreadCount.json │ └── listNotifications.json │ ├── unspecced │ └── getPopular.json │ ├── embed │ ├── recordWithMedia.json │ ├── images.json │ └── external.json │ └── richtext │ └── facet.json ├── .readthedocs.yaml ├── Makefile ├── hg2atp └── 00_intro.ipynb ├── LICENSE ├── .github └── workflows │ └── test.yaml ├── pyproject.toml ├── bin └── lexicon_codegen.py └── examples └── get_liked_posts_by_handle.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/firehose/types.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.4 -------------------------------------------------------------------------------- /psychonaut/common_web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/firehose/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/identifier/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/lexicon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/firehose/segment_ops.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/embed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/richtext/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/label/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/notification/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/unspecced/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/identity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/moderation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /psychonaut/nsid/__init__.py: -------------------------------------------------------------------------------- 1 | from .nsid import NSID 2 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Psychonaut Docs 2 | theme: 3 | name: readthedocs -------------------------------------------------------------------------------- /psychonaut/lexicon/constants.py: -------------------------------------------------------------------------------- 1 | MODULE_PREFIX = "psychonaut.api.lexicons." -------------------------------------------------------------------------------- /psychonaut_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbn/psychonaut/HEAD/psychonaut_logo.png -------------------------------------------------------------------------------- /psychonaut/cli/group.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | 4 | 5 | @click.group() 6 | def cli(): 7 | pass -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./lexicons/ 2 | dist 3 | __pycache__ 4 | .vscode 5 | *.pyc 6 | .env 7 | graph.jsonl 8 | graph_tasks.db 9 | .coverage 10 | streams/ 11 | 12 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/deleteSession.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.deleteSession", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Delete the current session." 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /lexicons/com/atproto/server/requestAccountDelete.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.requestAccountDelete", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Initiate a user account deletion via email." 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # psychonaut docs 2 | 3 | `psychonaut` is an async python {`bsky`, `atp`} x {`sdk`, `cli`}. 4 | 5 | It is a weekend-project WIP, so use at you're own risk. However, 6 | I am using it for a bunch of things, so it's not completely broken. 7 | And as long as you pin the version you're using (until this thing 8 | gets more stable) you should be fine. 9 | -------------------------------------------------------------------------------- /psychonaut/lexicon/fixtures/pydantic_model_lex_xrpc_parameters.txt: -------------------------------------------------------------------------------- 1 | class TestReq(BaseModel): 2 | """ 3 | test 1 4 | """ 5 | a_bool: bool = Field(default=True, description='A boolean') 6 | an_int: Optional[int] = Field(default=42, description='An integer', ge=0, le=100) 7 | a_string: Optional[str] = Field(default=None, description='A string', max_length=100) 8 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/moderation/defs.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from enum import auto, Enum 3 | 4 | 5 | class ReasonType(str, Enum): 6 | REASONVIOLATION = "reasonViolation" 7 | REASONOTHER = "reasonOther" 8 | REASONSPAM = "reasonSpam" 9 | REASONMISLEADING = "reasonMisleading" 10 | REASONRUDE = "reasonRude" 11 | REASONSEXUAL = "reasonSexual" 12 | -------------------------------------------------------------------------------- /psychonaut/firehose/events_test.py: -------------------------------------------------------------------------------- 1 | from multiformats import CID 2 | from .events import IndexRecord 3 | 4 | 5 | def test_index_record(): 6 | r = IndexRecord( 7 | type="index_record", 8 | action="Create", 9 | uri="at://foo/bar", 10 | cid=CID.decode("zdpuArKcqh4Bfc5ufSWKTSS1jFRYJ47gpuxCEVXeWdMEjDpAM"), 11 | timestamp="2021-01-01T00:00:00Z", 12 | ) 13 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/strong_ref.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_at_uri, validate_cid 4 | 5 | 6 | class StrongRef(BaseModel): 7 | """ 8 | [none provided by spec] 9 | """ 10 | 11 | uri: str = Field(..., pre=True, validator=validate_at_uri) 12 | cid: str = Field(..., pre=True, validator=validate_cid) 13 | -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/strongRef.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.strongRef", 4 | "description": "A URI with a content-hash fingerprint.", 5 | "defs": { 6 | "main": { 7 | "type": "object", 8 | "required": ["uri", "cid"], 9 | "properties": { 10 | "uri": {"type": "string", "format": "at-uri"}, 11 | "cid": {"type": "string", "format": "cid"} 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /psychonaut/cli/main.py: -------------------------------------------------------------------------------- 1 | from psychonaut.cli.group import cli 2 | 3 | # Imports to assemble the cli 4 | from psychonaut.cli.config import * # noqa 5 | from psychonaut.cli.stream import * # noqa 6 | from psychonaut.cli.useful_queries import * # noqa 7 | from psychonaut.cli.poasting import * # noqa 8 | from psychonaut.cli.graph import * # noqa 9 | 10 | 11 | 12 | 13 | # TODO: test poetry install 14 | if __name__ == "__main__": 15 | cli() 16 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/delete_session.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel 4 | 5 | 6 | class DeleteSessionReq(BaseModel): 7 | """ 8 | Delete the current session. 9 | """ 10 | 11 | @property 12 | def xrpc_id(self) -> str: 13 | return "com.atproto.server.deleteSession" 14 | 15 | async def do_xrpc(self, sess: Session) -> Any: 16 | return await sess.procedure(self) 17 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/repost.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.repost", 4 | "defs": { 5 | "main": { 6 | "type": "record", 7 | "key": "tid", 8 | "record": { 9 | "type": "object", 10 | "required": ["subject", "createdAt"], 11 | "properties": { 12 | "subject": {"type": "ref", "ref": "com.atproto.repo.strongRef"}, 13 | "createdAt": {"type": "string", "format": "datetime"} 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/like.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.like", 4 | "defs": { 5 | "main": { 6 | "type": "record", 7 | "key": "tid", 8 | "record": { 9 | "type": "object", 10 | "required": ["subject", "createdAt"], 11 | "properties": { 12 | "subject": {"type": "ref", "ref": "com.atproto.repo.strongRef"}, 13 | "createdAt": {"type": "string", "format": "datetime"} 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/block.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_did, validate_datetime 4 | 5 | 6 | class Block(BaseModel): 7 | """ 8 | A block. 9 | """ 10 | 11 | subject: str = Field(..., pre=True, validator=validate_did) 12 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "app.bsky.graph.block" 17 | -------------------------------------------------------------------------------- /psychonaut/common_web/ipld_test.py: -------------------------------------------------------------------------------- 1 | from .ipld import json_to_ipld, ipld_to_json, ipld_equals 2 | 3 | 4 | def test_ipld(): 5 | person = { 6 | "name": "Alice", 7 | "age": 30, 8 | "friend": { 9 | "/": { 10 | "$link": "bafyreih3j6mikj3ocg2reh4ox62uthv7g5rgl6yvbdg3qkbhaya5omazie" 11 | } 12 | } 13 | } 14 | 15 | person_ipld = json_to_ipld(person) 16 | person_json = ipld_to_json(person_ipld) 17 | assert ipld_equals(person, person_json) -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/follow.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_did, validate_datetime 4 | 5 | 6 | class Follow(BaseModel): 7 | """ 8 | A social follow. 9 | """ 10 | 11 | subject: str = Field(..., pre=True, validator=validate_did) 12 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "app.bsky.graph.follow" 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: refresh_canonical_lexicons 2 | refresh_canonical_lexicons: 3 | @echo "Refreshing canonical lexicons..." 4 | 5 | @echo " - Fetching latest canonical lexicons..." && \ 6 | temp_dir=$$(mktemp -d) && \ 7 | git clone git@github.com:bluesky-social/atproto.git "$$temp_dir" && \ 8 | echo " - Removing old canonical lexicons..." && \ 9 | rm -rf lexicons && \ 10 | echo " - Copying new canonical lexicons..." && \ 11 | mv "$$temp_dir/lexicons" . && \ 12 | echo " - Cleaning up..." && \ 13 | rm -rf "$$temp_dir"; 14 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.block", 4 | "defs": { 5 | "main": { 6 | "type": "record", 7 | "description": "A block.", 8 | "key": "tid", 9 | "record": { 10 | "type": "object", 11 | "required": ["subject", "createdAt"], 12 | "properties": { 13 | "subject": {"type": "string", "format": "did"}, 14 | "createdAt": {"type": "string", "format": "datetime"} 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/request_account_delete.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel 4 | 5 | 6 | class RequestAccountDeleteReq(BaseModel): 7 | """ 8 | Initiate a user account deletion via email. 9 | """ 10 | 11 | @property 12 | def xrpc_id(self) -> str: 13 | return "com.atproto.server.requestAccountDelete" 14 | 15 | async def do_xrpc(self, sess: Session) -> Any: 16 | return await sess.procedure(self) 17 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/follow.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.follow", 4 | "defs": { 5 | "main": { 6 | "type": "record", 7 | "description": "A social follow.", 8 | "key": "tid", 9 | "record": { 10 | "type": "object", 11 | "required": ["subject", "createdAt"], 12 | "properties": { 13 | "subject": {"type": "string", "format": "did"}, 14 | "createdAt": {"type": "string", "format": "datetime"} 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/requestCrawl.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.requestCrawl", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Request a service to persistently crawl hosted repos.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["hostname"], 11 | "properties": { 12 | "hostname": {"type": "string", "description": "Hostname of the service that is requesting to be crawled."} 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/like.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.lexicons.com.atproto.repo.strong_ref import StrongRef 4 | from psychonaut.lexicon.formats import validate_datetime 5 | 6 | 7 | class Like(BaseModel): 8 | """ 9 | [none provided by spec] 10 | """ 11 | 12 | subject: StrongRef 13 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "app.bsky.feed.like" 18 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/profile.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class Profile(BaseModel): 6 | """ 7 | [none provided by spec] 8 | """ 9 | 10 | displayName: Optional[str] = Field(default=None, max_length=640) 11 | description: Optional[str] = Field(default=None, max_length=2560) 12 | avatar: Optional[Any] = None 13 | banner: Optional[Any] = None 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "app.bsky.actor.profile" 18 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/muteActor.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.muteActor", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Mute an actor by did or handle.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["actor"], 13 | "properties": { 14 | "actor": { "type": "string", "format": "at-identifier" } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/repost.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.lexicons.com.atproto.repo.strong_ref import StrongRef 4 | from psychonaut.lexicon.formats import validate_datetime 5 | 6 | 7 | class Repost(BaseModel): 8 | """ 9 | [none provided by spec] 10 | """ 11 | 12 | subject: StrongRef 13 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "app.bsky.feed.repost" 18 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/unmuteActor.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.unmuteActor", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Unmute an actor by did or handle.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["actor"], 13 | "properties": { 14 | "actor": { "type": "string", "format": "at-identifier" } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/revokeAppPassword.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.revokeAppPassword", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Revoke an app-specific password by name.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["name"], 13 | "properties": { 14 | "name": {"type": "string"} 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_moderation_action.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class GetModerationActionReq(BaseModel): 7 | """ 8 | View details about a moderation action. 9 | """ 10 | 11 | id: int = Field(...) 12 | 13 | @property 14 | def xrpc_id(self) -> str: 15 | return "com.atproto.admin.getModerationAction" 16 | 17 | async def do_xrpc(self, sess: Session) -> Any: 18 | return await sess.query(self) 19 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_moderation_report.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class GetModerationReportReq(BaseModel): 7 | """ 8 | View details about a moderation report. 9 | """ 10 | 11 | id: int = Field(...) 12 | 13 | @property 14 | def xrpc_id(self) -> str: 15 | return "com.atproto.admin.getModerationReport" 16 | 17 | async def do_xrpc(self, sess: Session) -> Any: 18 | return await sess.query(self) 19 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/revoke_app_password.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class RevokeAppPasswordReq(BaseModel): 7 | """ 8 | Revoke an app-specific password by name. 9 | """ 10 | 11 | name: str = Field(...) 12 | 13 | @property 14 | def xrpc_id(self) -> str: 15 | return "com.atproto.server.revokeAppPassword" 16 | 17 | async def do_xrpc(self, sess: Session) -> Any: 18 | return await sess.procedure(self) 19 | -------------------------------------------------------------------------------- /lexicons/com/atproto/identity/updateHandle.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.identity.updateHandle", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Updates the handle of the account", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["handle"], 13 | "properties": { 14 | "handle": {"type": "string", "format": "handle"} 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /lexicons/app/bsky/actor/getProfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.actor.getProfile", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "required": ["actor"], 10 | "properties": { 11 | "actor": {"type": "string", "format": "at-identifier"} 12 | } 13 | }, 14 | "output": { 15 | "encoding": "application/json", 16 | "schema": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewDetailed"} 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/requestPasswordReset.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.requestPasswordReset", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Initiate a user account password reset via email.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["email"], 13 | "properties": { 14 | "email": { "type": "string" } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/label/subscribe_labels.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.lexicons.com.atproto.label.defs import Label 4 | 5 | 6 | class Info(BaseModel): 7 | """ 8 | [none provided by spec] 9 | """ 10 | 11 | name: str = Field(..., known_values=["OutdatedCursor"]) 12 | message: Optional[str] = Field(default=None) 13 | 14 | 15 | class Labels(BaseModel): 16 | """ 17 | [none provided by spec] 18 | """ 19 | 20 | seq: int = Field(...) 21 | labels: Any 22 | -------------------------------------------------------------------------------- /psychonaut/identifier/resolve_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .resolve import resolve_dns, NoHandleRecordError 3 | 4 | 5 | @pytest.mark.asyncio 6 | async def test_resolve_dns(): 7 | handle = "generativist.xyz" 8 | expected_did = "did:plc:o32okshy54r5h2vlrjpz3aln" 9 | 10 | did = await resolve_dns(handle) 11 | assert did == expected_did 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_resolve_dns_no_handle_record_error(): 16 | handle = "nope.generativist.xyz" 17 | 18 | with pytest.raises(NoHandleRecordError): 19 | await resolve_dns(handle) -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/request_password_reset.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class RequestPasswordResetReq(BaseModel): 7 | """ 8 | Initiate a user account password reset via email. 9 | """ 10 | 11 | email: str = Field(...) 12 | 13 | @property 14 | def xrpc_id(self) -> str: 15 | return "com.atproto.server.requestPasswordReset" 16 | 17 | async def do_xrpc(self, sess: Session) -> Any: 18 | return await sess.procedure(self) 19 | -------------------------------------------------------------------------------- /lexicons/app/bsky/notification/updateSeen.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.notification.updateSeen", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Notify server that the user has seen notifications.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["seenAt"], 13 | "properties": { 14 | "seenAt": { "type": "string", "format": "datetime"} 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/reset_password.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class ResetPasswordReq(BaseModel): 7 | """ 8 | Reset a user account password using a token. 9 | """ 10 | 11 | token: str = Field(...) 12 | password: str = Field(...) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "com.atproto.server.resetPassword" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.procedure(self) 20 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_repo.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did 5 | 6 | 7 | class GetRepoReq(BaseModel): 8 | """ 9 | View details about a repository. 10 | """ 11 | 12 | did: str = Field(..., pre=True, validator=validate_did) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "com.atproto.admin.getRepo" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.query(self) 20 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getRepo.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getRepo", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "View details about a repository.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did"], 11 | "properties": { 12 | "did": {"type": "string", "format":"did"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": {"type": "ref", "ref": "com.atproto.admin.defs#repoViewDetail"} 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/uploadBlob.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.uploadBlob", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Upload a new blob to be added to repo in a later request.", 8 | "input": { 9 | "encoding": "*/*" 10 | }, 11 | "output": { 12 | "encoding": "application/json", 13 | "schema": { 14 | "type": "object", 15 | "required": ["blob"], 16 | "properties": { 17 | "blob": {"type": "blob"} 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/get_profile.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_at_identifier 5 | 6 | 7 | class GetProfileReq(BaseModel): 8 | """ 9 | [none provided by spec] 10 | """ 11 | 12 | actor: str = Field(..., pre=True, validator=validate_at_identifier) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "app.bsky.actor.getProfile" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.query(self) 20 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/notifyOfUpdate.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.notifyOfUpdate", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Notify a crawling service of a recent update. Often when a long break between updates causes the connection with the crawling service to break.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["hostname"], 11 | "properties": { 12 | "hostname": {"type": "string", "description": "Hostname of the service that is notifying of update."} 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/embed/record_with_media.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.lexicons.app.bsky.embed.record import View, Record 4 | from psychonaut.api.lexicons.app.bsky.embed.images import View 5 | from psychonaut.api.lexicons.app.bsky.embed.external import View 6 | 7 | 8 | class RecordWithMedia(BaseModel): 9 | """ 10 | [none provided by spec] 11 | """ 12 | 13 | record: Record 14 | media: Any 15 | 16 | 17 | class View(BaseModel): 18 | """ 19 | [none provided by spec] 20 | """ 21 | 22 | record: View 23 | media: Any 24 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/mute_actor.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_at_identifier 5 | 6 | 7 | class MuteActorReq(BaseModel): 8 | """ 9 | Mute an actor by did or handle. 10 | """ 11 | 12 | actor: str = Field(..., pre=True, validator=validate_at_identifier) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "app.bsky.graph.muteActor" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.procedure(self) 20 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getModerationAction.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getModerationAction", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "View details about a moderation action.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["id"], 11 | "properties": { 12 | "id": {"type": "integer"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": {"type": "ref", "ref": "com.atproto.admin.defs#actionViewDetail"} 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getModerationReport.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getModerationReport", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "View details about a moderation report.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["id"], 11 | "properties": { 12 | "id": {"type": "integer"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": {"type": "ref", "ref": "com.atproto.admin.defs#reportViewDetail"} 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/unmute_actor.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_at_identifier 5 | 6 | 7 | class UnmuteActorReq(BaseModel): 8 | """ 9 | Unmute an actor by did or handle. 10 | """ 11 | 12 | actor: str = Field(..., pre=True, validator=validate_at_identifier) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "app.bsky.graph.unmuteActor" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.procedure(self) 20 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/identity/update_handle.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_handle 5 | 6 | 7 | class UpdateHandleReq(BaseModel): 8 | """ 9 | Updates the handle of the account 10 | """ 11 | 12 | handle: str = Field(..., pre=True, validator=validate_handle) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "com.atproto.identity.updateHandle" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.procedure(self) 20 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/upload_blob.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class UploadBlobResp(BaseModel): 7 | blob: Any 8 | 9 | 10 | class UploadBlobReq(BaseModel): 11 | """ 12 | Upload a new blob to be added to repo in a later request. 13 | """ 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "com.atproto.repo.uploadBlob" 18 | 19 | async def do_xrpc(self, sess: Session) -> UploadBlobResp: 20 | resp = await sess.procedure(self) 21 | return UploadBlobResp(**resp) 22 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/request_crawl.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class RequestCrawlReq(BaseModel): 7 | """ 8 | Request a service to persistently crawl hosted repos. 9 | """ 10 | 11 | hostname: str = Field( 12 | ..., description="Hostname of the service that is requesting to be crawled." 13 | ) 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "com.atproto.sync.requestCrawl" 18 | 19 | async def do_xrpc(self, sess: Session) -> Any: 20 | return await sess.query(self) 21 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/updateAccountHandle.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.updateAccountHandle", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Administrative action to update an account's handle", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["did", "handle"], 13 | "properties": { 14 | "did": {"type": "string", "format": "did"}, 15 | "handle": {"type": "string", "format": "handle"} 16 | } 17 | } 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/notification/update_seen.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_datetime 5 | 6 | 7 | class UpdateSeenReq(BaseModel): 8 | """ 9 | Notify server that the user has seen notifications. 10 | """ 11 | 12 | seenAt: str = Field(..., pre=True, validator=validate_datetime) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "app.bsky.notification.updateSeen" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.procedure(self) 20 | -------------------------------------------------------------------------------- /lexicons/app/bsky/notification/getUnreadCount.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.notification.getUnreadCount", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "properties": { 10 | "seenAt": { "type": "string", "format": "datetime"} 11 | } 12 | }, 13 | "output": { 14 | "encoding": "application/json", 15 | "schema": { 16 | "type": "object", 17 | "required": ["count"], 18 | "properties": { 19 | "count": { "type": "integer"} 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/getSession.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.getSession", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get information about the current session.", 8 | "output": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["handle", "did"], 13 | "properties": { 14 | "handle": {"type": "string", "format": "handle"}, 15 | "did": {"type": "string", "format": "did"}, 16 | "email": {"type": "string"} 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getBlob.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getBlob", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get a blob associated with a given repo.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did", "cid"], 11 | "properties": { 12 | "did": {"type": "string", "format": "did", "description": "The DID of the repo."}, 13 | "cid": {"type": "string", "format": "cid", "description": "The CID of the blob to fetch"} 14 | } 15 | }, 16 | "output": { 17 | "encoding": "*/*" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getRecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getRecord", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "View details about a record.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["uri"], 11 | "properties": { 12 | "uri": {"type": "string", "format": "at-uri"}, 13 | "cid": {"type": "string", "format": "cid"} 14 | } 15 | }, 16 | "output": { 17 | "encoding": "application/json", 18 | "schema": {"type": "ref", "ref": "com.atproto.admin.defs#recordViewDetail"} 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /psychonaut/firehose/exponential_backoff.py: -------------------------------------------------------------------------------- 1 | class FirehoseExponentialBackoff: 2 | 3 | def __init__(self, initial_wait: int = 0, base: int = 2, max_wait: int = 60): 4 | self._initial_wait = initial_wait 5 | self._base = base 6 | self._max_wait = max_wait 7 | self._failures = 0 8 | 9 | def reset(self): 10 | self._failures = 0 11 | 12 | def next_sleep_time(self) -> int: 13 | sleep_time = self._initial_wait 14 | 15 | if self._failures > 0: 16 | sleep_time = min(self._max_wait, self._base ** self._failures) 17 | 18 | self._failures += 1 19 | 20 | return sleep_time 21 | -------------------------------------------------------------------------------- /psychonaut/cli/config.py: -------------------------------------------------------------------------------- 1 | import click 2 | from psychonaut.client.credentials import write_homedir_creds 3 | from .util import clean_handle, print_error_and_fail 4 | from .group import cli 5 | 6 | 7 | @cli.command() 8 | @click.argument("handle") 9 | @click.option("--allow-overwrite", is_flag=True, default=False) 10 | def save_login(handle: str, allow_overwrite: bool): 11 | handle = clean_handle(handle) 12 | try: 13 | with write_homedir_creds(handle, allow_overwrite) as f: 14 | f(click.prompt(f"Enter password for {handle}", hide_input=True)) 15 | except FileExistsError as e: 16 | print_error_and_fail(f"{e}: use --allow-overwrite to overwrite") 17 | -------------------------------------------------------------------------------- /psychonaut/util.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import json 4 | 5 | 6 | @pytest.fixture 7 | def load_test_fixture(request): 8 | def _load_file(filename, as_json=False, fixtures_dir=True): 9 | test_module_path = os.path.dirname(request.fspath) 10 | file_relative_path = filename 11 | if fixtures_dir: 12 | file_relative_path = os.path.join("fixtures", filename) 13 | file_absolute_path = os.path.join(test_module_path, file_relative_path) 14 | with open(file_absolute_path, "r") as file: 15 | res = file.read() 16 | if as_json: 17 | res = json.loads(res) 18 | return res 19 | return _load_file -------------------------------------------------------------------------------- /lexicons/com/atproto/server/resetPassword.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.resetPassword", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Reset a user account password using a token.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["token", "password"], 13 | "properties": { 14 | "token": { "type": "string" }, 15 | "password": { "type": "string" } 16 | } 17 | } 18 | }, 19 | "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/disable_invite_codes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class DisableInviteCodesReq(BaseModel): 7 | """ 8 | Disable some set of codes and/or all codes associated with a set of 9 | users 10 | """ 11 | 12 | codes: Optional[List[str]] = Field(default=None) 13 | accounts: Optional[List[str]] = Field(default=None) 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "com.atproto.admin.disableInviteCodes" 18 | 19 | async def do_xrpc(self, sess: Session) -> Any: 20 | return await sess.procedure(self) 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/delete_account.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did 5 | 6 | 7 | class DeleteAccountReq(BaseModel): 8 | """ 9 | Delete a user account with a token and password. 10 | """ 11 | 12 | did: str = Field(..., pre=True, validator=validate_did) 13 | password: str = Field(...) 14 | token: str = Field(...) 15 | 16 | @property 17 | def xrpc_id(self) -> str: 18 | return "com.atproto.server.deleteAccount" 19 | 20 | async def do_xrpc(self, sess: Session) -> Any: 21 | return await sess.procedure(self) 22 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/embed/images.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class Image(BaseModel): 6 | """ 7 | [none provided by spec] 8 | """ 9 | 10 | image: Any 11 | alt: str = Field(...) 12 | 13 | 14 | class ViewImage(BaseModel): 15 | """ 16 | [none provided by spec] 17 | """ 18 | 19 | thumb: str = Field(...) 20 | fullsize: str = Field(...) 21 | alt: str = Field(...) 22 | 23 | 24 | class Images(BaseModel): 25 | """ 26 | [none provided by spec] 27 | """ 28 | 29 | images: Any 30 | 31 | 32 | class View(BaseModel): 33 | """ 34 | [none provided by spec] 35 | """ 36 | 37 | images: Any 38 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_at_uri, validate_cid 5 | 6 | 7 | class GetRecordReq(BaseModel): 8 | """ 9 | View details about a record. 10 | """ 11 | 12 | uri: str = Field(..., pre=True, validator=validate_at_uri) 13 | cid: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "com.atproto.admin.getRecord" 18 | 19 | async def do_xrpc(self, sess: Session) -> Any: 20 | return await sess.query(self) 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/reverse_moderation_action.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did 5 | 6 | 7 | class ReverseModerationActionReq(BaseModel): 8 | """ 9 | Reverse a moderation action. 10 | """ 11 | 12 | id: int = Field(...) 13 | reason: str = Field(...) 14 | createdBy: str = Field(..., pre=True, validator=validate_did) 15 | 16 | @property 17 | def xrpc_id(self) -> str: 18 | return "com.atproto.admin.reverseModerationAction" 19 | 20 | async def do_xrpc(self, sess: Session) -> Any: 21 | return await sess.procedure(self) 22 | -------------------------------------------------------------------------------- /psychonaut/client/cursors.py: -------------------------------------------------------------------------------- 1 | from psychonaut.api.session import Session 2 | 3 | 4 | async def collect_cursored( 5 | sess: Session, 6 | req, 7 | resp_type, 8 | collection_k: str, 9 | cursor_key="cursor", 10 | ): 11 | cursor, last_cursor = None, None 12 | while True: 13 | req = req.copy(update={cursor_key: cursor}) 14 | resp = await req.do_xrpc(sess) 15 | assert isinstance(resp, resp_type) 16 | cursor = resp.cursor 17 | 18 | new_items = getattr(resp, collection_k) 19 | for item in new_items: 20 | yield item 21 | 22 | if not cursor or cursor == last_cursor or not new_items: 23 | break 24 | 25 | last_cursor = cursor -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/update_account_handle.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_handle, validate_did 5 | 6 | 7 | class UpdateAccountHandleReq(BaseModel): 8 | """ 9 | Administrative action to update an account's handle 10 | """ 11 | 12 | did: str = Field(..., pre=True, validator=validate_did) 13 | handle: str = Field(..., pre=True, validator=validate_handle) 14 | 15 | @property 16 | def xrpc_id(self) -> str: 17 | return "com.atproto.admin.updateAccountHandle" 18 | 19 | async def do_xrpc(self, sess: Session) -> Any: 20 | return await sess.procedure(self) 21 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/notify_of_update.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class NotifyOfUpdateReq(BaseModel): 7 | """ 8 | Notify a crawling service of a recent update. Often when a long break 9 | between updates causes the connection with the crawling service to 10 | break. 11 | """ 12 | 13 | hostname: str = Field( 14 | ..., description="Hostname of the service that is notifying of update." 15 | ) 16 | 17 | @property 18 | def xrpc_id(self) -> str: 19 | return "com.atproto.sync.notifyOfUpdate" 20 | 21 | async def do_xrpc(self, sess: Session) -> Any: 22 | return await sess.query(self) 23 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/deleteAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.deleteAccount", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Delete a user account with a token and password.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["did", "password", "token"], 13 | "properties": { 14 | "did": { "type": "string", "format": "did" }, 15 | "password": { "type": "string" }, 16 | "token": { "type": "string" } 17 | } 18 | } 19 | }, 20 | "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/resolve_moderation_reports.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did 5 | 6 | 7 | class ResolveModerationReportsReq(BaseModel): 8 | """ 9 | Resolve moderation reports by an action. 10 | """ 11 | 12 | actionId: int = Field(...) 13 | reportIds: List[int] = Field(...) 14 | createdBy: str = Field(..., pre=True, validator=validate_did) 15 | 16 | @property 17 | def xrpc_id(self) -> str: 18 | return "com.atproto.admin.resolveModerationReports" 19 | 20 | async def do_xrpc(self, sess: Session) -> Any: 21 | return await sess.procedure(self) 22 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/disableInviteCodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.disableInviteCodes", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Disable some set of codes and/or all codes associated with a set of users", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "properties": { 13 | "codes": { 14 | "type": "array", 15 | "items": {"type": "string"} 16 | }, 17 | "accounts": { 18 | "type": "array", 19 | "items": {"type": "string"} 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/updateAccountEmail.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.updateAccountEmail", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Administrative action to update an account's email", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["account", "email"], 13 | "properties": { 14 | "account": { 15 | "type": "string", 16 | "format": "at-identifier", 17 | "description": "The handle or DID of the repo." 18 | }, 19 | "email": {"type": "string"} 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getBlocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getBlocks", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Gets blocks from a given repo.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did", "cids"], 11 | "properties": { 12 | "did": { 13 | "type": "string", 14 | "format": "did", 15 | "description": "The DID of the repo." 16 | }, 17 | "cids": { 18 | "type": "array", 19 | "items": {"type": "string", "format": "cid"} 20 | } 21 | } 22 | }, 23 | "output": { 24 | "encoding": "application/vnd.ipld.car" 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_blocks.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did, validate_array 5 | 6 | 7 | class GetBlocksReq(BaseModel): 8 | """ 9 | Gets blocks from a given repo. 10 | """ 11 | 12 | did: str = Field( 13 | ..., description="The DID of the repo.", pre=True, validator=validate_did 14 | ) 15 | cids: List[str] = Field(..., pre=True, validator=validate_array(validate_cid)) 16 | 17 | @property 18 | def xrpc_id(self) -> str: 19 | return "com.atproto.sync.getBlocks" 20 | 21 | async def do_xrpc(self, sess: Session) -> Any: 22 | return await sess.query(self) 23 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/notification/get_unread_count.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_datetime 5 | 6 | 7 | class GetUnreadCountResp(BaseModel): 8 | count: int = Field(...) 9 | 10 | 11 | class GetUnreadCountReq(BaseModel): 12 | """ 13 | [none provided by spec] 14 | """ 15 | 16 | seenAt: Optional[str] = Field(default=None, pre=True, validator=validate_datetime) 17 | 18 | @property 19 | def xrpc_id(self) -> str: 20 | return "app.bsky.notification.getUnreadCount" 21 | 22 | async def do_xrpc(self, sess: Session) -> GetUnreadCountResp: 23 | resp = await sess.query(self) 24 | return GetUnreadCountResp(**resp) 25 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/defs.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_did, validate_datetime 4 | 5 | 6 | class InviteCodeUse(BaseModel): 7 | """ 8 | [none provided by spec] 9 | """ 10 | 11 | usedBy: str = Field(..., pre=True, validator=validate_did) 12 | usedAt: str = Field(..., pre=True, validator=validate_datetime) 13 | 14 | 15 | class InviteCode(BaseModel): 16 | """ 17 | [none provided by spec] 18 | """ 19 | 20 | code: str = Field(...) 21 | available: int = Field(...) 22 | disabled: bool = Field(...) 23 | forAccount: str = Field(...) 24 | createdBy: str = Field(...) 25 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 26 | uses: Any 27 | -------------------------------------------------------------------------------- /psychonaut/identifier/resolve.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | import aiodns 4 | 5 | 6 | SUBDOMAIN = "_atproto" 7 | PREFIX = "did=" 8 | 9 | 10 | class NoHandleRecordError(Exception): 11 | pass 12 | 13 | 14 | async def resolve_dns( 15 | handle: str, resolver: Optional[aiodns.DNSResolver] = None 16 | ) -> str: 17 | resolver = resolver or aiodns.DNSResolver() 18 | 19 | try: 20 | resp = await resolver.query(f"{SUBDOMAIN}.{handle}", "TXT") 21 | 22 | first_did = next((r.text for r in resp if r.text.startswith(PREFIX)), None) 23 | if first_did: 24 | return first_did[len(PREFIX) :] 25 | except aiodns.error.DNSError as err: 26 | if err.args[0] != aiodns.error.ARES_ENOTFOUND: 27 | raise err 28 | raise NoHandleRecordError() 29 | -------------------------------------------------------------------------------- /psychonaut/lexicon/NOTES.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | ## `black` processing 4 | 5 | This isn't optional because it's just got good properties. But for the 6 | purpose of debugging, it also will fail loudly for bad syntax. 7 | 8 | ## `--remove_existing` flag 9 | 10 | Removes the existing generated code before generating new code. This is 11 | helpful because intermediate files are not removed when the generator 12 | fails, so it's somewhat debuggable. 13 | 14 | ## `--import-all-test` flag 15 | 16 | After generating all the code, if this flag is set, the generator tries to 17 | import each generated module. If it can't do this, something has gone wrong 18 | so it will print out which failures and then raise an exceptino. 19 | 20 | ## TODO 21 | 22 | - [ ] Fail the unit test if the generated code does not match what was commited. -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/update_account_email.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_at_identifier 5 | 6 | 7 | class UpdateAccountEmailReq(BaseModel): 8 | """ 9 | Administrative action to update an account's email 10 | """ 11 | 12 | account: str = Field( 13 | ..., 14 | description="The handle or DID of the repo.", 15 | pre=True, 16 | validator=validate_at_identifier, 17 | ) 18 | email: str = Field(...) 19 | 20 | @property 21 | def xrpc_id(self) -> str: 22 | return "com.atproto.admin.updateAccountEmail" 23 | 24 | async def do_xrpc(self, sess: Session) -> Any: 25 | return await sess.procedure(self) 26 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/refreshSession.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.refreshSession", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Refresh an authentication session.", 8 | "output": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["accessJwt", "refreshJwt", "handle", "did"], 13 | "properties": { 14 | "accessJwt": {"type": "string"}, 15 | "refreshJwt": {"type": "string"}, 16 | "handle": {"type": "string", "format": "handle"}, 17 | "did": {"type": "string", "format": "did"} 18 | } 19 | } 20 | }, 21 | "errors": [ 22 | {"name": "AccountTakedown"} 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getCheckout.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getCheckout", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Gets the repo state.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did"], 11 | "properties": { 12 | "did": { 13 | "type": "string", 14 | "format": "did", 15 | "description": "The DID of the repo." 16 | }, 17 | "commit": { 18 | "type": "string", 19 | "format": "cid", 20 | "description": "The commit to get the checkout from. Defaults to current HEAD." 21 | } 22 | } 23 | }, 24 | "output": { 25 | "encoding": "application/vnd.ipld.car" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/get_mutes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetMutesResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | mutes: Any 10 | 11 | 12 | class GetMutesReq(BaseModel): 13 | """ 14 | Who does the viewer mute? 15 | """ 16 | 17 | limit: Optional[int] = Field(default=50, ge=1, le=100) 18 | cursor: Optional[str] = Field(default=None) 19 | 20 | @property 21 | def xrpc_id(self) -> str: 22 | return "app.bsky.graph.getMutes" 23 | 24 | async def do_xrpc(self, sess: Session) -> GetMutesResp: 25 | resp = await sess.query(self) 26 | return GetMutesResp(**resp) 27 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_head.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did 5 | 6 | 7 | class GetHeadResp(BaseModel): 8 | root: str = Field(..., pre=True, validator=validate_cid) 9 | 10 | 11 | class GetHeadReq(BaseModel): 12 | """ 13 | Gets the current HEAD CID of a repo. 14 | """ 15 | 16 | did: str = Field( 17 | ..., description="The DID of the repo.", pre=True, validator=validate_did 18 | ) 19 | 20 | @property 21 | def xrpc_id(self) -> str: 22 | return "com.atproto.sync.getHead" 23 | 24 | async def do_xrpc(self, sess: Session) -> GetHeadResp: 25 | resp = await sess.query(self) 26 | return GetHeadResp(**resp) 27 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getHead.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getHead", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Gets the current HEAD CID of a repo.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did"], 11 | "properties": { 12 | "did": { 13 | "type": "string", 14 | "format": "did", 15 | "description": "The DID of the repo." 16 | } 17 | } 18 | }, 19 | "output": { 20 | "encoding": "application/json", 21 | "schema": { 22 | "type": "object", 23 | "required": ["root"], 24 | "properties": { 25 | "root": {"type": "string", "format": "cid"} 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/create_invite_code.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did 5 | 6 | 7 | class CreateInviteCodeResp(BaseModel): 8 | code: str = Field(...) 9 | 10 | 11 | class CreateInviteCodeReq(BaseModel): 12 | """ 13 | Create an invite code. 14 | """ 15 | 16 | useCount: int = Field(...) 17 | forAccount: Optional[str] = Field(default=None, pre=True, validator=validate_did) 18 | 19 | @property 20 | def xrpc_id(self) -> str: 21 | return "com.atproto.server.createInviteCode" 22 | 23 | async def do_xrpc(self, sess: Session) -> CreateInviteCodeResp: 24 | resp = await sess.procedure(self) 25 | return CreateInviteCodeResp(**resp) 26 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_blob.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did 5 | 6 | 7 | class GetBlobReq(BaseModel): 8 | """ 9 | Get a blob associated with a given repo. 10 | """ 11 | 12 | did: str = Field( 13 | ..., description="The DID of the repo.", pre=True, validator=validate_did 14 | ) 15 | cid: str = Field( 16 | ..., 17 | description="The CID of the blob to fetch", 18 | pre=True, 19 | validator=validate_cid, 20 | ) 21 | 22 | @property 23 | def xrpc_id(self) -> str: 24 | return "com.atproto.sync.getBlob" 25 | 26 | async def do_xrpc(self, sess: Session) -> Any: 27 | return await sess.query(self) 28 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/get_blocks.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetBlocksResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | blocks: Any 10 | 11 | 12 | class GetBlocksReq(BaseModel): 13 | """ 14 | Who is the requester's account blocking? 15 | """ 16 | 17 | limit: Optional[int] = Field(default=50, ge=1, le=100) 18 | cursor: Optional[str] = Field(default=None) 19 | 20 | @property 21 | def xrpc_id(self) -> str: 22 | return "app.bsky.graph.getBlocks" 23 | 24 | async def do_xrpc(self, sess: Session) -> GetBlocksResp: 25 | resp = await sess.query(self) 26 | return GetBlocksResp(**resp) 27 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/create_app_password.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_datetime 5 | 6 | 7 | class CreateAppPasswordReq(BaseModel): 8 | """ 9 | Create an app-specific password. 10 | """ 11 | 12 | name: str = Field(...) 13 | 14 | @property 15 | def xrpc_id(self) -> str: 16 | return "com.atproto.server.createAppPassword" 17 | 18 | async def do_xrpc(self, sess: Session) -> Any: 19 | return await sess.procedure(self) 20 | 21 | 22 | class AppPassword(BaseModel): 23 | """ 24 | [none provided by spec] 25 | """ 26 | 27 | name: str = Field(...) 28 | password: str = Field(...) 29 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 30 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/get_session.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did, validate_handle 5 | 6 | 7 | class GetSessionResp(BaseModel): 8 | handle: str = Field(..., pre=True, validator=validate_handle) 9 | did: str = Field(..., pre=True, validator=validate_did) 10 | email: Optional[str] = Field(default=None) 11 | 12 | 13 | class GetSessionReq(BaseModel): 14 | """ 15 | Get information about the current session. 16 | """ 17 | 18 | @property 19 | def xrpc_id(self) -> str: 20 | return "com.atproto.server.getSession" 21 | 22 | async def do_xrpc(self, sess: Session) -> GetSessionResp: 23 | resp = await sess.query(self) 24 | return GetSessionResp(**resp) 25 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/get_posts.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.lexicons.app.bsky.feed.defs import PostView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_uri, validate_array 6 | 7 | 8 | class GetPostsResp(BaseModel): 9 | posts: Any 10 | 11 | 12 | class GetPostsReq(BaseModel): 13 | """ 14 | A view of an actor's feed. 15 | """ 16 | 17 | uris: List[str] = Field( 18 | ..., max_items=25, pre=True, validator=validate_array(validate_at_uri) 19 | ) 20 | 21 | @property 22 | def xrpc_id(self) -> str: 23 | return "app.bsky.feed.getPosts" 24 | 25 | async def do_xrpc(self, sess: Session) -> GetPostsResp: 26 | resp = await sess.query(self) 27 | return GetPostsResp(**resp) 28 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/unspecced/get_popular.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.feed.defs import FeedViewPost 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetPopularResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | feed: Any 10 | 11 | 12 | class GetPopularReq(BaseModel): 13 | """ 14 | An unspecced view of globally popular items 15 | """ 16 | 17 | limit: Optional[int] = Field(default=50, ge=1, le=100) 18 | cursor: Optional[str] = Field(default=None) 19 | 20 | @property 21 | def xrpc_id(self) -> str: 22 | return "app.bsky.unspecced.getPopular" 23 | 24 | async def do_xrpc(self, sess: Session) -> GetPopularResp: 25 | resp = await sess.query(self) 26 | return GetPopularResp(**resp) 27 | -------------------------------------------------------------------------------- /lexicons/com/atproto/identity/resolveHandle.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.identity.resolveHandle", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Provides the DID of a repo.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "handle": { 12 | "type": "string", 13 | "format": "handle", 14 | "description": "The handle to resolve. If not supplied, will resolve the host's own handle." 15 | } 16 | } 17 | }, 18 | "output": { 19 | "encoding": "application/json", 20 | "schema": { 21 | "type": "object", 22 | "required": ["did"], 23 | "properties": { 24 | "did": {"type": "string", "format": "did"} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/search_actors_typeahead.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileViewBasic 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class SearchActorsTypeaheadResp(BaseModel): 8 | actors: Any 9 | 10 | 11 | class SearchActorsTypeaheadReq(BaseModel): 12 | """ 13 | Find actor suggestions for a search term. 14 | """ 15 | 16 | term: Optional[str] = Field(default=None) 17 | limit: Optional[int] = Field(default=50, ge=1, le=100) 18 | 19 | @property 20 | def xrpc_id(self) -> str: 21 | return "app.bsky.actor.searchActorsTypeahead" 22 | 23 | async def do_xrpc(self, sess: Session) -> SearchActorsTypeaheadResp: 24 | resp = await sess.query(self) 25 | return SearchActorsTypeaheadResp(**resp) 26 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/reverseModerationAction.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.reverseModerationAction", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Reverse a moderation action.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["id", "reason", "createdBy"], 13 | "properties": { 14 | "id": {"type": "integer"}, 15 | "reason": {"type": "string"}, 16 | "createdBy": {"type": "string", "format": "did"} 17 | } 18 | } 19 | }, 20 | "output": { 21 | "encoding": "application/json", 22 | "schema": { 23 | "type": "ref", 24 | "ref": "com.atproto.admin.defs#actionView" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/createInviteCode.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.createInviteCode", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Create an invite code.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["useCount"], 13 | "properties": { 14 | "useCount": {"type": "integer"}, 15 | "forAccount": {"type": "string", "format": "did"} 16 | } 17 | } 18 | }, 19 | "output": { 20 | "encoding": "application/json", 21 | "schema": { 22 | "type": "object", 23 | "required": ["code"], 24 | "properties": { 25 | "code": { "type": "string" } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/get_account_invite_codes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.com.atproto.server.defs import InviteCode 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetAccountInviteCodesResp(BaseModel): 8 | codes: Any 9 | 10 | 11 | class GetAccountInviteCodesReq(BaseModel): 12 | """ 13 | Get all invite codes for a given account 14 | """ 15 | 16 | includeUsed: Optional[bool] = Field(default=True) 17 | createAvailable: Optional[bool] = Field(default=True) 18 | 19 | @property 20 | def xrpc_id(self) -> str: 21 | return "com.atproto.server.getAccountInviteCodes" 22 | 23 | async def do_xrpc(self, sess: Session) -> GetAccountInviteCodesResp: 24 | resp = await sess.query(self) 25 | return GetAccountInviteCodesResp(**resp) 26 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/refresh_session.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did, validate_handle 5 | 6 | 7 | class RefreshSessionResp(BaseModel): 8 | accessJwt: str = Field(...) 9 | refreshJwt: str = Field(...) 10 | handle: str = Field(..., pre=True, validator=validate_handle) 11 | did: str = Field(..., pre=True, validator=validate_did) 12 | 13 | 14 | class RefreshSessionReq(BaseModel): 15 | """ 16 | Refresh an authentication session. 17 | """ 18 | 19 | @property 20 | def xrpc_id(self) -> str: 21 | return "com.atproto.server.refreshSession" 22 | 23 | async def do_xrpc(self, sess: Session) -> RefreshSessionResp: 24 | resp = await sess.procedure(self) 25 | return RefreshSessionResp(**resp) 26 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_checkout.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did 5 | 6 | 7 | class GetCheckoutReq(BaseModel): 8 | """ 9 | Gets the repo state. 10 | """ 11 | 12 | did: str = Field( 13 | ..., description="The DID of the repo.", pre=True, validator=validate_did 14 | ) 15 | commit: Optional[str] = Field( 16 | default=None, 17 | description="The commit to get the checkout from. Defaults to current HEAD.", 18 | pre=True, 19 | validator=validate_cid, 20 | ) 21 | 22 | @property 23 | def xrpc_id(self) -> str: 24 | return "com.atproto.sync.getCheckout" 25 | 26 | async def do_xrpc(self, sess: Session) -> Any: 27 | return await sess.query(self) 28 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/get_suggestions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetSuggestionsResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | actors: Any 10 | 11 | 12 | class GetSuggestionsReq(BaseModel): 13 | """ 14 | Get a list of actors suggested for following. Used in discovery UIs. 15 | """ 16 | 17 | limit: Optional[int] = Field(default=50, ge=1, le=100) 18 | cursor: Optional[str] = Field(default=None) 19 | 20 | @property 21 | def xrpc_id(self) -> str: 22 | return "app.bsky.actor.getSuggestions" 23 | 24 | async def do_xrpc(self, sess: Session) -> GetSuggestionsResp: 25 | resp = await sess.query(self) 26 | return GetSuggestionsResp(**resp) 27 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/get_timeline.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.feed.defs import FeedViewPost 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetTimelineResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | feed: Any 10 | 11 | 12 | class GetTimelineReq(BaseModel): 13 | """ 14 | A view of the user's home timeline. 15 | """ 16 | 17 | algorithm: Optional[str] = Field(default=None) 18 | limit: Optional[int] = Field(default=50, ge=1, le=100) 19 | cursor: Optional[str] = Field(default=None) 20 | 21 | @property 22 | def xrpc_id(self) -> str: 23 | return "app.bsky.feed.getTimeline" 24 | 25 | async def do_xrpc(self, sess: Session) -> GetTimelineResp: 26 | resp = await sess.query(self) 27 | return GetTimelineResp(**resp) 28 | -------------------------------------------------------------------------------- /psychonaut/cli/util.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import wraps 3 | import sys 4 | 5 | 6 | def as_async(func): 7 | @wraps(func) 8 | def wrapper(*args, **kwargs): 9 | return asyncio.run(func(*args, **kwargs)) 10 | 11 | return wrapper 12 | 13 | 14 | 15 | def print_error_and_fail(msg: str): 16 | print(msg, file=sys.stderr) 17 | sys.exit(1) 18 | 19 | 20 | HANDLE_WARNING = """WARNING: 21 | 22 | You input handle, 23 | {} 24 | which looks like an email address but should probably look like, 25 | @{} 26 | """ 27 | 28 | def clean_handle(handle: str) -> str: 29 | if handle.startswith("@"): 30 | handle = handle[1:] 31 | 32 | idx = handle.find("@") 33 | if idx != -1 and (0 < idx < len(handle) - 1): 34 | suggestion = handle.replace("@", ".") 35 | print(HANDLE_WARNING.format(handle, suggestion), file=sys.stderr) 36 | 37 | 38 | return handle -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/get_profiles.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileViewDetailed 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_identifier, validate_array 6 | 7 | 8 | class GetProfilesResp(BaseModel): 9 | profiles: Any 10 | 11 | 12 | class GetProfilesReq(BaseModel): 13 | """ 14 | [none provided by spec] 15 | """ 16 | 17 | actors: List[str] = Field( 18 | ..., max_items=25, pre=True, validator=validate_array(validate_at_identifier) 19 | ) 20 | 21 | @property 22 | def xrpc_id(self) -> str: 23 | return "app.bsky.actor.getProfiles" 24 | 25 | async def do_xrpc(self, sess: Session) -> GetProfilesResp: 26 | resp = await sess.query(self) 27 | return GetProfilesResp(**resp) 28 | -------------------------------------------------------------------------------- /lexicons/app/bsky/actor/searchActorsTypeahead.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.actor.searchActorsTypeahead", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Find actor suggestions for a search term.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "term": {"type": "string"}, 12 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["actors"], 20 | "properties": { 21 | "actors": { 22 | "type": "array", 23 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic"} 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/getMutes.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.getMutes", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Who does the viewer mute?", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 12 | "cursor": {"type": "string"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["mutes"], 20 | "properties": { 21 | "cursor": {"type": "string"}, 22 | "mutes": { 23 | "type": "array", 24 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/actor/search_actors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class SearchActorsResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | actors: Any 10 | 11 | 12 | class SearchActorsReq(BaseModel): 13 | """ 14 | Find actors matching search criteria. 15 | """ 16 | 17 | term: Optional[str] = Field(default=None) 18 | limit: Optional[int] = Field(default=50, ge=1, le=100) 19 | cursor: Optional[str] = Field(default=None) 20 | 21 | @property 22 | def xrpc_id(self) -> str: 23 | return "app.bsky.actor.searchActors" 24 | 25 | async def do_xrpc(self, sess: Session) -> SearchActorsResp: 26 | resp = await sess.query(self) 27 | return SearchActorsResp(**resp) 28 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/list_app_passwords.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_datetime 4 | from psychonaut.api.session import Session 5 | 6 | 7 | class AppPassword(BaseModel): 8 | """ 9 | [none provided by spec] 10 | """ 11 | 12 | name: str = Field(...) 13 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 14 | 15 | 16 | class ListAppPasswordsResp(BaseModel): 17 | passwords: Any 18 | 19 | 20 | class ListAppPasswordsReq(BaseModel): 21 | """ 22 | List all app-specific passwords. 23 | """ 24 | 25 | @property 26 | def xrpc_id(self) -> str: 27 | return "com.atproto.server.listAppPasswords" 28 | 29 | async def do_xrpc(self, sess: Session) -> ListAppPasswordsResp: 30 | resp = await sess.query(self) 31 | return ListAppPasswordsResp(**resp) 32 | -------------------------------------------------------------------------------- /lexicons/app/bsky/actor/getProfiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.actor.getProfiles", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "required": ["actors"], 10 | "properties": { 11 | "actors": { 12 | "type": "array", 13 | "items": {"type": "string", "format": "at-identifier"}, 14 | "maxLength": 25 15 | } 16 | } 17 | }, 18 | "output": { 19 | "encoding": "application/json", 20 | "schema": { 21 | "type": "object", 22 | "required": ["profiles"], 23 | "properties": { 24 | "profiles": { 25 | "type": "array", 26 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewDetailed"} 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /psychonaut/lexicon/defs.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import json 4 | from pathlib import Path 5 | from psychonaut.lexicon.types import LexiconDoc 6 | 7 | 8 | def generate_defs_from(file_path: Path, output_dir: Path, verbose: bool = False): 9 | # Load the LexiconDoc 10 | with open(file_path, "r") as json_file: 11 | doc = LexiconDoc(**json.load(json_file)) 12 | 13 | defs = doc.defs 14 | if not defs: 15 | return 16 | 17 | print(doc.id) 18 | 19 | for k, defn in doc.defs.items(): 20 | if defn.type != "object": 21 | continue 22 | 23 | props = defn.properties 24 | if not props: 25 | continue 26 | 27 | for k, v in props.items(): 28 | if v.type == "ref": 29 | ref = v.ref 30 | if ref.startswith("#"): 31 | ref = f"{doc.id}{ref}" 32 | print(f"\tFound ref: {k} -> {ref}") 33 | 34 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/describeServer.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.describeServer", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get a document describing the service's accounts configuration.", 8 | "output": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["availableUserDomains"], 13 | "properties": { 14 | "inviteCodeRequired": {"type": "boolean"}, 15 | "availableUserDomains": {"type": "array", "items": {"type": "string"}}, 16 | "links": {"type": "ref", "ref": "#links"} 17 | } 18 | } 19 | } 20 | }, 21 | "links": { 22 | "type": "object", 23 | "properties": { 24 | "privacyPolicy": {"type": "string"}, 25 | "termsOfService": {"type": "string"} 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/get_post_thread.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.feed.defs import ( 3 | ThreadViewPost, 4 | BlockedPost, 5 | NotFoundPost, 6 | ) 7 | from psychonaut.api.session import Session 8 | from pydantic import BaseModel, Field 9 | from psychonaut.lexicon.formats import validate_at_uri 10 | 11 | 12 | class GetPostThreadResp(BaseModel): 13 | thread: Any 14 | 15 | 16 | class GetPostThreadReq(BaseModel): 17 | """ 18 | [none provided by spec] 19 | """ 20 | 21 | uri: str = Field(..., pre=True, validator=validate_at_uri) 22 | depth: Optional[int] = Field(default=None) 23 | 24 | @property 25 | def xrpc_id(self) -> str: 26 | return "app.bsky.feed.getPostThread" 27 | 28 | async def do_xrpc(self, sess: Session) -> GetPostThreadResp: 29 | resp = await sess.query(self) 30 | return GetPostThreadResp(**resp) 31 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/getBlocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.getBlocks", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Who is the requester's account blocking?", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 12 | "cursor": {"type": "string"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["blocks"], 20 | "properties": { 21 | "cursor": {"type": "string"}, 22 | "blocks": { 23 | "type": "array", 24 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/getPosts.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.getPosts", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "A view of an actor's feed.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["uris"], 11 | "properties": { 12 | "uris": { 13 | "type": "array", 14 | "items": {"type": "string", "format": "at-uri"}, 15 | "maxLength": 25 16 | } 17 | } 18 | }, 19 | "output": { 20 | "encoding": "application/json", 21 | "schema": { 22 | "type": "object", 23 | "required": ["posts"], 24 | "properties": { 25 | "posts": { 26 | "type": "array", 27 | "items": {"type": "ref", "ref": "app.bsky.feed.defs#postView"} 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/resolveModerationReports.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.resolveModerationReports", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Resolve moderation reports by an action.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["actionId", "reportIds", "createdBy"], 13 | "properties": { 14 | "actionId": {"type": "integer"}, 15 | "reportIds": {"type": "array", "items": {"type": "integer"}}, 16 | "createdBy": {"type": "string", "format": "did"} 17 | } 18 | } 19 | }, 20 | "output": { 21 | "encoding": "application/json", 22 | "schema": { 23 | "type": "ref", 24 | "ref": "com.atproto.admin.defs#actionView" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /hg2atp/00_intro.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# A Hitchhiker's Guide to ATP\n", 8 | "\n", 9 | "\n", 10 | "(work in progress)" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "1. Hash all the things" 18 | ] 19 | } 20 | ], 21 | "metadata": { 22 | "kernelspec": { 23 | "display_name": "psychonaut-hgjsIY8y-py3.10", 24 | "language": "python", 25 | "name": "python3" 26 | }, 27 | "language_info": { 28 | "codemirror_mode": { 29 | "name": "ipython", 30 | "version": 3 31 | }, 32 | "file_extension": ".py", 33 | "mimetype": "text/x-python", 34 | "name": "python", 35 | "nbconvert_exporter": "python", 36 | "pygments_lexer": "ipython3", 37 | "version": "3.10.6" 38 | }, 39 | "orig_nbformat": 4 40 | }, 41 | "nbformat": 4, 42 | "nbformat_minor": 2 43 | } 44 | -------------------------------------------------------------------------------- /lexicons/app/bsky/unspecced/getPopular.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.unspecced.getPopular", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "An unspecced view of globally popular items", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 12 | "cursor": {"type": "string"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["feed"], 20 | "properties": { 21 | "cursor": {"type": "string"}, 22 | "feed": { 23 | "type": "array", 24 | "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lexicons/app/bsky/actor/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.actor.profile", 4 | "defs": { 5 | "main": { 6 | "type": "record", 7 | "key": "literal:self", 8 | "record": { 9 | "type": "object", 10 | "properties": { 11 | "displayName": { 12 | "type": "string", 13 | "maxGraphemes": 64, 14 | "maxLength": 640 15 | }, 16 | "description": { 17 | "type": "string", 18 | "maxGraphemes": 256, 19 | "maxLength": 2560 20 | }, 21 | "avatar": { 22 | "type": "blob", 23 | "accept": ["image/png", "image/jpeg"], 24 | "maxSize": 1000000 25 | }, 26 | "banner": { 27 | "type": "blob", 28 | "accept": ["image/png", "image/jpeg"], 29 | "maxSize": 1000000 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getModerationActions.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getModerationActions", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "List moderation actions related to a subject.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "subject": {"type": "string"}, 12 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 13 | "cursor": {"type": "string"} 14 | } 15 | }, 16 | "output": { 17 | "encoding": "application/json", 18 | "schema": { 19 | "type": "object", 20 | "required": ["actions"], 21 | "properties": { 22 | "cursor": {"type": "string"}, 23 | "actions": {"type": "array", "items": {"type": "ref", "ref": "com.atproto.admin.defs#actionView"}} 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getRecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getRecord", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Gets blocks needed for existence or non-existence of record.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did", "collection", "rkey"], 11 | "properties": { 12 | "did": { 13 | "type": "string", 14 | "format": "did", 15 | "description": "The DID of the repo." 16 | }, 17 | "collection": {"type": "string", "format": "nsid"}, 18 | "rkey": {"type": "string" }, 19 | "commit": { 20 | "type": "string", 21 | "format": "cid", 22 | "description": "An optional past commit CID." 23 | } 24 | } 25 | }, 26 | "output": { 27 | "encoding": "application/vnd.ipld.car" 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_invite_codes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.com.atproto.server.defs import InviteCode 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetInviteCodesResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | codes: Any 10 | 11 | 12 | class GetInviteCodesReq(BaseModel): 13 | """ 14 | Admin view of invite codes 15 | """ 16 | 17 | sort: Optional[str] = Field(default="recent", known_values=["recent", "usage"]) 18 | limit: Optional[int] = Field(default=100, ge=1, le=500) 19 | cursor: Optional[str] = Field(default=None) 20 | 21 | @property 22 | def xrpc_id(self) -> str: 23 | return "com.atproto.admin.getInviteCodes" 24 | 25 | async def do_xrpc(self, sess: Session) -> GetInviteCodesResp: 26 | resp = await sess.query(self) 27 | return GetInviteCodesResp(**resp) 28 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/richtext/facet.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_uri, validate_did 4 | 5 | 6 | class ByteSlice(BaseModel): 7 | """ 8 | A text segment. Start is inclusive, end is exclusive. Indices are for 9 | utf8-encoded strings. 10 | """ 11 | 12 | byteStart: int = Field(..., ge=0) 13 | byteEnd: int = Field(..., ge=0) 14 | 15 | 16 | class Mention(BaseModel): 17 | """ 18 | A facet feature for actor mentions. 19 | """ 20 | 21 | did: str = Field(..., pre=True, validator=validate_did) 22 | 23 | 24 | class Link(BaseModel): 25 | """ 26 | A facet feature for links. 27 | """ 28 | 29 | uri: str = Field(..., pre=True, validator=validate_uri) 30 | 31 | 32 | class Facet(BaseModel): 33 | """ 34 | [none provided by spec] 35 | """ 36 | 37 | index: ByteSlice 38 | features: List[Any] = Field(...) 39 | -------------------------------------------------------------------------------- /lexicons/app/bsky/actor/getSuggestions.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.actor.getSuggestions", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get a list of actors suggested for following. Used in discovery UIs.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 12 | "cursor": {"type": "string"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["actors"], 20 | "properties": { 21 | "cursor": {"type": "string"}, 22 | "actors": { 23 | "type": "array", 24 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/getTimeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.getTimeline", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "A view of the user's home timeline.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "algorithm": {"type": "string"}, 12 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 13 | "cursor": {"type": "string"} 14 | } 15 | }, 16 | "output": { 17 | "encoding": "application/json", 18 | "schema": { 19 | "type": "object", 20 | "required": ["feed"], 21 | "properties": { 22 | "cursor": {"type": "string"}, 23 | "feed": { 24 | "type": "array", 25 | "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/search_repos.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.com.atproto.admin.defs import RepoView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class SearchReposResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | repos: Any 10 | 11 | 12 | class SearchReposReq(BaseModel): 13 | """ 14 | Find repositories based on a search term. 15 | """ 16 | 17 | term: Optional[str] = Field(default=None) 18 | invitedBy: Optional[str] = Field(default=None) 19 | limit: Optional[int] = Field(default=50, ge=1, le=100) 20 | cursor: Optional[str] = Field(default=None) 21 | 22 | @property 23 | def xrpc_id(self) -> str: 24 | return "com.atproto.admin.searchRepos" 25 | 26 | async def do_xrpc(self, sess: Session) -> SearchReposResp: 27 | resp = await sess.query(self) 28 | return SearchReposResp(**resp) 29 | -------------------------------------------------------------------------------- /lexicons/app/bsky/actor/searchActors.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.actor.searchActors", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Find actors matching search criteria.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "term": {"type": "string"}, 12 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 13 | "cursor": {"type": "string"} 14 | } 15 | }, 16 | "output": { 17 | "encoding": "application/json", 18 | "schema": { 19 | "type": "object", 20 | "required": ["actors"], 21 | "properties": { 22 | "cursor": {"type": "string"}, 23 | "actors": { 24 | "type": "array", 25 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/searchRepos.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.searchRepos", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Find repositories based on a search term.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "term": {"type": "string"}, 12 | "invitedBy": {"type": "string"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["repos"], 22 | "properties": { 23 | "cursor": {"type": "string"}, 24 | "repos": {"type": "array", "items": {"type": "ref", "ref": "com.atproto.admin.defs#repoView"}} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/listAppPasswords.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.listAppPasswords", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "List all app-specific passwords.", 8 | "output": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["passwords"], 13 | "properties": { 14 | "passwords": { 15 | "type": "array", 16 | "items": {"type": "ref", "ref": "#appPassword"} 17 | } 18 | } 19 | } 20 | }, 21 | "errors": [ 22 | {"name": "AccountTakedown"} 23 | ] 24 | }, 25 | "appPassword": { 26 | "type": "object", 27 | "required": ["name", "createdAt"], 28 | "properties": { 29 | "name": {"type": "string"}, 30 | "createdAt": {"type": "string", "format": "datetime"} 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_moderation_actions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.com.atproto.admin.defs import ActionView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetModerationActionsResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | actions: Any 10 | 11 | 12 | class GetModerationActionsReq(BaseModel): 13 | """ 14 | List moderation actions related to a subject. 15 | """ 16 | 17 | subject: Optional[str] = Field(default=None) 18 | limit: Optional[int] = Field(default=50, ge=1, le=100) 19 | cursor: Optional[str] = Field(default=None) 20 | 21 | @property 22 | def xrpc_id(self) -> str: 23 | return "com.atproto.admin.getModerationActions" 24 | 25 | async def do_xrpc(self, sess: Session) -> GetModerationActionsResp: 26 | resp = await sess.query(self) 27 | return GetModerationActionsResp(**resp) 28 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/identity/resolve_handle.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_handle, validate_did 5 | 6 | 7 | class ResolveHandleResp(BaseModel): 8 | did: str = Field(..., pre=True, validator=validate_did) 9 | 10 | 11 | class ResolveHandleReq(BaseModel): 12 | """ 13 | Provides the DID of a repo. 14 | """ 15 | 16 | handle: Optional[str] = Field( 17 | default=None, 18 | description="The handle to resolve. If not supplied, will resolve the host's own handle.", 19 | pre=True, 20 | validator=validate_handle, 21 | ) 22 | 23 | @property 24 | def xrpc_id(self) -> str: 25 | return "com.atproto.identity.resolveHandle" 26 | 27 | async def do_xrpc(self, sess: Session) -> ResolveHandleResp: 28 | resp = await sess.query(self) 29 | return ResolveHandleResp(**resp) 30 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/get_author_feed.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.feed.defs import FeedViewPost 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_identifier 6 | 7 | 8 | class GetAuthorFeedResp(BaseModel): 9 | cursor: Optional[str] = Field(default=None) 10 | feed: Any 11 | 12 | 13 | class GetAuthorFeedReq(BaseModel): 14 | """ 15 | A view of an actor's feed. 16 | """ 17 | 18 | actor: str = Field(..., pre=True, validator=validate_at_identifier) 19 | limit: Optional[int] = Field(default=50, ge=1, le=100) 20 | cursor: Optional[str] = Field(default=None) 21 | 22 | @property 23 | def xrpc_id(self) -> str: 24 | return "app.bsky.feed.getAuthorFeed" 25 | 26 | async def do_xrpc(self, sess: Session) -> GetAuthorFeedResp: 27 | resp = await sess.query(self) 28 | return GetAuthorFeedResp(**resp) 29 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/embed/external.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_uri 4 | 5 | 6 | class External(BaseModel): 7 | """ 8 | [none provided by spec] 9 | """ 10 | 11 | uri: str = Field(..., pre=True, validator=validate_uri) 12 | title: str = Field(...) 13 | description: str = Field(...) 14 | thumb: Optional[Any] = None 15 | 16 | 17 | class ViewExternal(BaseModel): 18 | """ 19 | [none provided by spec] 20 | """ 21 | 22 | uri: str = Field(..., pre=True, validator=validate_uri) 23 | title: str = Field(...) 24 | description: str = Field(...) 25 | thumb: Optional[str] = Field(default=None) 26 | 27 | 28 | class External(BaseModel): 29 | """ 30 | [none provided by spec] 31 | """ 32 | 33 | external: External 34 | 35 | 36 | class View(BaseModel): 37 | """ 38 | [none provided by spec] 39 | """ 40 | 41 | external: ViewExternal 42 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getModerationReports.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getModerationReports", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "List moderation reports related to a subject.", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "subject": {"type": "string"}, 12 | "resolved": {"type": "boolean"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["reports"], 22 | "properties": { 23 | "cursor": {"type": "string"}, 24 | "reports": {"type": "array", "items": {"type": "ref", "ref": "com.atproto.admin.defs#reportView"}} 25 | } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/defs.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.defs", 4 | "defs": { 5 | "inviteCode": { 6 | "type": "object", 7 | "required": ["code", "available", "disabled", "forAccount", "createdBy", "createdAt", "uses"], 8 | "properties": { 9 | "code": {"type": "string"}, 10 | "available": {"type": "integer"}, 11 | "disabled": {"type": "boolean"}, 12 | "forAccount": {"type": "string"}, 13 | "createdBy": {"type": "string"}, 14 | "createdAt": {"type": "string", "format": "datetime"}, 15 | "uses": { 16 | "type": "array", 17 | "items": {"type": "ref", "ref": "#inviteCodeUse"} 18 | } 19 | } 20 | }, 21 | "inviteCodeUse": { 22 | "type": "object", 23 | "required": ["usedBy", "usedAt"], 24 | "properties": { 25 | "usedBy": {"type": "string", "format": "did"}, 26 | "usedAt": {"type": "string", "format": "datetime"} 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getRepo.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getRepo", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Gets the repo state.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did"], 11 | "properties": { 12 | "did": { 13 | "type": "string", 14 | "format": "did", 15 | "description": "The DID of the repo." 16 | }, 17 | "earliest": { 18 | "type": "string", 19 | "format": "cid", 20 | "description": "The earliest commit in the commit range (not inclusive)" 21 | }, 22 | "latest": { 23 | "type": "string", 24 | "format": "cid", 25 | "description": "The latest commit in the commit range (inclusive)" 26 | } 27 | } 28 | }, 29 | "output": { 30 | "encoding": "application/vnd.ipld.car" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/get_follows.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_identifier 6 | 7 | 8 | class GetFollowsResp(BaseModel): 9 | subject: ProfileView 10 | cursor: Optional[str] = Field(default=None) 11 | follows: Any 12 | 13 | 14 | class GetFollowsReq(BaseModel): 15 | """ 16 | Who is an actor following? 17 | """ 18 | 19 | actor: str = Field(..., pre=True, validator=validate_at_identifier) 20 | limit: Optional[int] = Field(default=50, ge=1, le=100) 21 | cursor: Optional[str] = Field(default=None) 22 | 23 | @property 24 | def xrpc_id(self) -> str: 25 | return "app.bsky.graph.getFollows" 26 | 27 | async def do_xrpc(self, sess: Session) -> GetFollowsResp: 28 | resp = await sess.query(self) 29 | return GetFollowsResp(**resp) 30 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did, validate_nsid 5 | 6 | 7 | class GetRecordReq(BaseModel): 8 | """ 9 | Gets blocks needed for existence or non-existence of record. 10 | """ 11 | 12 | did: str = Field( 13 | ..., description="The DID of the repo.", pre=True, validator=validate_did 14 | ) 15 | collection: str = Field(..., pre=True, validator=validate_nsid) 16 | rkey: str = Field(...) 17 | commit: Optional[str] = Field( 18 | default=None, 19 | description="An optional past commit CID.", 20 | pre=True, 21 | validator=validate_cid, 22 | ) 23 | 24 | @property 25 | def xrpc_id(self) -> str: 26 | return "com.atproto.sync.getRecord" 27 | 28 | async def do_xrpc(self, sess: Session) -> Any: 29 | return await sess.query(self) 30 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/graph/get_followers.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_identifier 6 | 7 | 8 | class GetFollowersResp(BaseModel): 9 | subject: ProfileView 10 | cursor: Optional[str] = Field(default=None) 11 | followers: Any 12 | 13 | 14 | class GetFollowersReq(BaseModel): 15 | """ 16 | Who is following an actor? 17 | """ 18 | 19 | actor: str = Field(..., pre=True, validator=validate_at_identifier) 20 | limit: Optional[int] = Field(default=50, ge=1, le=100) 21 | cursor: Optional[str] = Field(default=None) 22 | 23 | @property 24 | def xrpc_id(self) -> str: 25 | return "app.bsky.graph.getFollowers" 26 | 27 | async def do_xrpc(self, sess: Session) -> GetFollowersResp: 28 | resp = await sess.query(self) 29 | return GetFollowersResp(**resp) 30 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/describe_server.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.session import Session 4 | 5 | 6 | class Links(BaseModel): 7 | """ 8 | [none provided by spec] 9 | """ 10 | 11 | privacyPolicy: Optional[str] = Field(default=None) 12 | termsOfService: Optional[str] = Field(default=None) 13 | 14 | 15 | class DescribeServerResp(BaseModel): 16 | inviteCodeRequired: Optional[bool] = Field(default=None) 17 | availableUserDomains: List[str] = Field(...) 18 | links: Optional[Links] = None 19 | 20 | 21 | class DescribeServerReq(BaseModel): 22 | """ 23 | Get a document describing the service's accounts configuration. 24 | """ 25 | 26 | @property 27 | def xrpc_id(self) -> str: 28 | return "com.atproto.server.describeServer" 29 | 30 | async def do_xrpc(self, sess: Session) -> DescribeServerResp: 31 | resp = await sess.query(self) 32 | return DescribeServerResp(**resp) 33 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/getPostThread.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.getPostThread", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "required": ["uri"], 10 | "properties": { 11 | "uri": {"type": "string", "format": "at-uri"}, 12 | "depth": {"type": "integer"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["thread"], 20 | "properties": { 21 | "thread": { 22 | "type": "union", 23 | "refs": [ 24 | "app.bsky.feed.defs#threadViewPost", 25 | "app.bsky.feed.defs#notFoundPost", 26 | "app.bsky.feed.defs#blockedPost" 27 | ] 28 | } 29 | } 30 | } 31 | }, 32 | "errors": [ 33 | {"name": "NotFound"} 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/get_moderation_reports.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.com.atproto.admin.defs import ReportView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class GetModerationReportsResp(BaseModel): 8 | cursor: Optional[str] = Field(default=None) 9 | reports: Any 10 | 11 | 12 | class GetModerationReportsReq(BaseModel): 13 | """ 14 | List moderation reports related to a subject. 15 | """ 16 | 17 | subject: Optional[str] = Field(default=None) 18 | resolved: Optional[bool] = Field(default=None) 19 | limit: Optional[int] = Field(default=50, ge=1, le=100) 20 | cursor: Optional[str] = Field(default=None) 21 | 22 | @property 23 | def xrpc_id(self) -> str: 24 | return "com.atproto.admin.getModerationReports" 25 | 26 | async def do_xrpc(self, sess: Session) -> GetModerationReportsResp: 27 | resp = await sess.query(self) 28 | return GetModerationReportsResp(**resp) 29 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/getAccountInviteCodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.getAccountInviteCodes", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get all invite codes for a given account", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "includeUsed": { "type": "boolean", "default": true }, 12 | "createAvailable": { "type": "boolean", "default": true } 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["codes"], 20 | "properties": { 21 | "codes": { 22 | "type": "array", 23 | "items": { 24 | "type": "ref", 25 | "ref": "com.atproto.server.defs#inviteCode" 26 | } 27 | } 28 | } 29 | } 30 | }, 31 | "errors": [ 32 | {"name": "DuplicateCreate"} 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /psychonaut/lexicon/tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Iterable 4 | import jmespath 5 | 6 | 7 | def collect_all_values(lexicon_dir: str, query: str, transform=lambda d: d) -> Iterable[str]: 8 | """ 9 | Iterate over every .json file in the lexicon directory and collect all values 10 | """ 11 | for lexicon_file in Path(lexicon_dir).glob("**/*.json"): 12 | with open(lexicon_file, "r") as f: 13 | lexicon = json.load(f) 14 | res = jmespath.search(query, lexicon) 15 | if res: 16 | res = transform(res) 17 | for value in res: 18 | yield value 19 | 20 | 21 | 22 | def find_doc_match(lexicon_dir: str, pred_f=lambda d: bool): 23 | for lexicon_file in Path(lexicon_dir).glob("**/*.json"): 24 | with open(lexicon_file, "r") as f: 25 | src = f.read() 26 | lexicon = json.loads(src) 27 | 28 | if pred_f(lexicon): 29 | print(lexicon_file) 30 | print(src) 31 | print() -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/listBlobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.listBlobs", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "List blob cids for some range of commits", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did"], 11 | "properties": { 12 | "did": {"type": "string", "format": "did", "description": "The DID of the repo."}, 13 | "latest": { "type": "string", "format": "cid", "description": "The most recent commit"}, 14 | "earliest": { "type": "string", "format": "cid", "description": "The earliest commit to start from"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["cids"], 22 | "properties": { 23 | "cids": { 24 | "type": "array", 25 | "items": { "type": "string", "format": "cid" } 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/list_repos.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_cid, validate_did 4 | from psychonaut.api.session import Session 5 | 6 | 7 | class Repo(BaseModel): 8 | """ 9 | [none provided by spec] 10 | """ 11 | 12 | did: str = Field(..., pre=True, validator=validate_did) 13 | head: str = Field(..., pre=True, validator=validate_cid) 14 | 15 | 16 | class ListReposResp(BaseModel): 17 | cursor: Optional[str] = Field(default=None) 18 | repos: Any 19 | 20 | 21 | class ListReposReq(BaseModel): 22 | """ 23 | List dids and root cids of hosted repos 24 | """ 25 | 26 | limit: Optional[int] = Field(default=500, ge=1, le=1000) 27 | cursor: Optional[str] = Field(default=None) 28 | 29 | @property 30 | def xrpc_id(self) -> str: 31 | return "com.atproto.sync.listRepos" 32 | 33 | async def do_xrpc(self, sess: Session) -> ListReposResp: 34 | resp = await sess.query(self) 35 | return ListReposResp(**resp) 36 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_repo.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did 5 | 6 | 7 | class GetRepoReq(BaseModel): 8 | """ 9 | Gets the repo state. 10 | """ 11 | 12 | did: str = Field( 13 | ..., description="The DID of the repo.", pre=True, validator=validate_did 14 | ) 15 | earliest: Optional[str] = Field( 16 | default=None, 17 | description="The earliest commit in the commit range (not inclusive)", 18 | pre=True, 19 | validator=validate_cid, 20 | ) 21 | latest: Optional[str] = Field( 22 | default=None, 23 | description="The latest commit in the commit range (inclusive)", 24 | pre=True, 25 | validator=validate_cid, 26 | ) 27 | 28 | @property 29 | def xrpc_id(self) -> str: 30 | return "com.atproto.sync.getRepo" 31 | 32 | async def do_xrpc(self, sess: Session) -> Any: 33 | return await sess.query(self) 34 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/createAppPassword.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.createAppPassword", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Create an app-specific password.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["name"], 13 | "properties": { 14 | "name": {"type": "string"} 15 | } 16 | } 17 | }, 18 | "output": { 19 | "encoding": "application/json", 20 | "schema": { 21 | "type": "ref", 22 | "ref": "#appPassword" 23 | } 24 | }, 25 | "errors": [ 26 | {"name": "AccountTakedown"} 27 | ] 28 | }, 29 | "appPassword": { 30 | "type": "object", 31 | "required": ["name", "password", "createdAt"], 32 | "properties": { 33 | "name": {"type": "string"}, 34 | "password": {"type": "string"}, 35 | "createdAt": {"type": "string", "format": "datetime"} 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/getFollows.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.getFollows", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Who is an actor following?", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["actor"], 11 | "properties": { 12 | "actor": {"type": "string", "format": "at-identifier"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["subject", "follows"], 22 | "properties": { 23 | "subject": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"}, 24 | "cursor": {"type": "string"}, 25 | "follows": { 26 | "type": "array", 27 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/create_invite_codes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.session import Session 4 | from psychonaut.lexicon.formats import validate_did, validate_array 5 | 6 | 7 | class AccountCodes(BaseModel): 8 | """ 9 | [none provided by spec] 10 | """ 11 | 12 | account: str = Field(...) 13 | codes: List[str] = Field(...) 14 | 15 | 16 | class CreateInviteCodesResp(BaseModel): 17 | codes: Any 18 | 19 | 20 | class CreateInviteCodesReq(BaseModel): 21 | """ 22 | Create an invite code. 23 | """ 24 | 25 | codeCount: int = Field(default=1) 26 | useCount: int = Field(...) 27 | forAccounts: Optional[List[str]] = Field( 28 | default=None, pre=True, validator=validate_array(validate_did) 29 | ) 30 | 31 | @property 32 | def xrpc_id(self) -> str: 33 | return "com.atproto.server.createInviteCodes" 34 | 35 | async def do_xrpc(self, sess: Session) -> CreateInviteCodesResp: 36 | resp = await sess.procedure(self) 37 | return CreateInviteCodesResp(**resp) 38 | -------------------------------------------------------------------------------- /lexicons/app/bsky/graph/getFollowers.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.graph.getFollowers", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Who is following an actor?", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["actor"], 11 | "properties": { 12 | "actor": {"type": "string", "format": "at-identifier"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["subject", "followers"], 22 | "properties": { 23 | "subject": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"}, 24 | "cursor": {"type": "string"}, 25 | "followers": { 26 | "type": "array", 27 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/create_account.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did, validate_handle 5 | 6 | 7 | class CreateAccountResp(BaseModel): 8 | accessJwt: str = Field(...) 9 | refreshJwt: str = Field(...) 10 | handle: str = Field(..., pre=True, validator=validate_handle) 11 | did: str = Field(..., pre=True, validator=validate_did) 12 | 13 | 14 | class CreateAccountReq(BaseModel): 15 | """ 16 | Create an account. 17 | """ 18 | 19 | email: str = Field(...) 20 | handle: str = Field(..., pre=True, validator=validate_handle) 21 | inviteCode: Optional[str] = Field(default=None) 22 | password: str = Field(...) 23 | recoveryKey: Optional[str] = Field(default=None) 24 | 25 | @property 26 | def xrpc_id(self) -> str: 27 | return "com.atproto.server.createAccount" 28 | 29 | async def do_xrpc(self, sess: Session) -> CreateAccountResp: 30 | resp = await sess.procedure(self) 31 | return CreateAccountResp(**resp) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2003 Abreka, Inc 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/getAuthorFeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.getAuthorFeed", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "A view of an actor's feed.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["actor"], 11 | "properties": { 12 | "actor": {"type": "string", "format": "at-identifier"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["feed"], 22 | "properties": { 23 | "cursor": {"type": "string"}, 24 | "feed": { 25 | "type": "array", 26 | "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} 27 | } 28 | } 29 | } 30 | }, 31 | "errors": [ 32 | {"name": "BlockedActor"}, 33 | {"name": "BlockedByActor"} 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/server/create_session.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_did, validate_handle 5 | 6 | 7 | class CreateSessionResp(BaseModel): 8 | accessJwt: str = Field(...) 9 | refreshJwt: str = Field(...) 10 | handle: str = Field(..., pre=True, validator=validate_handle) 11 | did: str = Field(..., pre=True, validator=validate_did) 12 | email: Optional[str] = Field(default=None) 13 | 14 | 15 | class CreateSessionReq(BaseModel): 16 | """ 17 | Create an authentication session. 18 | """ 19 | 20 | identifier: str = Field( 21 | ..., 22 | description="Handle or other identifier supported by the server for the authenticating user.", 23 | ) 24 | password: str = Field(...) 25 | 26 | @property 27 | def xrpc_id(self) -> str: 28 | return "com.atproto.server.createSession" 29 | 30 | async def do_xrpc(self, sess: Session) -> CreateSessionResp: 31 | resp = await sess.procedure(self) 32 | return CreateSessionResp(**resp) 33 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/getRepostedBy.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.getRepostedBy", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "required": ["uri"], 10 | "properties": { 11 | "uri": {"type": "string", "format": "at-uri"}, 12 | "cid": {"type": "string", "format": "cid"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["uri", "repostedBy"], 22 | "properties": { 23 | "uri": {"type": "string", "format": "at-uri"}, 24 | "cid": {"type": "string", "format": "cid"}, 25 | "cursor": {"type": "string"}, 26 | "repostedBy": { 27 | "type": "array", 28 | "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/getInviteCodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.getInviteCodes", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Admin view of invite codes", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "sort": { 12 | "type": "string", 13 | "knownValues": [ 14 | "recent", 15 | "usage" 16 | ], 17 | "default": "recent" 18 | }, 19 | "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 100}, 20 | "cursor": {"type": "string"} 21 | } 22 | }, 23 | "output": { 24 | "encoding": "application/json", 25 | "schema": { 26 | "type": "object", 27 | "required": ["codes"], 28 | "properties": { 29 | "cursor": {"type": "string"}, 30 | "codes": { 31 | "type": "array", 32 | "items": {"type": "ref", "ref": "com.atproto.server.defs#inviteCode"} 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/listRepos.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.listRepos", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "List dids and root cids of hosted repos", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 500}, 12 | "cursor": {"type": "string"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["repos"], 20 | "properties": { 21 | "cursor": {"type": "string"}, 22 | "repos": { 23 | "type": "array", 24 | "items": {"type": "ref", "ref": "#repo"} 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "repo": { 31 | "type": "object", 32 | "required": ["did", "head"], 33 | "properties": { 34 | "did": { "type": "string", "format": "did" }, 35 | "head": { "type": "string", "format": "cid" } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /lexicons/app/bsky/embed/recordWithMedia.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.embed.recordWithMedia", 4 | "description": "A representation of a record embedded in another form of content, alongside other compatible embeds", 5 | "defs": { 6 | "main": { 7 | "type": "object", 8 | "required": ["record", "media"], 9 | "properties": { 10 | "record": { 11 | "type": "ref", 12 | "ref": "app.bsky.embed.record" 13 | }, 14 | "media": { 15 | "type": "union", 16 | "refs": [ 17 | "app.bsky.embed.images", 18 | "app.bsky.embed.external" 19 | ] 20 | } 21 | } 22 | }, 23 | "view": { 24 | "type": "object", 25 | "required": ["record", "media"], 26 | "properties": { 27 | "record": { 28 | "type": "ref", 29 | "ref": "app.bsky.embed.record#view" 30 | }, 31 | "media": { 32 | "type": "union", 33 | "refs": [ 34 | "app.bsky.embed.images#view", 35 | "app.bsky.embed.external#view" 36 | ] 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/describeRepo.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.describeRepo", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get information about the repo, including the list of collections.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["repo"], 11 | "properties": { 12 | "repo": { 13 | "type": "string", 14 | "format": "at-identifier", 15 | "description": "The handle or DID of the repo." 16 | } 17 | } 18 | }, 19 | "output": { 20 | "encoding": "application/json", 21 | "schema": { 22 | "type": "object", 23 | "required": ["handle", "did", "didDoc", "collections", "handleIsCorrect"], 24 | "properties": { 25 | "handle": {"type": "string", "format": "handle"}, 26 | "did": {"type": "string", "format": "did"}, 27 | "didDoc": {"type": "unknown"}, 28 | "collections": {"type": "array", "items": {"type": "string", "format": "nsid"}}, 29 | "handleIsCorrect": {"type": "boolean"} 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/moderation/create_report.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.com.atproto.moderation.defs import ReasonType 3 | from psychonaut.api.lexicons.com.atproto.admin.defs import RepoRef 4 | from psychonaut.api.session import Session 5 | from pydantic import BaseModel, Field 6 | from psychonaut.lexicon.formats import validate_did, validate_datetime 7 | 8 | 9 | class CreateReportResp(BaseModel): 10 | id: int = Field(...) 11 | reasonType: ReasonType 12 | reason: Optional[str] = Field(default=None) 13 | subject: Any 14 | reportedBy: str = Field(..., pre=True, validator=validate_did) 15 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 16 | 17 | 18 | class CreateReportReq(BaseModel): 19 | """ 20 | Report a repo or a record. 21 | """ 22 | 23 | reasonType: ReasonType 24 | reason: Optional[str] = Field(default=None) 25 | subject: Any 26 | 27 | @property 28 | def xrpc_id(self) -> str: 29 | return "com.atproto.moderation.createReport" 30 | 31 | async def do_xrpc(self, sess: Session) -> CreateReportResp: 32 | resp = await sess.procedure(self) 33 | return CreateReportResp(**resp) 34 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/get_reposted_by.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_uri, validate_cid 6 | 7 | 8 | class GetRepostedByResp(BaseModel): 9 | uri: str = Field(..., pre=True, validator=validate_at_uri) 10 | cid: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 11 | cursor: Optional[str] = Field(default=None) 12 | repostedBy: Any 13 | 14 | 15 | class GetRepostedByReq(BaseModel): 16 | """ 17 | [none provided by spec] 18 | """ 19 | 20 | uri: str = Field(..., pre=True, validator=validate_at_uri) 21 | cid: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 22 | limit: Optional[int] = Field(default=50, ge=1, le=100) 23 | cursor: Optional[str] = Field(default=None) 24 | 25 | @property 26 | def xrpc_id(self) -> str: 27 | return "app.bsky.feed.getRepostedBy" 28 | 29 | async def do_xrpc(self, sess: Session) -> GetRepostedByResp: 30 | resp = await sess.query(self) 31 | return GetRepostedByResp(**resp) 32 | -------------------------------------------------------------------------------- /lexicons/com/atproto/sync/getCommitPath.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.sync.getCommitPath", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Gets the path of repo commits", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["did"], 11 | "properties": { 12 | "did": { 13 | "type": "string", 14 | "format": "did", 15 | "description": "The DID of the repo." 16 | }, 17 | "latest": { 18 | "type": "string", 19 | "format": "cid", 20 | "description": "The most recent commit" 21 | }, 22 | "earliest": { 23 | "type": "string", 24 | "format": "cid", 25 | "description": "The earliest commit to start from" 26 | } 27 | } 28 | }, 29 | "output": { 30 | "encoding": "application/json", 31 | "schema": { 32 | "type": "object", 33 | "required": ["commits"], 34 | "properties": { 35 | "commits": { 36 | "type": "array", 37 | "items": { "type": "string", "format": "cid" } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/list_blobs.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did, validate_array 5 | 6 | 7 | class ListBlobsResp(BaseModel): 8 | cids: List[str] = Field(..., pre=True, validator=validate_array(validate_cid)) 9 | 10 | 11 | class ListBlobsReq(BaseModel): 12 | """ 13 | List blob cids for some range of commits 14 | """ 15 | 16 | did: str = Field( 17 | ..., description="The DID of the repo.", pre=True, validator=validate_did 18 | ) 19 | latest: Optional[str] = Field( 20 | default=None, 21 | description="The most recent commit", 22 | pre=True, 23 | validator=validate_cid, 24 | ) 25 | earliest: Optional[str] = Field( 26 | default=None, 27 | description="The earliest commit to start from", 28 | pre=True, 29 | validator=validate_cid, 30 | ) 31 | 32 | @property 33 | def xrpc_id(self) -> str: 34 | return "com.atproto.sync.listBlobs" 35 | 36 | async def do_xrpc(self, sess: Session) -> ListBlobsResp: 37 | resp = await sess.query(self) 38 | return ListBlobsResp(**resp) 39 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/get_commit_path.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_cid, validate_did, validate_array 5 | 6 | 7 | class GetCommitPathResp(BaseModel): 8 | commits: List[str] = Field(..., pre=True, validator=validate_array(validate_cid)) 9 | 10 | 11 | class GetCommitPathReq(BaseModel): 12 | """ 13 | Gets the path of repo commits 14 | """ 15 | 16 | did: str = Field( 17 | ..., description="The DID of the repo.", pre=True, validator=validate_did 18 | ) 19 | latest: Optional[str] = Field( 20 | default=None, 21 | description="The most recent commit", 22 | pre=True, 23 | validator=validate_cid, 24 | ) 25 | earliest: Optional[str] = Field( 26 | default=None, 27 | description="The earliest commit to start from", 28 | pre=True, 29 | validator=validate_cid, 30 | ) 31 | 32 | @property 33 | def xrpc_id(self) -> str: 34 | return "com.atproto.sync.getCommitPath" 35 | 36 | async def do_xrpc(self, sess: Session) -> GetCommitPathResp: 37 | resp = await sess.query(self) 38 | return GetCommitPathResp(**resp) 39 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/describe_repo.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import ( 5 | validate_at_identifier, 6 | validate_handle, 7 | validate_array, 8 | validate_did, 9 | validate_nsid, 10 | ) 11 | 12 | 13 | class DescribeRepoResp(BaseModel): 14 | handle: str = Field(..., pre=True, validator=validate_handle) 15 | did: str = Field(..., pre=True, validator=validate_did) 16 | didDoc: Any 17 | collections: List[str] = Field( 18 | ..., pre=True, validator=validate_array(validate_nsid) 19 | ) 20 | handleIsCorrect: bool = Field(...) 21 | 22 | 23 | class DescribeRepoReq(BaseModel): 24 | """ 25 | Get information about the repo, including the list of collections. 26 | """ 27 | 28 | repo: str = Field( 29 | ..., 30 | description="The handle or DID of the repo.", 31 | pre=True, 32 | validator=validate_at_identifier, 33 | ) 34 | 35 | @property 36 | def xrpc_id(self) -> str: 37 | return "com.atproto.repo.describeRepo" 38 | 39 | async def do_xrpc(self, sess: Session) -> DescribeRepoResp: 40 | resp = await sess.query(self) 41 | return DescribeRepoResp(**resp) 42 | -------------------------------------------------------------------------------- /psychonaut/lexicon/ctx.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from pydantic import BaseModel, Field 3 | from collections import OrderedDict, defaultdict 4 | from typing import Dict, Optional, Set, Any 5 | from psychonaut.lexicon.planner import LexiconDef 6 | 7 | 8 | class GenCtx(BaseModel): 9 | imports: Dict[str, Set[str]] = Field(default_factory=lambda: defaultdict(set)) 10 | fragments: OrderedDict[str, LexiconDef] = Field(default_factory=OrderedDict) 11 | verbose: bool = Field(default=False, repr=False) 12 | input_path: Optional[Path] = Field(default=None, repr=False) 13 | global_vars: Dict[str, Any] = Field(default_factory=dict, repr=False) 14 | 15 | def add_fragment(self, name: str, node: LexiconDef): 16 | assert name not in self.fragments, f"Fragment {name} already exists" 17 | self.fragments[name] = node 18 | 19 | @property 20 | def common_xrpc_id(self) -> Optional[str]: 21 | if not self.fragments: 22 | return None 23 | 24 | xrpc_ids = { 25 | v.node_id.split("#")[0] 26 | for v in self.fragments.values() 27 | } 28 | 29 | if len(xrpc_ids) == 1: 30 | return xrpc_ids.pop() 31 | 32 | raise ValueError(f"Multiple xrpc ids found: {xrpc_ids}") 33 | 34 | class Config: 35 | arbitrary_types_allowed = True -------------------------------------------------------------------------------- /lexicons/app/bsky/richtext/facet.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.richtext.facet", 4 | "defs": { 5 | "main": { 6 | "type": "object", 7 | "required": ["index", "features"], 8 | "properties": { 9 | "index": {"type": "ref", "ref": "#byteSlice"}, 10 | "features": { 11 | "type": "array", 12 | "items": {"type": "union", "refs": ["#mention", "#link"]} 13 | } 14 | } 15 | }, 16 | "mention": { 17 | "type": "object", 18 | "description": "A facet feature for actor mentions.", 19 | "required": ["did"], 20 | "properties": { 21 | "did": {"type": "string", "format": "did"} 22 | } 23 | }, 24 | "link": { 25 | "type": "object", 26 | "description": "A facet feature for links.", 27 | "required": ["uri"], 28 | "properties": { 29 | "uri": {"type": "string", "format": "uri"} 30 | } 31 | }, 32 | "byteSlice": { 33 | "type": "object", 34 | "description": "A text segment. Start is inclusive, end is exclusive. Indices are for utf8-encoded strings.", 35 | "required": ["byteStart", "byteEnd"], 36 | "properties": { 37 | "byteStart": {"type": "integer", "minimum": 0}, 38 | "byteEnd": {"type": "integer", "minimum": 0} 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/admin/take_moderation_action.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.lexicons.com.atproto.admin.defs import RepoRef 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_cid, validate_did, validate_array 6 | 7 | 8 | class TakeModerationActionReq(BaseModel): 9 | """ 10 | Take a moderation action on a repo. 11 | """ 12 | 13 | action: str = Field( 14 | ..., 15 | known_values=[ 16 | "com.atproto.admin.defs#takedown", 17 | "com.atproto.admin.defs#flag", 18 | "com.atproto.admin.defs#acknowledge", 19 | ], 20 | ) 21 | subject: Any 22 | subjectBlobCids: Optional[List[str]] = Field( 23 | default=None, pre=True, validator=validate_array(validate_cid) 24 | ) 25 | createLabelVals: Optional[List[str]] = Field(default=None) 26 | negateLabelVals: Optional[List[str]] = Field(default=None) 27 | reason: str = Field(...) 28 | createdBy: str = Field(..., pre=True, validator=validate_did) 29 | 30 | @property 31 | def xrpc_id(self) -> str: 32 | return "com.atproto.admin.takeModerationAction" 33 | 34 | async def do_xrpc(self, sess: Session) -> Any: 35 | return await sess.procedure(self) 36 | -------------------------------------------------------------------------------- /lexicons/app/bsky/embed/images.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.embed.images", 4 | "description": "A set of images embedded in some other form of content", 5 | "defs": { 6 | "main": { 7 | "type": "object", 8 | "required": ["images"], 9 | "properties": { 10 | "images": { 11 | "type": "array", 12 | "items": {"type": "ref", "ref": "#image"}, 13 | "maxLength": 4 14 | } 15 | } 16 | }, 17 | "image": { 18 | "type": "object", 19 | "required": ["image", "alt"], 20 | "properties": { 21 | "image": { 22 | "type": "blob", 23 | "accept": ["image/*"], 24 | "maxSize": 1000000 25 | }, 26 | "alt": {"type": "string"} 27 | } 28 | }, 29 | "view": { 30 | "type": "object", 31 | "required": ["images"], 32 | "properties": { 33 | "images": { 34 | "type": "array", 35 | "items": {"type": "ref", "ref": "#viewImage"}, 36 | "maxLength": 4 37 | } 38 | } 39 | }, 40 | "viewImage": { 41 | "type": "object", 42 | "required": ["thumb", "fullsize", "alt"], 43 | "properties": { 44 | "thumb": {"type": "string"}, 45 | "fullsize": {"type": "string"}, 46 | "alt": {"type": "string"} 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/createSession.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.createSession", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Create an authentication session.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["identifier", "password"], 13 | "properties": { 14 | "identifier": { 15 | "type": "string", 16 | "description": "Handle or other identifier supported by the server for the authenticating user." 17 | }, 18 | "password": {"type": "string"} 19 | } 20 | } 21 | }, 22 | "output": { 23 | "encoding": "application/json", 24 | "schema": { 25 | "type": "object", 26 | "required": ["accessJwt", "refreshJwt", "handle", "did"], 27 | "properties": { 28 | "accessJwt": {"type": "string"}, 29 | "refreshJwt": {"type": "string"}, 30 | "handle": {"type": "string", "format": "handle"}, 31 | "did": {"type": "string", "format": "did"}, 32 | "email": {"type": "string"} 33 | } 34 | } 35 | }, 36 | "errors": [ 37 | {"name": "AccountTakedown"} 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/embed/record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_at_uri, validate_cid, validate_datetime 4 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileViewBasic 5 | from psychonaut.api.lexicons.com.atproto.repo.strong_ref import StrongRef 6 | 7 | 8 | class ViewBlocked(BaseModel): 9 | """ 10 | [none provided by spec] 11 | """ 12 | 13 | uri: str = Field(..., pre=True, validator=validate_at_uri) 14 | 15 | 16 | class ViewNotFound(BaseModel): 17 | """ 18 | [none provided by spec] 19 | """ 20 | 21 | uri: str = Field(..., pre=True, validator=validate_at_uri) 22 | 23 | 24 | class ViewRecord(BaseModel): 25 | """ 26 | [none provided by spec] 27 | """ 28 | 29 | uri: str = Field(..., pre=True, validator=validate_at_uri) 30 | cid: str = Field(..., pre=True, validator=validate_cid) 31 | author: ProfileViewBasic 32 | value: Any 33 | labels: Optional[Any] = None 34 | embeds: Optional[List[Any]] = Field(default=None) 35 | indexedAt: str = Field(..., pre=True, validator=validate_datetime) 36 | 37 | 38 | class Record(BaseModel): 39 | """ 40 | [none provided by spec] 41 | """ 42 | 43 | record: StrongRef 44 | 45 | 46 | class View(BaseModel): 47 | """ 48 | [none provided by spec] 49 | """ 50 | 51 | record: Any 52 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/label/query_labels.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from psychonaut.api.lexicons.com.atproto.label.defs import Label 3 | from psychonaut.api.session import Session 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_did, validate_array 6 | 7 | 8 | class QueryLabelsResp(BaseModel): 9 | cursor: Optional[str] = Field(default=None) 10 | labels: Any 11 | 12 | 13 | class QueryLabelsReq(BaseModel): 14 | """ 15 | Find labels relevant to the provided URI patterns. 16 | """ 17 | 18 | uriPatterns: List[str] = Field( 19 | ..., 20 | description="List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI", 21 | ) 22 | sources: Optional[List[str]] = Field( 23 | default=None, 24 | description="Optional list of label sources (DIDs) to filter on", 25 | pre=True, 26 | validator=validate_array(validate_did), 27 | ) 28 | limit: Optional[int] = Field(default=50, ge=1, le=250) 29 | cursor: Optional[str] = Field(default=None) 30 | 31 | @property 32 | def xrpc_id(self) -> str: 33 | return "com.atproto.label.queryLabels" 34 | 35 | async def do_xrpc(self, sess: Session) -> QueryLabelsResp: 36 | resp = await sess.query(self) 37 | return QueryLabelsResp(**resp) 38 | -------------------------------------------------------------------------------- /lexicons/com/atproto/label/subscribeLabels.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.label.subscribeLabels", 4 | "defs": { 5 | "main": { 6 | "type": "subscription", 7 | "description": "Subscribe to label updates", 8 | "parameters": { 9 | "type": "params", 10 | "properties": { 11 | "cursor": { 12 | "type": "integer", 13 | "description": "The last known event to backfill from." 14 | } 15 | } 16 | }, 17 | "message": { 18 | "schema": { 19 | "type": "union", 20 | "refs": [ 21 | "#labels", 22 | "#info" 23 | ] 24 | } 25 | }, 26 | "errors": [ 27 | {"name": "FutureCursor"} 28 | ] 29 | }, 30 | "labels": { 31 | "type": "object", 32 | "required": ["seq", "labels"], 33 | "properties": { 34 | "seq": {"type": "integer"}, 35 | "labels": { 36 | "type": "array", 37 | "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 38 | } 39 | } 40 | }, 41 | "info": { 42 | "type": "object", 43 | "required": ["name"], 44 | "properties": { 45 | "name": { 46 | "type": "string", 47 | "knownValues": ["OutdatedCursor"] 48 | }, 49 | "message": { 50 | "type": "string" 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/post.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from pydantic import BaseModel, Field 3 | from psychonaut.api.lexicons.com.atproto.repo.strong_ref import StrongRef 4 | from psychonaut.lexicon.formats import validate_datetime 5 | 6 | 7 | class TextSlice(BaseModel): 8 | """ 9 | Deprecated. Use app.bsky.richtext instead -- A text segment. Start is 10 | inclusive, end is exclusive. Indices are for utf16-encoded strings. 11 | """ 12 | 13 | start: int = Field(..., ge=0) 14 | end: int = Field(..., ge=0) 15 | 16 | 17 | class ReplyRef(BaseModel): 18 | """ 19 | [none provided by spec] 20 | """ 21 | 22 | root: StrongRef 23 | parent: StrongRef 24 | 25 | 26 | class Entity(BaseModel): 27 | """ 28 | Deprecated: use facets instead. 29 | """ 30 | 31 | index: TextSlice 32 | type: str = Field(..., description="Expected values are 'mention' and 'link'.") 33 | value: str = Field(...) 34 | 35 | 36 | class Post(BaseModel): 37 | """ 38 | [none provided by spec] 39 | """ 40 | 41 | text: str = Field(..., max_length=3000) 42 | entities: Optional[Any] = None 43 | facets: Optional[Any] = None 44 | reply: Optional[ReplyRef] = None 45 | embed: Optional[Any] = None 46 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 47 | 48 | @property 49 | def xrpc_id(self) -> str: 50 | return "app.bsky.feed.post" 51 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/createInviteCodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.createInviteCodes", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Create an invite code.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["codeCount", "useCount"], 13 | "properties": { 14 | "codeCount": {"type": "integer", "default": 1}, 15 | "useCount": {"type": "integer"}, 16 | "forAccounts": { 17 | "type": "array", 18 | "items": {"type": "string", "format": "did"} 19 | } 20 | } 21 | } 22 | }, 23 | "output": { 24 | "encoding": "application/json", 25 | "schema": { 26 | "type": "object", 27 | "required": ["codes"], 28 | "properties": { 29 | "codes": { 30 | "type": "array", 31 | "items": {"type": "ref", "ref": "#accountCodes"} 32 | } 33 | } 34 | } 35 | } 36 | }, 37 | "accountCodes": { 38 | "type": "object", 39 | "required": ["account", "codes"], 40 | "properties": { 41 | "account": {"type": "string"}, 42 | "codes": { 43 | "type": "array", 44 | "items": {"type": "string"} 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /psychonaut/cli/graph.py: -------------------------------------------------------------------------------- 1 | import json 2 | import click 3 | from psychonaut.api.lexicons.app.bsky.graph.get_followers import ( 4 | GetFollowersReq, GetFollowersResp 5 | ) 6 | from psychonaut.api.lexicons.app.bsky.graph.get_follows import ( 7 | GetFollowsReq, GetFollowsResp, 8 | ) 9 | from psychonaut.cli.group import cli 10 | from psychonaut.cli.util import as_async, clean_handle 11 | from psychonaut.client import get_simple_client_session 12 | from psychonaut.client.cursors import collect_cursored 13 | 14 | 15 | @cli.command() 16 | @click.argument("actor") 17 | @as_async 18 | async def get_followers(actor: str): 19 | actor = clean_handle(actor) 20 | 21 | async with get_simple_client_session() as sess: 22 | gen = collect_cursored( 23 | sess, 24 | GetFollowersReq(actor=actor, limit=100), 25 | GetFollowersResp, 26 | "followers" 27 | ) 28 | 29 | async for follower in gen: 30 | print(json.dumps(follower)) 31 | 32 | 33 | @cli.command() 34 | @click.argument("actor") 35 | @as_async 36 | async def get_follows(actor: str): 37 | actor = clean_handle(actor) 38 | 39 | async with get_simple_client_session() as sess: 40 | gen = collect_cursored( 41 | sess, 42 | GetFollowsReq(actor=actor, limit=100), 43 | GetFollowsResp, 44 | "follows" 45 | ) 46 | 47 | async for follows in gen: 48 | print(json.dumps(follows)) -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/getRecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.getRecord", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Get a record.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["repo", "collection", "rkey"], 11 | "properties": { 12 | "repo": { 13 | "type": "string", 14 | "format": "at-identifier", 15 | "description": "The handle or DID of the repo." 16 | }, 17 | "collection": { 18 | "type": "string", 19 | "format": "nsid", 20 | "description": "The NSID of the record collection." 21 | }, 22 | "rkey": {"type": "string", "description": "The key of the record."}, 23 | "cid": { 24 | "type": "string", 25 | "format": "cid", 26 | "description": "The CID of the version of the record. If not specified, then return the most recent version." 27 | } 28 | } 29 | }, 30 | "output": { 31 | "encoding": "application/json", 32 | "schema": { 33 | "type": "object", 34 | "required": ["uri", "value"], 35 | "properties": { 36 | "uri": {"type": "string", "format": "at-uri"}, 37 | "cid": {"type": "string", "format": "cid"}, 38 | "value": {"type": "unknown"} 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lexicons/app/bsky/feed/getLikes.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.feed.getLikes", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "required": ["uri"], 10 | "properties": { 11 | "uri": {"type": "string", "format": "at-uri"}, 12 | "cid": {"type": "string", "format": "cid"}, 13 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 14 | "cursor": {"type": "string"} 15 | } 16 | }, 17 | "output": { 18 | "encoding": "application/json", 19 | "schema": { 20 | "type": "object", 21 | "required": ["uri", "likes"], 22 | "properties": { 23 | "uri": {"type": "string", "format": "at-uri"}, 24 | "cid": {"type": "string", "format": "cid"}, 25 | "cursor": {"type": "string"}, 26 | "likes": { 27 | "type": "array", 28 | "items": {"type": "ref", "ref": "#like"} 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "like": { 35 | "type": "object", 36 | "required": ["indexedAt", "createdAt", "actor"], 37 | "properties": { 38 | "indexedAt": {"type": "string", "format": "datetime"}, 39 | "createdAt": {"type": "string", "format": "datetime"}, 40 | "actor": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lexicons/com/atproto/label/defs.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.label.defs", 4 | "defs": { 5 | "label": { 6 | "type": "object", 7 | "description": "Metadata tag on an atproto resource (eg, repo or record)", 8 | "required": ["src", "uri", "val", "cts"], 9 | "properties": { 10 | "src": { 11 | "type": "string", 12 | "format": "did", 13 | "description": "DID of the actor who created this label" 14 | }, 15 | "uri": { 16 | "type": "string", 17 | "format": "uri", 18 | "description": "AT URI of the record, repository (account), or other resource which this label applies to" 19 | }, 20 | "cid": { 21 | "type": "string", 22 | "format": "cid", 23 | "description": "optionally, CID specifying the specific version of 'uri' resource this label applies to" 24 | }, 25 | "val": { 26 | "type": "string", 27 | "maxLength": 128, 28 | "description": "the short string name of the value or type of this label" 29 | }, 30 | "neg": { 31 | "type": "boolean", 32 | "description": "if true, this is a negation label, overwriting a previous label" 33 | }, 34 | "cts": { 35 | "type": "string", 36 | "format": "datetime", 37 | "description": "timestamp when this label was created" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lexicons/app/bsky/embed/external.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.embed.external", 4 | "description": "A representation of some externally linked content, embedded in another form of content", 5 | "defs": { 6 | "main": { 7 | "type": "object", 8 | "required": ["external"], 9 | "properties": { 10 | "external": { 11 | "type": "ref", 12 | "ref": "#external" 13 | } 14 | } 15 | }, 16 | "external": { 17 | "type": "object", 18 | "required": ["uri", "title", "description"], 19 | "properties": { 20 | "uri": {"type": "string", "format": "uri"}, 21 | "title": {"type": "string"}, 22 | "description": {"type": "string"}, 23 | "thumb": { 24 | "type": "blob", 25 | "accept": ["image/*"], 26 | "maxSize": 1000000 27 | } 28 | } 29 | }, 30 | "view": { 31 | "type": "object", 32 | "required": ["external"], 33 | "properties": { 34 | "external": { 35 | "type": "ref", 36 | "ref": "#viewExternal" 37 | } 38 | } 39 | }, 40 | "viewExternal": { 41 | "type": "object", 42 | "required": ["uri", "title", "description"], 43 | "properties": { 44 | "uri": {"type": "string", "format": "uri"}, 45 | "title": {"type": "string"}, 46 | "description": {"type": "string"}, 47 | "thumb": {"type": "string"} 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/feed/get_likes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import validate_at_uri, validate_cid, validate_datetime 5 | from psychonaut.api.session import Session 6 | 7 | 8 | class Like(BaseModel): 9 | """ 10 | [none provided by spec] 11 | """ 12 | 13 | indexedAt: str = Field(..., pre=True, validator=validate_datetime) 14 | createdAt: str = Field(..., pre=True, validator=validate_datetime) 15 | actor: ProfileView 16 | 17 | 18 | class GetLikesResp(BaseModel): 19 | uri: str = Field(..., pre=True, validator=validate_at_uri) 20 | cid: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 21 | cursor: Optional[str] = Field(default=None) 22 | likes: Any 23 | 24 | 25 | class GetLikesReq(BaseModel): 26 | """ 27 | [none provided by spec] 28 | """ 29 | 30 | uri: str = Field(..., pre=True, validator=validate_at_uri) 31 | cid: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 32 | limit: Optional[int] = Field(default=50, ge=1, le=100) 33 | cursor: Optional[str] = Field(default=None) 34 | 35 | @property 36 | def xrpc_id(self) -> str: 37 | return "app.bsky.feed.getLikes" 38 | 39 | async def do_xrpc(self, sess: Session) -> GetLikesResp: 40 | resp = await sess.query(self) 41 | return GetLikesResp(**resp) 42 | -------------------------------------------------------------------------------- /lexicons/com/atproto/server/createAccount.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.server.createAccount", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Create an account.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["handle", "email", "password"], 13 | "properties": { 14 | "email": {"type": "string"}, 15 | "handle": {"type": "string", "format": "handle"}, 16 | "inviteCode": {"type": "string"}, 17 | "password": {"type": "string"}, 18 | "recoveryKey": {"type": "string"} 19 | } 20 | } 21 | }, 22 | "output": { 23 | "encoding": "application/json", 24 | "schema": { 25 | "type": "object", 26 | "required": ["accessJwt", "refreshJwt", "handle", "did"], 27 | "properties": { 28 | "accessJwt": { "type": "string" }, 29 | "refreshJwt": { "type": "string" }, 30 | "handle": { "type": "string", "format": "handle" }, 31 | "did": { "type": "string", "format": "did" } 32 | } 33 | } 34 | }, 35 | "errors": [ 36 | {"name": "InvalidHandle"}, 37 | {"name": "InvalidPassword"}, 38 | {"name": "InvalidInviteCode"}, 39 | {"name": "HandleNotAvailable"}, 40 | {"name": "UnsupportedDomain"} 41 | ] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /lexicons/com/atproto/moderation/defs.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.moderation.defs", 4 | "defs": { 5 | "reasonType": { 6 | "type": "string", 7 | "knownValues": [ 8 | "com.atproto.moderation.defs#reasonSpam", 9 | "com.atproto.moderation.defs#reasonViolation", 10 | "com.atproto.moderation.defs#reasonMisleading", 11 | "com.atproto.moderation.defs#reasonSexual", 12 | "com.atproto.moderation.defs#reasonRude", 13 | "com.atproto.moderation.defs#reasonOther" 14 | ] 15 | }, 16 | "reasonSpam": { 17 | "type": "token", 18 | "description": "Spam: frequent unwanted promotion, replies, mentions" 19 | }, 20 | "reasonViolation": { 21 | "type": "token", 22 | "description": "Direct violation of server rules, laws, terms of service" 23 | }, 24 | "reasonMisleading": { 25 | "type": "token", 26 | "description": "Misleading identity, affiliation, or content" 27 | }, 28 | "reasonSexual": { 29 | "type": "token", 30 | "description": "Unwanted or mis-labeled sexual content" 31 | }, 32 | "reasonRude": { 33 | "type": "token", 34 | "description": "Rude, harassing, explicit, or otherwise unwelcoming behavior" 35 | }, 36 | "reasonOther": { 37 | "type": "token", 38 | "description": "Other: reports not falling under another report category" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/delete_record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import ( 5 | validate_cid, 6 | validate_at_identifier, 7 | validate_nsid, 8 | ) 9 | 10 | 11 | class DeleteRecordReq(BaseModel): 12 | """ 13 | Delete a record, or ensure it doesn't exist. 14 | """ 15 | 16 | repo: str = Field( 17 | ..., 18 | description="The handle or DID of the repo.", 19 | pre=True, 20 | validator=validate_at_identifier, 21 | ) 22 | collection: str = Field( 23 | ..., 24 | description="The NSID of the record collection.", 25 | pre=True, 26 | validator=validate_nsid, 27 | ) 28 | rkey: str = Field(..., description="The key of the record.") 29 | swapRecord: Optional[str] = Field( 30 | default=None, 31 | description="Compare and swap with the previous record by cid.", 32 | pre=True, 33 | validator=validate_cid, 34 | ) 35 | swapCommit: Optional[str] = Field( 36 | default=None, 37 | description="Compare and swap with the previous commit by cid.", 38 | pre=True, 39 | validator=validate_cid, 40 | ) 41 | 42 | @property 43 | def xrpc_id(self) -> str: 44 | return "com.atproto.repo.deleteRecord" 45 | 46 | async def do_xrpc(self, sess: Session) -> Any: 47 | return await sess.procedure(self) 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: [push] 3 | jobs: 4 | test: 5 | 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: true 9 | matrix: 10 | python-version: ["3.10", "3.11"] 11 | 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@v3 15 | - name: Set up python ${{ matrix.python-version }} 16 | id: setup-python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install Poetry 21 | uses: snok/install-poetry@v1 22 | with: 23 | virtualenvs-create: true 24 | virtualenvs-in-project: true 25 | - name: Load cached venv 26 | id: cached-poetry-dependencies 27 | uses: actions/cache@v3 28 | with: 29 | path: .venv 30 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 31 | - name: Install dependencies 32 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 33 | run: poetry install --no-interaction --no-root 34 | - name: Install library 35 | run: poetry install --no-interaction 36 | - name: Run tests 37 | run: | 38 | source .venv/bin/activate 39 | coverage run -m pytest 40 | coverage report -m 41 | - name: Run command 42 | run: | 43 | source .venv/bin/activate 44 | psychonaut --help -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "psychonaut" 3 | version = "0.0.16" 4 | description = "Python async client and TUI for bsky" 5 | authors = ["generativist "] 6 | readme = "README.md" 7 | homepage = "https://github.com/jbn/psychonaut" 8 | repository = "https://github.com/jbn/psychonaut" 9 | documentation = "https://github.com/jbn/psychonaut" 10 | license = "MIT" 11 | 12 | [tool.poetry.scripts] 13 | psychonaut = "psychonaut.cli.main:cli" 14 | 15 | [tool.poetry.dependencies] 16 | python = "^3.10" 17 | aiohttp = "^3.8.4" 18 | pydantic = "^1.10.7" 19 | click = "^8.1.3" 20 | python-dotenv = "^1.0.0" 21 | multiformats = "^0.2.1" 22 | python-dateutil = "^2.8.2" 23 | aiodns = "^3.0.0" 24 | more-itertools = "^9.1.0" 25 | websockets = "^11.0.2" 26 | cbor2 = "^5.4.6" 27 | dag-cbor = "^0.3.2" 28 | carbox = "^0.0.1" 29 | 30 | [tool.poetry.group.dev.dependencies] 31 | pytest = "^7.3.1" 32 | jmespath = "^1.0.1" 33 | pytest-asyncio = "^0.21.0" 34 | jinja2 = "^3.1.2" 35 | coverage = {extras = ["toml"], version = "^7.2.3"} 36 | ipykernel = "^6.22.0" 37 | black = "^23.3.0" 38 | numba = "^0.57.0" 39 | fastparquet = "^2023.4.0" 40 | pandas = "^2.0.1" 41 | mkdocs = "^1.4.3" 42 | 43 | [tool.poetry.extras] 44 | optimized = ["numba"] 45 | 46 | [build-system] 47 | requires = ["poetry-core"] 48 | build-backend = "poetry.core.masonry.api" 49 | 50 | [tool.pytest.ini_options] 51 | addopts = "--capture=no" 52 | 53 | [tool.coverage.run] 54 | omit = [".*", "bin/*", "**/*_test.py"] 55 | 56 | [tool.coverage.report] 57 | fail_under = 1 58 | -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/deleteRecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.deleteRecord", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Delete a record, or ensure it doesn't exist.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["repo", "collection", "rkey"], 13 | "properties": { 14 | "repo": { 15 | "type": "string", 16 | "format": "at-identifier", 17 | "description": "The handle or DID of the repo." 18 | }, 19 | "collection": { 20 | "type": "string", 21 | "format": "nsid", 22 | "description": "The NSID of the record collection." 23 | }, 24 | "rkey": { 25 | "type": "string", 26 | "description": "The key of the record." 27 | }, 28 | "swapRecord": { 29 | "type": "string", 30 | "format": "cid", 31 | "description": "Compare and swap with the previous record by cid." 32 | }, 33 | "swapCommit": { 34 | "type": "string", 35 | "format": "cid", 36 | "description": "Compare and swap with the previous commit by cid." 37 | } 38 | } 39 | } 40 | }, 41 | "errors": [ 42 | {"name": "InvalidSwap"} 43 | ] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lexicons/com/atproto/label/queryLabels.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.label.queryLabels", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "Find labels relevant to the provided URI patterns.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["uriPatterns"], 11 | "properties": { 12 | "uriPatterns": { 13 | "type": "array", 14 | "items": {"type": "string"}, 15 | "description": "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI" 16 | }, 17 | "sources": { 18 | "type": "array", 19 | "items": {"type": "string", "format": "did"}, 20 | "description": "Optional list of label sources (DIDs) to filter on" 21 | }, 22 | "limit": {"type": "integer", "minimum": 1, "maximum": 250, "default": 50}, 23 | "cursor": {"type": "string"} 24 | } 25 | }, 26 | "output": { 27 | "encoding": "application/json", 28 | "schema": { 29 | "type": "object", 30 | "required": ["labels"], 31 | "properties": { 32 | "cursor": {"type": "string"}, 33 | "labels": { 34 | "type": "array", 35 | "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /psychonaut/identifier/did.py: -------------------------------------------------------------------------------- 1 | """ 2 | See: 3 | 4 | https://github.dev/bluesky-social/atproto/blob/main/packages/lexicon/src/validators/formats.ts 5 | """ 6 | import re 7 | 8 | 9 | class InvalidDidError(ValueError): 10 | pass 11 | 12 | 13 | def ensure_valid_did(did: str) -> None: 14 | # check that all chars are boring ASCII 15 | if not re.match(r"^[a-zA-Z0-9._:%-]*$", did): 16 | raise InvalidDidError( 17 | "Disallowed characters in DID (ASCII letters, digits, and a couple other characters only)" 18 | ) 19 | 20 | parts = did.split(":") 21 | if len(parts) < 3: 22 | raise InvalidDidError( 23 | "DID requires prefix, method, and method-specific content" 24 | ) 25 | 26 | if parts[0] != "did": 27 | raise InvalidDidError('DID requires "did:" prefix') 28 | 29 | if not re.match(r"^[a-z]+$", parts[1]): 30 | raise InvalidDidError("DID method must be lower-case letters") 31 | 32 | if did.endswith(":") or did.endswith("%"): 33 | raise InvalidDidError('DID can not end with ":" or "%"') 34 | 35 | if len(did) > 8 * 1024: 36 | raise InvalidDidError("DID is far too long") 37 | 38 | 39 | def ensure_valid_did_regex(did: str) -> None: 40 | # simple regex to enforce most constraints via just regex and length 41 | if not re.match(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$", did): 42 | raise InvalidDidError("DID didn't validate via regex") 43 | 44 | if len(did) > 8 * 1024: 45 | raise InvalidDidError("DID is far too long") 46 | -------------------------------------------------------------------------------- /psychonaut/lexicon/codegen_test.py: -------------------------------------------------------------------------------- 1 | from psychonaut.lexicon.ctx import GenCtx 2 | from psychonaut.lexicon.types import LexBoolean, LexInteger, LexString, LexXrpcParameters 3 | from .codegen import generate_pydantic_model 4 | from psychonaut.util import load_test_fixture 5 | 6 | 7 | class TestGeneratePydanticModel: 8 | def test_from_lex_xrpc_parameters(self, load_test_fixture): 9 | expected = load_test_fixture("pydantic_model_lex_xrpc_parameters.txt", False) 10 | 11 | ctx = GenCtx() 12 | 13 | lex_xrpc_parameters = LexXrpcParameters( 14 | description="A test request", 15 | required=["a_bool"], 16 | properties={ 17 | "a_bool": LexBoolean( 18 | description="A boolean", 19 | default=True, 20 | #const=False, # TODO: const is not supported yet 21 | ), 22 | "an_int": LexInteger( 23 | description="An integer", 24 | default=42, 25 | minimum=0, 26 | maximum=100, 27 | #const=42, # TODO: const is not supported yet 28 | ), 29 | "a_string": LexString( 30 | description="A string", 31 | minLength=1, 32 | maxLength=100, 33 | ) 34 | } 35 | ) 36 | 37 | src = generate_pydantic_model(ctx, "TestReq", "test 1", lex_xrpc_parameters) 38 | 39 | assert src == expected 40 | 41 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/label/defs.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import ( 4 | validate_cid, 5 | validate_uri, 6 | validate_did, 7 | validate_datetime, 8 | ) 9 | 10 | 11 | class Label(BaseModel): 12 | """ 13 | Metadata tag on an atproto resource (eg, repo or record) 14 | """ 15 | 16 | src: str = Field( 17 | ..., 18 | description="DID of the actor who created this label", 19 | pre=True, 20 | validator=validate_did, 21 | ) 22 | uri: str = Field( 23 | ..., 24 | description="AT URI of the record, repository (account), or other resource which this label applies to", 25 | pre=True, 26 | validator=validate_uri, 27 | ) 28 | cid: Optional[str] = Field( 29 | default=None, 30 | description="optionally, CID specifying the specific version of 'uri' resource this label applies to", 31 | pre=True, 32 | validator=validate_cid, 33 | ) 34 | val: str = Field( 35 | ..., 36 | description="the short string name of the value or type of this label", 37 | max_length=128, 38 | ) 39 | neg: Optional[bool] = Field( 40 | default=None, 41 | description="if true, this is a negation label, overwriting a previous label", 42 | ) 43 | cts: str = Field( 44 | ..., 45 | description="timestamp when this label was created", 46 | pre=True, 47 | validator=validate_datetime, 48 | ) 49 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/get_record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import ( 5 | validate_at_uri, 6 | validate_cid, 7 | validate_at_identifier, 8 | validate_nsid, 9 | ) 10 | 11 | 12 | class GetRecordResp(BaseModel): 13 | uri: str = Field(..., pre=True, validator=validate_at_uri) 14 | cid: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 15 | value: Any 16 | 17 | 18 | class GetRecordReq(BaseModel): 19 | """ 20 | Get a record. 21 | """ 22 | 23 | repo: str = Field( 24 | ..., 25 | description="The handle or DID of the repo.", 26 | pre=True, 27 | validator=validate_at_identifier, 28 | ) 29 | collection: str = Field( 30 | ..., 31 | description="The NSID of the record collection.", 32 | pre=True, 33 | validator=validate_nsid, 34 | ) 35 | rkey: str = Field(..., description="The key of the record.") 36 | cid: Optional[str] = Field( 37 | default=None, 38 | description="The CID of the version of the record. If not specified, then return the most recent version.", 39 | pre=True, 40 | validator=validate_cid, 41 | ) 42 | 43 | @property 44 | def xrpc_id(self) -> str: 45 | return "com.atproto.repo.getRecord" 46 | 47 | async def do_xrpc(self, sess: Session) -> GetRecordResp: 48 | resp = await sess.query(self) 49 | return GetRecordResp(**resp) 50 | -------------------------------------------------------------------------------- /psychonaut/firehose/simple_reader.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Iterable 2 | 3 | from pydantic import BaseModel 4 | from carbox.car import read_car 5 | from psychonaut.firehose.serde import read_event_pair, read_enriched_event 6 | 7 | 8 | ERROR_CALLBACK_TYPE = Callable[[int, bytes], None] 9 | 10 | 11 | def noop_error_callback(i: int, e: Exception, msg: bytes) -> None: 12 | pass 13 | 14 | 15 | class ErrorCollector: 16 | def __init__(self): 17 | self.errors = [] 18 | 19 | def __call__(self, i: int, e: Exception, msg: bytes) -> None: 20 | self.errors.append((i, e, msg)) 21 | 22 | def __len__(self): 23 | return len(self.errors) 24 | 25 | 26 | def iter_events( 27 | messages: Iterable[bytes], err_callback: ERROR_CALLBACK_TYPE = noop_error_callback 28 | ) -> Iterable[Any]: 29 | for i, msg in enumerate(messages): 30 | header, event = read_event_pair(msg) 31 | try: 32 | roots, blocks = read_car(event["blocks"]) 33 | yield header, event, roots, blocks 34 | except KeyError as e: 35 | err_callback(i, e, msg) 36 | 37 | 38 | def iter_enriched_events( 39 | messages: Iterable[bytes], err_callback: ERROR_CALLBACK_TYPE = noop_error_callback, with_messages=False 40 | ) -> Iterable[BaseModel]: 41 | for i, msg in enumerate(messages): 42 | try: 43 | if with_messages: 44 | yield read_enriched_event(msg), msg 45 | else: 46 | yield read_enriched_event(msg) 47 | except Exception as e: 48 | err_callback(i, e, msg) 49 | 50 | -------------------------------------------------------------------------------- /bin/lexicon_codegen.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import click 3 | from pathlib import Path 4 | from psychonaut.lexicon.codegen import generate_all 5 | 6 | 7 | @click.command() 8 | # Add the input directory (which must exist!) 9 | @click.option( 10 | "--input-dir", 11 | default="lexicons", 12 | help="The input directory for the lexicon json files.", 13 | type=click.Path(exists=True), 14 | ) 15 | @click.option( 16 | "--output-dir", 17 | default="psychonaut/api/lexicons", 18 | type=click.Path(exists=False), 19 | help="The output directory for the generated lexicon files.", 20 | ) 21 | @click.option( 22 | "--remove-existing", 23 | default=False, 24 | type=bool, 25 | help="Whether to remove existing files in the output directory.", 26 | is_flag=True, 27 | ) 28 | @click.option( 29 | "--verbose", 30 | default=False, 31 | type=bool, 32 | help="Whether to print verbose output.", 33 | is_flag=True, 34 | ) 35 | @click.option( 36 | "--import-all-test", 37 | default=False, 38 | type=bool, 39 | help="Test the generated code by importing all the generated files.", 40 | is_flag=True, 41 | ) 42 | def main(input_dir: str, output_dir: str, remove_existing: bool, verbose: bool, import_all_test: bool): 43 | if remove_existing and Path(output_dir).exists(): 44 | if verbose: 45 | print(f"Removing existing files in {output_dir}") 46 | 47 | shutil.rmtree(output_dir) 48 | 49 | generate_all( 50 | input_dir=Path(input_dir), 51 | output_dir=Path(output_dir), 52 | verbose=verbose, 53 | import_all_test=import_all_test, 54 | ) 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /lexicons/com/atproto/admin/takeModerationAction.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.admin.takeModerationAction", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Take a moderation action on a repo.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["action", "subject", "reason", "createdBy"], 13 | "properties": { 14 | "action": { 15 | "type": "string", 16 | "knownValues": [ 17 | "com.atproto.admin.defs#takedown", 18 | "com.atproto.admin.defs#flag", 19 | "com.atproto.admin.defs#acknowledge" 20 | ] 21 | }, 22 | "subject": { 23 | "type": "union", 24 | "refs": [ 25 | "com.atproto.admin.defs#repoRef", 26 | "com.atproto.repo.strongRef" 27 | ] 28 | }, 29 | "subjectBlobCids": {"type": "array", "items": {"type": "string", "format": "cid"}}, 30 | "createLabelVals": {"type": "array", "items": {"type": "string"}}, 31 | "negateLabelVals": {"type": "array", "items": {"type": "string"}}, 32 | "reason": {"type": "string"}, 33 | "createdBy": {"type": "string", "format": "did"} 34 | } 35 | } 36 | }, 37 | "output": { 38 | "encoding": "application/json", 39 | "schema": { 40 | "type": "ref", 41 | "ref": "com.atproto.admin.defs#actionView" 42 | } 43 | }, 44 | "errors": [{ "name": "SubjectHasAction" }] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/create_record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import ( 5 | validate_at_uri, 6 | validate_cid, 7 | validate_at_identifier, 8 | validate_nsid, 9 | ) 10 | 11 | 12 | class CreateRecordResp(BaseModel): 13 | uri: str = Field(..., pre=True, validator=validate_at_uri) 14 | cid: str = Field(..., pre=True, validator=validate_cid) 15 | 16 | 17 | class CreateRecordReq(BaseModel): 18 | """ 19 | Create a new record. 20 | """ 21 | 22 | repo: str = Field( 23 | ..., 24 | description="The handle or DID of the repo.", 25 | pre=True, 26 | validator=validate_at_identifier, 27 | ) 28 | collection: str = Field( 29 | ..., 30 | description="The NSID of the record collection.", 31 | pre=True, 32 | validator=validate_nsid, 33 | ) 34 | rkey: Optional[str] = Field(default=None, description="The key of the record.") 35 | validate_flag: Optional[bool] = Field( 36 | alias="validate", default=True, description="Validate the record?" 37 | ) 38 | record: Any 39 | swapCommit: Optional[str] = Field( 40 | default=None, 41 | description="Compare and swap with the previous commit by cid.", 42 | pre=True, 43 | validator=validate_cid, 44 | ) 45 | 46 | @property 47 | def xrpc_id(self) -> str: 48 | return "com.atproto.repo.createRecord" 49 | 50 | async def do_xrpc(self, sess: Session) -> CreateRecordResp: 51 | resp = await sess.procedure(self) 52 | return CreateRecordResp(**resp) 53 | -------------------------------------------------------------------------------- /lexicons/com/atproto/moderation/createReport.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.moderation.createReport", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Report a repo or a record.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["reasonType", "subject"], 13 | "properties": { 14 | "reasonType": {"type": "ref", "ref": "com.atproto.moderation.defs#reasonType"}, 15 | "reason": {"type": "string"}, 16 | "subject": { 17 | "type": "union", 18 | "refs": [ 19 | "com.atproto.admin.defs#repoRef", 20 | "com.atproto.repo.strongRef" 21 | ] 22 | } 23 | } 24 | } 25 | }, 26 | "output": { 27 | "encoding": "application/json", 28 | "schema": { 29 | "type": "object", 30 | "required": ["id", "reasonType", "subject", "reportedBy", "createdAt"], 31 | "properties": { 32 | "id": {"type": "integer"}, 33 | "reasonType": {"type": "ref", "ref": "com.atproto.moderation.defs#reasonType"}, 34 | "reason": {"type": "string"}, 35 | "subject": { 36 | "type": "union", 37 | "refs": [ 38 | "com.atproto.admin.defs#repoRef", 39 | "com.atproto.repo.strongRef" 40 | ] 41 | }, 42 | "reportedBy": {"type": "string", "format": "did"}, 43 | "createdAt": {"type": "string", "format": "datetime"} 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/apply_writes.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import ( 4 | validate_cid, 5 | validate_at_identifier, 6 | validate_nsid, 7 | ) 8 | from psychonaut.api.session import Session 9 | 10 | 11 | class Delete(BaseModel): 12 | """ 13 | Delete an existing record. 14 | """ 15 | 16 | collection: str = Field(..., pre=True, validator=validate_nsid) 17 | rkey: str = Field(...) 18 | 19 | 20 | class Create(BaseModel): 21 | """ 22 | Create a new record. 23 | """ 24 | 25 | collection: str = Field(..., pre=True, validator=validate_nsid) 26 | rkey: Optional[str] = Field(default=None) 27 | value: Any 28 | 29 | 30 | class Update(BaseModel): 31 | """ 32 | Update an existing record. 33 | """ 34 | 35 | collection: str = Field(..., pre=True, validator=validate_nsid) 36 | rkey: str = Field(...) 37 | value: Any 38 | 39 | 40 | class ApplyWritesReq(BaseModel): 41 | """ 42 | Apply a batch transaction of creates, updates, and deletes. 43 | """ 44 | 45 | repo: str = Field( 46 | ..., 47 | description="The handle or DID of the repo.", 48 | pre=True, 49 | validator=validate_at_identifier, 50 | ) 51 | validate_flag: Optional[bool] = Field( 52 | alias="validate", default=True, description="Validate the records?" 53 | ) 54 | writes: List[Any] = Field(...) 55 | swapCommit: Optional[str] = Field(default=None, pre=True, validator=validate_cid) 56 | 57 | @property 58 | def xrpc_id(self) -> str: 59 | return "com.atproto.repo.applyWrites" 60 | 61 | async def do_xrpc(self, sess: Session) -> Any: 62 | return await sess.procedure(self) 63 | -------------------------------------------------------------------------------- /psychonaut/cli/useful_queries.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Tuple 3 | import click 4 | 5 | from psychonaut.cli.util import clean_handle 6 | from .group import cli 7 | from .util import as_async, print_error_and_fail 8 | from psychonaut.api.lexicons.app.bsky.actor.get_profiles import GetProfilesReq 9 | from psychonaut.api.lexicons.app.bsky.actor.get_profile import GetProfileReq 10 | from psychonaut.api.lexicons.com.atproto.identity.resolve_handle import ResolveHandleReq 11 | from psychonaut.client import get_simple_client_session 12 | 13 | 14 | @cli.command() 15 | @click.argument("handle") 16 | @as_async 17 | async def resolve_handle(handle: str): 18 | handle = clean_handle(handle) 19 | 20 | if not handle: 21 | print_error_and_fail("No handle provided") 22 | 23 | async with get_simple_client_session() as sess: 24 | resp = await ResolveHandleReq(handle=handle).do_xrpc(sess) 25 | print(resp.json()) 26 | 27 | 28 | @cli.command() 29 | @click.argument("actors", nargs=-1) 30 | @as_async 31 | async def get_profiles(actors: Tuple[str]): 32 | actors = [clean_handle(actor) for actor in actors if clean_handle(actor)] 33 | 34 | if not actors: 35 | print_error_and_fail("No actors provided") 36 | 37 | async with get_simple_client_session() as sess: 38 | resp = await GetProfilesReq(actors=actors).do_xrpc(sess) 39 | for profile in resp.profiles: 40 | print(json.dumps(profile)) 41 | 42 | 43 | @cli.command() 44 | @click.argument("actor", nargs=1) 45 | @as_async 46 | async def get_profile(actor: str): 47 | actor = clean_handle(actor) 48 | if not actor: 49 | print_error_and_fail("No actor provided") 50 | 51 | async with get_simple_client_session() as sess: 52 | resp = await GetProfileReq(actor=actor).do_xrpc(sess) 53 | print(json.dumps(resp)) 54 | -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/listRecords.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.listRecords", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "description": "List a range of records in a collection.", 8 | "parameters": { 9 | "type": "params", 10 | "required": ["repo", "collection"], 11 | "properties": { 12 | "repo": {"type": "string", "format": "at-identifier", "description": "The handle or DID of the repo."}, 13 | "collection": {"type": "string", "format": "nsid", "description": "The NSID of the record type."}, 14 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50, "description": "The number of records to return."}, 15 | "cursor": {"type": "string"}, 16 | "rkeyStart": {"type": "string", "description": "DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)"}, 17 | "rkeyEnd": {"type": "string", "description": "DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)"}, 18 | "reverse": {"type": "boolean", "description": "Reverse the order of the returned records?"} 19 | } 20 | }, 21 | "output": { 22 | "encoding": "application/json", 23 | "schema": { 24 | "type": "object", 25 | "required": ["records"], 26 | "properties": { 27 | "cursor": {"type": "string"}, 28 | "records": { 29 | "type": "array", 30 | "items": {"type": "ref", "ref": "#record"} 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "record": { 37 | "type": "object", 38 | "required": ["uri", "cid", "value"], 39 | "properties": { 40 | "uri": {"type": "string", "format": "at-uri"}, 41 | "cid": {"type": "string", "format": "cid"}, 42 | "value": {"type": "unknown"} 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/repo/put_record.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.session import Session 3 | from pydantic import BaseModel, Field 4 | from psychonaut.lexicon.formats import ( 5 | validate_at_uri, 6 | validate_cid, 7 | validate_at_identifier, 8 | validate_nsid, 9 | ) 10 | 11 | 12 | class PutRecordResp(BaseModel): 13 | uri: str = Field(..., pre=True, validator=validate_at_uri) 14 | cid: str = Field(..., pre=True, validator=validate_cid) 15 | 16 | 17 | class PutRecordReq(BaseModel): 18 | """ 19 | Write a record, creating or updating it as needed. 20 | """ 21 | 22 | repo: str = Field( 23 | ..., 24 | description="The handle or DID of the repo.", 25 | pre=True, 26 | validator=validate_at_identifier, 27 | ) 28 | collection: str = Field( 29 | ..., 30 | description="The NSID of the record collection.", 31 | pre=True, 32 | validator=validate_nsid, 33 | ) 34 | rkey: str = Field(..., description="The key of the record.") 35 | validate_flag: Optional[bool] = Field( 36 | alias="validate", default=True, description="Validate the record?" 37 | ) 38 | record: Any 39 | swapRecord: Optional[str] = Field( 40 | default=None, 41 | description="Compare and swap with the previous record by cid.", 42 | pre=True, 43 | validator=validate_cid, 44 | ) 45 | swapCommit: Optional[str] = Field( 46 | default=None, 47 | description="Compare and swap with the previous commit by cid.", 48 | pre=True, 49 | validator=validate_cid, 50 | ) 51 | 52 | @property 53 | def xrpc_id(self) -> str: 54 | return "com.atproto.repo.putRecord" 55 | 56 | async def do_xrpc(self, sess: Session) -> PutRecordResp: 57 | resp = await sess.procedure(self) 58 | return PutRecordResp(**resp) 59 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/app/bsky/notification/list_notifications.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from psychonaut.api.lexicons.app.bsky.actor.defs import ProfileView 3 | from psychonaut.api.lexicons.com.atproto.label.defs import Label 4 | from pydantic import BaseModel, Field 5 | from psychonaut.lexicon.formats import validate_at_uri, validate_cid, validate_datetime 6 | from psychonaut.api.session import Session 7 | 8 | 9 | class Notification(BaseModel): 10 | """ 11 | [none provided by spec] 12 | """ 13 | 14 | uri: str = Field(..., pre=True, validator=validate_at_uri) 15 | cid: str = Field(..., pre=True, validator=validate_cid) 16 | author: ProfileView 17 | reason: str = Field( 18 | ..., 19 | description="Expected values are 'like', 'repost', 'follow', 'mention', 'reply', and 'quote'.", 20 | known_values=["like", "repost", "follow", "mention", "reply", "quote"], 21 | ) 22 | reasonSubject: Optional[str] = Field( 23 | default=None, pre=True, validator=validate_at_uri 24 | ) 25 | record: Any 26 | isRead: bool = Field(...) 27 | indexedAt: str = Field(..., pre=True, validator=validate_datetime) 28 | labels: Optional[Any] = None 29 | 30 | 31 | class ListNotificationsResp(BaseModel): 32 | cursor: Optional[str] = Field(default=None) 33 | notifications: Any 34 | 35 | 36 | class ListNotificationsReq(BaseModel): 37 | """ 38 | [none provided by spec] 39 | """ 40 | 41 | limit: Optional[int] = Field(default=50, ge=1, le=100) 42 | cursor: Optional[str] = Field(default=None) 43 | seenAt: Optional[str] = Field(default=None, pre=True, validator=validate_datetime) 44 | 45 | @property 46 | def xrpc_id(self) -> str: 47 | return "app.bsky.notification.listNotifications" 48 | 49 | async def do_xrpc(self, sess: Session) -> ListNotificationsResp: 50 | resp = await sess.query(self) 51 | return ListNotificationsResp(**resp) 52 | -------------------------------------------------------------------------------- /lexicons/com/atproto/repo/createRecord.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "com.atproto.repo.createRecord", 4 | "defs": { 5 | "main": { 6 | "type": "procedure", 7 | "description": "Create a new record.", 8 | "input": { 9 | "encoding": "application/json", 10 | "schema": { 11 | "type": "object", 12 | "required": ["repo", "collection", "record"], 13 | "properties": { 14 | "repo": { 15 | "type": "string", 16 | "format": "at-identifier", 17 | "description": "The handle or DID of the repo." 18 | }, 19 | "collection": { 20 | "type": "string", 21 | "format": "nsid", 22 | "description": "The NSID of the record collection." 23 | }, 24 | "rkey": { 25 | "type": "string", 26 | "description": "The key of the record." 27 | }, 28 | "validate": { 29 | "type": "boolean", 30 | "default": true, 31 | "description": "Validate the record?" 32 | }, 33 | "record": { 34 | "type": "unknown", 35 | "description": "The record to create." 36 | }, 37 | "swapCommit": { 38 | "type": "string", 39 | "format": "cid", 40 | "description": "Compare and swap with the previous commit by cid." 41 | } 42 | } 43 | } 44 | }, 45 | "output": { 46 | "encoding": "application/json", 47 | "schema": { 48 | "type": "object", 49 | "required": ["uri", "cid"], 50 | "properties": { 51 | "uri": {"type": "string", "format": "at-uri"}, 52 | "cid": {"type": "string", "format": "cid"} 53 | } 54 | } 55 | }, 56 | "errors": [ 57 | {"name": "InvalidSwap"} 58 | ] 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lexicons/app/bsky/notification/listNotifications.json: -------------------------------------------------------------------------------- 1 | { 2 | "lexicon": 1, 3 | "id": "app.bsky.notification.listNotifications", 4 | "defs": { 5 | "main": { 6 | "type": "query", 7 | "parameters": { 8 | "type": "params", 9 | "properties": { 10 | "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, 11 | "cursor": {"type": "string"}, 12 | "seenAt": { "type": "string", "format": "datetime"} 13 | } 14 | }, 15 | "output": { 16 | "encoding": "application/json", 17 | "schema": { 18 | "type": "object", 19 | "required": ["notifications"], 20 | "properties": { 21 | "cursor": {"type": "string"}, 22 | "notifications": { 23 | "type": "array", 24 | "items": {"type": "ref", "ref": "#notification"} 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "notification": { 31 | "type": "object", 32 | "required": ["uri", "cid", "author", "reason", "record", "isRead", "indexedAt"], 33 | "properties": { 34 | "uri": {"type": "string", "format": "at-uri"}, 35 | "cid": {"type": "string", "format": "cid" }, 36 | "author": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"}, 37 | "reason": { 38 | "type": "string", 39 | "description": "Expected values are 'like', 'repost', 'follow', 'mention', 'reply', and 'quote'.", 40 | "knownValues": ["like", "repost", "follow", "mention", "reply", "quote"] 41 | }, 42 | "reasonSubject": {"type": "string", "format": "at-uri"}, 43 | "record": {"type": "unknown"}, 44 | "isRead": {"type": "boolean"}, 45 | "indexedAt": {"type": "string", "format": "datetime"}, 46 | "labels": { 47 | "type": "array", 48 | "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /psychonaut/api/lexicons/com/atproto/sync/subscribe_repos.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, List 2 | from pydantic import BaseModel, Field 3 | from psychonaut.lexicon.formats import validate_handle, validate_did, validate_datetime 4 | 5 | 6 | class Handle(BaseModel): 7 | """ 8 | [none provided by spec] 9 | """ 10 | 11 | seq: int = Field(...) 12 | did: str = Field(..., pre=True, validator=validate_did) 13 | handle: str = Field(..., pre=True, validator=validate_handle) 14 | time: str = Field(..., pre=True, validator=validate_datetime) 15 | 16 | 17 | class Tombstone(BaseModel): 18 | """ 19 | [none provided by spec] 20 | """ 21 | 22 | seq: int = Field(...) 23 | did: str = Field(..., pre=True, validator=validate_did) 24 | time: str = Field(..., pre=True, validator=validate_datetime) 25 | 26 | 27 | class Migrate(BaseModel): 28 | """ 29 | [none provided by spec] 30 | """ 31 | 32 | seq: int = Field(...) 33 | did: str = Field(..., pre=True, validator=validate_did) 34 | migrateTo: str = Field(...) 35 | time: str = Field(..., pre=True, validator=validate_datetime) 36 | 37 | 38 | class Info(BaseModel): 39 | """ 40 | [none provided by spec] 41 | """ 42 | 43 | name: str = Field(..., known_values=["OutdatedCursor"]) 44 | message: Optional[str] = Field(default=None) 45 | 46 | 47 | class RepoOp(BaseModel): 48 | """ 49 | [none provided by spec] 50 | """ 51 | 52 | action: str = Field(..., known_values=["create", "update", "delete"]) 53 | path: str = Field(...) 54 | cid: Any 55 | 56 | 57 | class Commit(BaseModel): 58 | """ 59 | [none provided by spec] 60 | """ 61 | 62 | seq: int = Field(...) 63 | rebase: bool = Field(...) 64 | tooBig: bool = Field(...) 65 | repo: str = Field(..., pre=True, validator=validate_did) 66 | commit: Any 67 | prev: Any 68 | blocks: Any 69 | ops: Any 70 | blobs: List[Any] = Field(...) 71 | time: str = Field(..., pre=True, validator=validate_datetime) 72 | -------------------------------------------------------------------------------- /examples/get_liked_posts_by_handle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import sys 4 | from more_itertools import chunked 5 | 6 | from psychonaut.client import get_simple_client_session 7 | from psychonaut.client.cursors import collect_cursored 8 | from psychonaut.cli.util import clean_handle 9 | 10 | from psychonaut.api.lexicons.com.atproto.identity.resolve_handle import ResolveHandleReq 11 | from psychonaut.api.lexicons.com.atproto.repo.list_records import ( 12 | ListRecordsReq, 13 | ListRecordsResp, 14 | ) 15 | from psychonaut.api.lexicons.app.bsky.feed.get_posts import GetPostsReq 16 | 17 | 18 | async def main(handle: str): 19 | async with get_simple_client_session() as sess: 20 | # Resolve the handle 21 | response = await ResolveHandleReq(handle=handle).do_xrpc(sess) 22 | print(response) 23 | 24 | # Taking the DID from the resolved handle and get all likes by that person 25 | likes = [ 26 | record 27 | async for record in collect_cursored( 28 | sess, 29 | ListRecordsReq( 30 | repo=response.did, 31 | collection="app.bsky.feed.like", 32 | limit=100, 33 | ), 34 | ListRecordsResp, 35 | "records", 36 | ) 37 | ] 38 | 39 | uris = [] 40 | for record in likes: 41 | print(json.dumps(record, indent=2)) 42 | uris.append( 43 | # TODO: update after fixing ref bug that makes these Any types 44 | record["value"]["subject"]["uri"] 45 | ) 46 | 47 | # Get all liked posts 48 | posts = [ 49 | post 50 | for uri_chunk in chunked(uris, 25) 51 | for post in await GetPostsReq(uris=uri_chunk).do_xrpc(sess) 52 | ] 53 | 54 | print(json.dumps({"posts": posts}, indent=2)) 55 | 56 | 57 | if __name__ == "__main__": 58 | handle = sys.argv[1] 59 | asyncio.run(main(clean_handle(handle))) 60 | --------------------------------------------------------------------------------