From d45bb37ce1fade335ca83d4229cc3a27de515698 Mon Sep 17 00:00:00 2001 From: retoor Date: Sat, 4 Jul 2026 19:16:32 +0000 Subject: [PATCH] refactor: split devii catalog.py into package (ref.md 3.5) Co-Authored-By: Claude Opus 4.8 (1M context) --- devplacepy/services/devii/actions/catalog.py | 2036 ----------------- .../devii/actions/catalog/__init__.py | 52 + .../services/devii/actions/catalog/_shared.py | 41 + .../services/devii/actions/catalog/admin.py | 425 ++++ .../services/devii/actions/catalog/auth.py | 72 + .../devii/actions/catalog/comments.py | 41 + .../services/devii/actions/catalog/dbapi.py | 127 + .../devii/actions/catalog/engagement.py | 80 + .../services/devii/actions/catalog/game.py | 169 ++ .../services/devii/actions/catalog/gateway.py | 106 + .../services/devii/actions/catalog/gists.py | 78 + .../services/devii/actions/catalog/issues.py | 156 ++ .../services/devii/actions/catalog/jobs.py | 77 + .../devii/actions/catalog/messages.py | 38 + .../services/devii/actions/catalog/news.py | 31 + .../devii/actions/catalog/notifications.py | 44 + .../services/devii/actions/catalog/posts.py | 90 + .../services/devii/actions/catalog/profile.py | 79 + .../devii/actions/catalog/project_files.py | 179 ++ .../devii/actions/catalog/projects.py | 136 ++ .../services/devii/actions/catalog/social.py | 106 + .../services/devii/actions/catalog/tools.py | 111 + .../services/devii/actions/catalog/uploads.py | 45 + 23 files changed, 2283 insertions(+), 2036 deletions(-) delete mode 100644 devplacepy/services/devii/actions/catalog.py create mode 100644 devplacepy/services/devii/actions/catalog/__init__.py create mode 100644 devplacepy/services/devii/actions/catalog/_shared.py create mode 100644 devplacepy/services/devii/actions/catalog/admin.py create mode 100644 devplacepy/services/devii/actions/catalog/auth.py create mode 100644 devplacepy/services/devii/actions/catalog/comments.py create mode 100644 devplacepy/services/devii/actions/catalog/dbapi.py create mode 100644 devplacepy/services/devii/actions/catalog/engagement.py create mode 100644 devplacepy/services/devii/actions/catalog/game.py create mode 100644 devplacepy/services/devii/actions/catalog/gateway.py create mode 100644 devplacepy/services/devii/actions/catalog/gists.py create mode 100644 devplacepy/services/devii/actions/catalog/issues.py create mode 100644 devplacepy/services/devii/actions/catalog/jobs.py create mode 100644 devplacepy/services/devii/actions/catalog/messages.py create mode 100644 devplacepy/services/devii/actions/catalog/news.py create mode 100644 devplacepy/services/devii/actions/catalog/notifications.py create mode 100644 devplacepy/services/devii/actions/catalog/posts.py create mode 100644 devplacepy/services/devii/actions/catalog/profile.py create mode 100644 devplacepy/services/devii/actions/catalog/project_files.py create mode 100644 devplacepy/services/devii/actions/catalog/projects.py create mode 100644 devplacepy/services/devii/actions/catalog/social.py create mode 100644 devplacepy/services/devii/actions/catalog/tools.py create mode 100644 devplacepy/services/devii/actions/catalog/uploads.py diff --git a/devplacepy/services/devii/actions/catalog.py b/devplacepy/services/devii/actions/catalog.py deleted file mode 100644 index ec3cdafa..00000000 --- a/devplacepy/services/devii/actions/catalog.py +++ /dev/null @@ -1,2036 +0,0 @@ -# retoor - -from __future__ import annotations - -from .spec import Action, Catalog, Param - - -def path(name: str, description: str, required: bool = True) -> Param: - return Param(name=name, location="path", description=description, required=required) - - -def query(name: str, description: str, required: bool = False) -> Param: - return Param( - name=name, location="query", description=description, required=required - ) - - -def body(name: str, description: str, required: bool = False) -> Param: - return Param(name=name, location="body", description=description, required=required) - - -def upload(name: str, description: str) -> Param: - return Param(name=name, location="file", description=description, required=True) - - -def confirm() -> Param: - return Param( - name="confirm", - location="body", - description=( - "Set to true ONLY after the user has explicitly confirmed this irreversible action. " - "Leave it unset on the first call: the tool refuses and tells you to confirm with the " - "user, and only a repeat call with confirm=true actually performs it." - ), - required=False, - type="boolean", - ) - - -ATTACHMENTS = "Comma separated attachment uids returned by upload_file." -TARGET_TYPE = "Target type, e.g. post, comment, project, gist." - -ACTIONS: tuple[Action, ...] = ( - Action( - name="auth_status", - method="GET", - path="", - summary="Report whether the current session is authenticated", - handler="status", - requires_auth=False, - ), - Action( - name="login", - method="POST", - path="/auth/login", - summary="Authenticate with email and password and start a session", - handler="login", - requires_auth=False, - params=( - body("email", "Account email address.", required=True), - body("password", "Account password.", required=True), - body("remember_me", "Keep the session alive longer ('on' or '')."), - ), - ), - Action( - name="logout", - method="GET", - path="/auth/logout", - summary="End the current session", - handler="logout", - requires_auth=True, - ), - Action( - name="signup", - method="POST", - path="/auth/signup", - summary="Create a new account", - requires_auth=False, - params=( - body("username", "Desired username.", required=True), - body("email", "Email address.", required=True), - body("password", "Password (minimum six characters).", required=True), - body("confirm_password", "Password confirmation.", required=True), - ), - ), - Action( - name="forgot_password", - method="POST", - path="/auth/forgot-password", - summary="Request a password reset email", - requires_auth=False, - params=(body("email", "Account email address.", required=True),), - ), - Action( - name="reset_password", - method="POST", - path="/auth/reset-password/{token}", - summary="Reset a password using a reset token", - requires_auth=False, - params=( - path("token", "Reset token from the email link."), - body("password", "New password.", required=True), - body("confirm_password", "New password confirmation.", required=True), - ), - ), - Action( - name="view_feed", - method="GET", - path="/feed", - summary="View the activity feed", - requires_auth=False, - params=( - query("tab", "Feed tab to view."), - query("topic", "Filter by topic."), - query("search", "Search post title, content, and author username."), - query("before", "Pagination cursor."), - ), - ), - Action( - name="create_post", - method="POST", - path="/posts/create", - summary="Create a new post", - params=( - body("content", "Post body text.", required=True), - body("title", "Optional post title."), - body("topic", "Optional topic."), - body("project_uid", "Attach the post to a project uid."), - body("attachment_uids", ATTACHMENTS), - body("poll_question", "Optional poll question."), - body( - "poll_options", - "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.", - ), - ), - ), - Action( - name="view_post", - method="GET", - path="/posts/{post_slug}", - summary="View a single post by slug", - requires_auth=False, - params=( - path( - "post_slug", - "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", - ), - ), - ), - Action( - name="edit_post", - method="POST", - path="/posts/edit/{post_slug}", - summary="Edit an existing post", - params=( - path( - "post_slug", - "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", - ), - body("content", "Updated post body.", required=True), - body("title", "Updated title."), - body("topic", "Updated topic."), - body( - "poll_question", - "Optional poll question. Adds a poll to a post that does not already have one.", - ), - body( - "poll_options", - "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.", - ), - ), - ), - Action( - name="delete_post", - method="POST", - path="/posts/delete/{post_slug}", - summary="Delete a post (your own, or any post when you are an administrator). Soft delete, confirmation required", - params=( - path( - "post_slug", - "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", - ), - confirm(), - ), - ), - Action( - name="create_comment", - method="POST", - path="/comments/create", - summary="Create a comment on a post or other target", - params=( - body("content", "Comment body.", required=True), - body("post_uid", "Uid of the post being commented on."), - body("target_uid", "Uid of the target when not a post."), - body("target_type", TARGET_TYPE), - body("parent_uid", "Parent comment uid for replies."), - body("attachment_uids", ATTACHMENTS), - ), - ), - Action( - name="edit_comment", - method="POST", - path="/comments/edit/{comment_uid}", - summary="Edit the body of one of your own comments", - params=( - path("comment_uid", "Uid of the comment."), - body("content", "New comment body, 3-1000 characters.", required=True), - ), - ), - Action( - name="delete_comment", - method="POST", - path="/comments/delete/{comment_uid}", - summary="Delete a comment (your own, or any comment when you are an administrator). Soft delete, confirmation required", - params=(path("comment_uid", "Uid of the comment."), confirm()), - ), - Action( - name="list_projects", - method="GET", - path="/projects", - summary="List projects", - requires_auth=False, - params=( - query("tab", "Projects tab."), - query("search", "Search project title, description, and author username."), - query("user_uid", "Filter by owner uid."), - query("project_type", "Filter by project type."), - query("before", "Pagination cursor."), - ), - ), - Action( - name="view_project", - method="GET", - path="/projects/{project_slug}", - summary="View a project by slug", - requires_auth=False, - params=( - path( - "project_slug", - "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", - ), - ), - ), - Action( - name="create_project", - method="POST", - path="/projects/create", - summary="Create a new project", - params=( - body("title", "Project title.", required=True), - body("description", "Project description.", required=True), - body("release_date", "Release date."), - body("demo_date", "Demo date."), - body("project_type", "Project type."), - body("platforms", "Supported platforms."), - body("status", "Project status."), - body("attachment_uids", ATTACHMENTS), - ), - ), - Action( - name="edit_project", - method="POST", - path="/projects/edit/{project_slug}", - summary="Edit an existing project", - params=( - path( - "project_slug", - "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", - ), - body("title", "Updated project title.", required=True), - body("description", "Updated project description.", required=True), - body("release_date", "Updated release date."), - body("demo_date", "Updated demo date."), - body("project_type", "Updated project type."), - body("platforms", "Updated supported platforms."), - body("status", "Updated project status."), - ), - ), - Action( - name="delete_project", - method="POST", - path="/projects/delete/{project_slug}", - summary="Delete a project and all its files (your own, or any project when you are an administrator). Soft delete, confirmation required", - params=( - path( - "project_slug", - "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", - ), - confirm(), - ), - ), - Action( - name="project_set_private", - method="POST", - path="/projects/{project_slug}/private", - summary="Mark a project private (owner-only) or public", - description=( - "Set value=true to hide the project from everyone except its owner (and administrators), " - "or value=false to make it public again. This is a significant change: you MUST ask the " - "user for explicit confirmation BEFORE calling it, and only pass confirm=true once they " - "have agreed. Only the project owner may change this." - ), - params=( - path("project_slug", "Project slug or uid."), - body( - "value", - "true to make the project private, false to make it public.", - required=True, - ), - body( - "confirm", - "Must be true, set only after the user has explicitly confirmed.", - required=True, - ), - ), - ), - Action( - name="project_set_readonly", - method="POST", - path="/projects/{project_slug}/readonly", - summary="Mark a project read-only (immutable files) or writable again", - description=( - "Set value=true to make every file in the project immutable - no writes, edits, line " - "edits, moves, deletes, or uploads succeed afterwards, from anyone including you - or " - "value=false to allow changes again. This is a significant change: you MUST ask the user " - "for explicit confirmation BEFORE calling it, and only pass confirm=true once they have " - "agreed. Only the project owner may change this." - ), - params=( - path("project_slug", "Project slug or uid."), - body( - "value", - "true to make the project read-only, false to make it writable.", - required=True, - ), - body( - "confirm", - "Must be true, set only after the user has explicitly confirmed.", - required=True, - ), - ), - ), - Action( - name="project_list_files", - method="GET", - path="/projects/{project_slug}/files", - summary="List every file and directory in a project filesystem", - description="Returns the flat list of nodes (path, type, size, mime). Use it to inspect the project tree before reading or writing files.", - params=(path("project_slug", "Project slug or uid."),), - requires_auth=False, - ), - Action( - name="project_read_file", - method="GET", - path="/projects/{project_slug}/files/raw", - summary="Read one file from a project filesystem", - description="Returns the file metadata plus the text content. Binary files return a url instead of content.", - params=( - path("project_slug", "Project slug or uid."), - query( - "path", - "Relative file path inside the project, e.g. src/main.py.", - required=True, - ), - ), - requires_auth=False, - ), - Action( - name="project_write_file", - method="POST", - path="/projects/{project_slug}/files/write", - summary="Create or overwrite a whole text file in a project (parent directories are created automatically)", - description=( - "Replaces the ENTIRE file with the content you send. Creating a new file needs no prior " - "read, but overwriting an existing one requires reading it first (project_read_file). " - "For an existing file prefer the surgical line tools (project_replace_lines, " - "project_insert_lines, project_delete_lines, project_append_file) instead of rewriting it, " - "and never batch several writes in one turn. Use this only to create a new file or fully " - "rewrite a small one. Missing parent directories are created recursively." - ), - params=( - path("project_slug", "Project slug or uid."), - body("path", "Relative file path, e.g. src/app/main.py.", required=True), - body("content", "Full file content.", required=True), - ), - ), - Action( - name="project_read_lines", - method="GET", - path="/projects/{project_slug}/files/lines", - summary="Read a 1-indexed line range of a text file in a project", - description=( - "Returns {path, start, end, total_lines, lines, content} for the requested range. Use it " - "to inspect part of a large file before editing, and to learn total_lines so you can target " - "the right range with the line-edit tools." - ), - params=( - path("project_slug", "Project slug or uid."), - query("path", "Relative file path inside the project.", required=True), - query("start", "First line to read (1-indexed, default 1)."), - query("end", "Last line to read (inclusive); omit for end of file."), - ), - requires_auth=False, - ), - Action( - name="project_replace_lines", - method="POST", - path="/projects/{project_slug}/files/replace-lines", - summary="Replace an inclusive 1-indexed line range of a text file with new content", - description=( - "Surgically rewrites lines start..end (inclusive) with content (which may be any number of " - "lines, or empty to delete the range). The preferred way to edit an existing file: it leaves " - "the rest of the file untouched and never overflows the model output limit." - ), - params=( - path("project_slug", "Project slug or uid."), - body("path", "Relative file path.", required=True), - body("start", "First line to replace (1-indexed).", required=True), - body("end", "Last line to replace (inclusive).", required=True), - body("content", "Replacement text for those lines (empty deletes them)."), - ), - ), - Action( - name="project_insert_lines", - method="POST", - path="/projects/{project_slug}/files/insert-lines", - summary="Insert content before a 1-indexed line of a text file", - description=( - "Inserts content before line 'at' without touching existing lines. Use at=1 to prepend and " - "at=total_lines+1 to insert at the end." - ), - params=( - path("project_slug", "Project slug or uid."), - body("path", "Relative file path.", required=True), - body( - "at", - "Insert before this 1-indexed line (1 prepends, total+1 appends).", - required=True, - ), - body("content", "Text to insert.", required=True), - ), - ), - Action( - name="project_delete_lines", - method="POST", - path="/projects/{project_slug}/files/delete-lines", - summary="Delete an inclusive 1-indexed line range from a text file", - params=( - path("project_slug", "Project slug or uid."), - body("path", "Relative file path.", required=True), - body("start", "First line to delete (1-indexed).", required=True), - body("end", "Last line to delete (inclusive).", required=True), - ), - ), - Action( - name="project_append_file", - method="POST", - path="/projects/{project_slug}/files/append", - summary="Append content to the end of a text file in a project", - description="Adds content as new lines at the end of the file. Use it to grow a large file across turns without resending the whole thing.", - params=( - path("project_slug", "Project slug or uid."), - body("path", "Relative file path.", required=True), - body("content", "Text to append.", required=True), - ), - ), - Action( - name="project_upload_file", - method="POST", - path="/projects/{project_slug}/files/upload", - summary="Upload a local file into a project directory (parents created automatically)", - params=( - path("project_slug", "Project slug or uid."), - upload("file", "Local filesystem path of the file to upload."), - body("path", "Target directory inside the project, empty for the root."), - ), - ), - Action( - name="project_make_dir", - method="POST", - path="/projects/{project_slug}/files/mkdir", - summary="Create a directory (and parents) in a project filesystem", - params=( - path("project_slug", "Project slug or uid."), - body( - "path", "Relative directory path, e.g. src/components.", required=True - ), - ), - ), - Action( - name="project_move_file", - method="POST", - path="/projects/{project_slug}/files/move", - summary="Move or rename a file or directory within a project", - params=( - path("project_slug", "Project slug or uid."), - body("from_path", "Existing path.", required=True), - body("to_path", "New path.", required=True), - ), - ), - Action( - name="project_delete_file", - method="POST", - path="/projects/{project_slug}/files/delete", - summary="Delete a file or directory (recursive) from a project filesystem (project owner, or any project when you are an administrator). Soft delete, confirmation required", - params=( - path("project_slug", "Project slug or uid."), - body("path", "Relative path to delete.", required=True), - confirm(), - ), - ), - Action( - name="zip_project", - method="POST", - path="/projects/{project_slug}/zip", - summary="Build a downloadable zip archive of a whole project", - description=( - "Queues a background zip job and returns {uid, status_url}. Poll the status_url with " - "zip_status until status is 'done', then give the user the download_url." - ), - params=(path("project_slug", "Project slug or uid."),), - requires_auth=False, - ), - Action( - name="project_files_zip", - method="POST", - path="/projects/{project_slug}/files/zip", - summary="Build a downloadable zip archive of a project subdirectory", - description=( - "Queues a background zip job for a subpath inside the project and returns " - "{uid, status_url}. Poll the status_url with zip_status until status is 'done', " - "then give the user the download_url. When path is empty the whole project is zipped." - ), - params=( - path("project_slug", "Project slug or uid."), - query("path", "Relative subpath inside the project to zip; empty for the whole project."), - ), - requires_auth=False, - ), - Action( - name="zip_status", - method="GET", - path="/zips/{uid}", - summary="Check a zip job and obtain its download link once finished", - description=( - "Returns the job status and stats. When status is 'done', download_url points at the " - "ready archive; while 'pending' or 'running', poll again shortly." - ), - params=(path("uid", "Zip job uid returned by zip_project."),), - requires_auth=False, - ), - Action( - name="fork_project", - method="POST", - path="/projects/{project_slug}/fork", - summary="Fork a project into a new project owned by the current user", - description=( - "Queues a background fork job and returns {uid, status_url}. Poll the status_url with " - "fork_status until status is 'done', then give the user the project_url of the new fork." - ), - params=( - path("project_slug", "Slug or uid of the project to fork."), - body("title", "Title for the new forked project.", required=True), - ), - requires_auth=True, - ), - Action( - name="fork_status", - method="GET", - path="/forks/{uid}", - summary="Check a fork job and obtain the new project once finished", - description=( - "Returns the job status. When status is 'done', project_url points at the new forked " - "project; while 'pending' or 'running', poll again shortly." - ), - params=(path("uid", "Fork job uid returned by fork_project."),), - requires_auth=False, - ), - Action( - name="seo_diagnostics", - method="POST", - path="/tools/seo/run", - summary="Run an SEO audit on a URL or sitemap", - description=( - "Queues a background SEO diagnostics job and returns {uid, status_url}. Poll the " - "status_url with seo_status until status is 'done', then share the score, grade and " - "report_url. mode='url' audits a single page; mode='sitemap' crawls up to max_pages." - ), - params=( - body("url", "The page URL or sitemap.xml URL to audit.", required=True), - body("mode", "'url' for a single page or 'sitemap' to crawl a sitemap."), - body("max_pages", "Maximum pages to crawl in sitemap mode (1-50)."), - ), - requires_auth=False, - ), - Action( - name="seo_status", - method="GET", - path="/tools/seo/{uid}", - summary="Check an SEO audit and obtain its score once finished", - description=( - "Returns the audit status. When status is 'done', score, grade and report_url are " - "populated; while 'pending' or 'running', poll again shortly." - ), - params=(path("uid", "SEO job uid returned by seo_diagnostics."),), - requires_auth=False, - ), - Action( - name="seo_report", - method="GET", - path="/tools/seo/{uid}/report", - summary="Read a finished SEO audit report", - description=( - "Returns the full audit for a finished SEO job: overall score and grade, per-category " - "scores, per-check results, per-page details, and site-wide checks. Use it after " - "seo_status reports status 'done' to explain what passed, what failed, and how to improve." - ), - params=(path("uid", "SEO job uid returned by seo_diagnostics."),), - requires_auth=False, - ), - Action( - name="seo_meta_status", - method="GET", - path="/tools/seo-meta/{target_type}/{target_uid}", - summary="Read the SEO metadata generated for a content item", - description=( - "Returns the clean SEO title, description and keywords for a published content " - "item. target_type is one of post, project, gist, news or issue and target_uid " - "is its uid (or issue number). When the AI value is not ready yet a plain-content " - "default is returned with status 'pending'." - ), - params=( - path("target_type", "One of post, project, gist, news or issue."), - path("target_uid", "The content uid (or issue number)."), - ), - requires_auth=False, - ), - Action( - name="deepsearch", - method="POST", - path="/tools/deepsearch/run", - summary="Start a deep multi-agent web research job", - description=( - "Queues a background DeepSearch job and returns {uid, status_url}. Poll the " - "status_url with deepsearch_status until status is 'done', then share the " - "score, confidence and session_url. depth (1-4) and max_pages (1-30) control " - "how widely it crawls." - ), - params=( - body("query", "The research question to investigate.", required=True), - body("depth", "Research depth 1-4 (default 2)."), - body("max_pages", "Maximum sources to crawl 1-30 (default 12)."), - ), - requires_auth=False, - ), - Action( - name="deepsearch_status", - method="GET", - path="/tools/deepsearch/{uid}", - summary="Check a DeepSearch job and obtain its metrics once finished", - description=( - "Returns the research status. When status is 'done', score, confidence, " - "source_diversity and session_url are populated; otherwise poll again shortly." - ), - params=(path("uid", "DeepSearch job uid returned by deepsearch."),), - requires_auth=False, - ), - Action( - name="deepsearch_session", - method="GET", - path="/tools/deepsearch/{uid}/session", - summary="Read a finished DeepSearch report", - description=( - "Returns the full cited report for a finished DeepSearch: summary, findings, " - "gaps, sources and metrics." - ), - params=(path("uid", "DeepSearch job uid returned by deepsearch."),), - requires_auth=False, - ), - Action( - name="search_users", - method="GET", - path="/profile/search", - summary="Search for users by name", - params=(query("q", "Search query."),), - ), - Action( - name="my_profile", - method="GET", - path="/profile", - summary="View the current user's own profile", - params=(query("tab", "Profile tab."),), - ), - Action( - name="view_profile", - method="GET", - path="/profile/{username}", - summary="View a user profile", - requires_auth=False, - params=( - path("username", "Username to view."), - query("tab", "Profile tab."), - ), - ), - Action( - name="update_profile", - method="POST", - path="/profile/update", - summary="Update the current user's profile", - params=( - body("bio", "Profile biography."), - body("location", "Location."), - body("git_link", "Git profile link."), - body("website", "Personal website."), - ), - ), - Action( - name="regenerate_api_key", - method="POST", - path="/profile/regenerate-api-key", - summary="Issue a new API key and invalidate the current one", - description=( - "Returns the new api_key. Warning: this immediately invalidates any key currently " - "used for authentication, so confirm with the user before calling it." - ), - params=( - body( - "confirm", - "Must be true, set only after the user has explicitly confirmed.", - required=True, - ), - ), - ), - Action( - name="regenerate_avatar", - method="POST", - path="/profile/{username}/regenerate-avatar", - summary="Generate a new random avatar for a user", - description=( - "Replaces the user's avatar with a freshly generated random one and returns the new " - "avatar_url. Owner-or-admin only. Irreversible: the previous avatar is gone for good, " - "so confirm with the user before calling it." - ), - params=( - path("username", "Username whose avatar to regenerate."), - confirm(), - ), - ), - Action( - name="list_messages", - method="GET", - path="/messages", - summary="View direct message conversations", - params=( - query("with_uid", "Open a conversation with a user uid."), - query("search", "Search conversations."), - ), - ), - Action( - name="search_message_users", - method="GET", - path="/messages/search", - summary="Search users to message", - params=(query("q", "Search query."),), - ), - Action( - name="send_message", - method="POST", - path="/messages/send", - summary="Send a direct message", - params=( - body("content", "Message body.", required=True), - body("receiver_uid", "Recipient user uid.", required=True), - body("attachment_uids", ATTACHMENTS), - ), - ), - Action( - name="list_notifications", - method="GET", - path="/notifications", - summary="List notifications", - params=(query("before", "Pagination cursor."),), - ), - Action( - name="notification_counts", - method="GET", - path="/notifications/counts", - summary="Get unread notification and message counts", - requires_auth=False, - ), - Action( - name="open_notification", - method="GET", - path="/notifications/open/{notification_uid}", - summary="Open a notification and follow it", - params=(path("notification_uid", "Notification uid."),), - ), - Action( - name="mark_notification_read", - method="POST", - path="/notifications/mark-read/{notification_uid}", - summary="Mark a single notification read", - params=(path("notification_uid", "Notification uid."),), - ), - Action( - name="mark_all_notifications_read", - method="POST", - path="/notifications/mark-all-read", - summary="Mark all notifications read", - ), - Action( - name="vote", - method="POST", - path="/votes/{target_type}/{target_uid}", - summary="Cast or toggle a vote on a target", - description="Re-sending the same value removes the vote. Returns the net/up/down tally.", - ajax=True, - params=( - path("target_type", TARGET_TYPE), - path( - "target_uid", - "Uid of the target, copied from a listing response; do not invent it.", - ), - body( - "value", - "Vote value: 1 to upvote, -1 to downvote (re-send to remove).", - required=True, - ), - ), - ), - Action( - name="react", - method="POST", - path="/reactions/{target_type}/{target_uid}", - summary="Toggle an emoji reaction on a target", - description="Re-sending the same emoji removes it. Returns reaction counts.", - ajax=True, - params=( - path("target_type", TARGET_TYPE), - path( - "target_uid", - "Uid of the target, copied from a listing response; do not invent it.", - ), - body("emoji", "One of the allowed reaction emoji.", required=True), - ), - ), - Action( - name="list_bookmarks", - method="GET", - path="/bookmarks/saved", - summary="List saved bookmarks", - params=(query("before", "Pagination cursor."),), - ), - Action( - name="toggle_bookmark", - method="POST", - path="/bookmarks/{target_type}/{target_uid}", - summary="Toggle a bookmark on a target", - description="Re-sending removes the bookmark. Returns {saved: true|false}.", - ajax=True, - params=( - path("target_type", TARGET_TYPE), - path( - "target_uid", - "Uid of the target, copied from a listing response; do not invent it.", - ), - ), - ), - Action( - name="vote_poll", - method="POST", - path="/polls/{poll_uid}/vote", - summary="Vote in a poll", - description="Casts or changes your vote on a poll option. Returns the option tallies.", - ajax=True, - params=( - path("poll_uid", "Poll uid."), - body("option_uid", "Chosen option uid.", required=True), - ), - ), - Action( - name="follow_user", - method="POST", - path="/follow/{username}", - summary="Follow a user", - params=(path("username", "Username to follow."),), - ), - Action( - name="unfollow_user", - method="POST", - path="/follow/unfollow/{username}", - summary="Unfollow a user", - params=(path("username", "Username to unfollow."),), - ), - Action( - name="block_user", - method="POST", - path="/block/{username}", - summary="Block a user so their posts, comments and messages are hidden and they cannot notify you", - params=(path("username", "Username to block."),), - ), - Action( - name="unblock_user", - method="POST", - path="/block/unblock/{username}", - summary="Unblock a user", - params=(path("username", "Username to unblock."),), - ), - Action( - name="mute_user", - method="POST", - path="/mute/{username}", - summary="Mute a user so they can no longer create notifications for you", - params=(path("username", "Username to mute."),), - ), - Action( - name="unmute_user", - method="POST", - path="/mute/unmute/{username}", - summary="Unmute a user", - params=(path("username", "Username to unmute."),), - ), - Action( - name="list_followers", - method="GET", - path="/profile/{username}/followers", - summary="List the users who follow a profile", - requires_auth=False, - params=( - path("username", "Username whose followers to list."), - query("page", "Page number, 25 per page."), - ), - ), - Action( - name="list_following", - method="GET", - path="/profile/{username}/following", - summary="List the users a profile follows", - requires_auth=False, - params=( - path("username", "Username whose following to list."), - query("page", "Page number, 25 per page."), - ), - ), - Action( - name="list_media", - method="GET", - path="/profile/{username}", - summary="List a user's uploaded media, newest first", - requires_auth=False, - params=( - path("username", "Username whose media to list."), - query("tab", "Profile tab (defaults to media for this action)."), - query("page", "Page number, 24 per page."), - ), - ), - Action( - name="delete_media", - method="POST", - path="/media/{uid}/delete", - summary="Delete an uploaded media attachment (your own, or anyone's when you are an administrator)", - description=( - "Removes the attachment from the user's Media tab and from any post, project, or " - "gist where it was attached. Soft delete (restorable from admin trash). Show the user " - "the exact media, get explicit confirmation, then call again with confirm=true." - ), - requires_auth=True, - params=(path("uid", "Attachment uid to delete."), confirm()), - ), - Action( - name="view_leaderboard", - method="GET", - path="/leaderboard", - summary="View the leaderboard", - requires_auth=False, - ), - Action( - name="list_issues", - method="GET", - path="/issues", - summary="List issue tickets from the Gitea tracker", - requires_auth=False, - params=( - query("state", "Filter by state: open, closed, or all."), - query("page", "1-based page number."), - ), - ), - Action( - name="create_issue", - method="POST", - path="/issues/create", - summary="Report an issue. It is rewritten by AI and filed on the tracker.", - description=( - "Queues a background issue creation job and returns {uid, status_url}. Poll the " - "status_url with issue_job_status until status is 'done', then show the user the " - "issue_url pointing at the filed ticket." - ), - params=( - body("title", "Issue title.", required=True), - body("description", "Issue description.", required=True), - ), - ), - Action( - name="planning_report_generate", - method="POST", - path="/issues/planning", - summary="Queue an admin planning report of open tickets.", - description=( - "Queues a background job that builds a phased, ordered markdown implementation " - "document for open tickets and returns {uid, status_url}. Pass 'numbers' as a " - "comma-separated list of issue numbers to plan only those tickets; omit it to plan " - "every open ticket. Each ticket's full description is reproduced verbatim, both " - "inline and in a 'Source Tickets (verbatim)' appendix, so the document is " - "self-contained. Poll the status_url until status is 'done', then show the markdown " - "and the download_url. Admin only." - ), - params=( - body( - "numbers", - "Comma-separated issue numbers to include (e.g. '1,4,7'). Omit for all open tickets.", - ), - ), - requires_auth=True, - requires_admin=True, - ), - Action( - name="issue_job_status", - method="GET", - path="/issues/jobs/{uid}", - summary="Poll an issue creation job and obtain the filed issue ticket URL once finished", - description=( - "Returns the job status. When status is 'done', issue_url and number are populated; " - "while 'pending' or 'running', poll again shortly." - ), - params=(path("uid", "Issue job uid returned by create_issue."),), - requires_auth=False, - ), - Action( - name="view_issue", - method="GET", - path="/issues/{number}", - summary="View an issue ticket and its developer updates", - requires_auth=False, - params=(path("number", "The issue ticket number from a /issues listing."),), - ), - Action( - name="comment_issue", - method="POST", - path="/issues/{number}/comment", - summary="Post a comment on an issue ticket, attributed to the current user", - params=( - path("number", "The issue ticket number."), - body("body", "Comment text.", required=True), - ), - ), - Action( - name="set_issue_status", - method="POST", - path="/issues/{number}/status", - summary="Change an issue ticket status (admin only)", - requires_admin=True, - params=( - path("number", "The issue ticket number."), - body("status", "New status: open or closed.", required=True), - ), - ), - Action( - name="list_issue_attachments", - method="GET", - path="/issues/{number}/attachments", - summary="List the files attached to an issue ticket", - requires_auth=False, - params=(path("number", "The issue ticket number."),), - ), - Action( - name="add_issue_attachment", - method="POST", - path="/issues/{number}/attachments", - summary="Attach already-uploaded files to an open issue ticket", - description=( - "Upload files first with upload_file, then pass their uids here. Only works while " - "the issue is open; the files are mirrored to the tracker." - ), - params=( - path("number", "The issue ticket number."), - body("attachment_uids", ATTACHMENTS, required=True), - ), - ), - Action( - name="delete_issue_attachment", - method="DELETE", - path="/issues/{number}/attachments/{uid}", - summary="Delete a file from an open issue ticket (your own, or any when administrator). Soft delete, confirmation required", - params=( - path("number", "The issue ticket number."), - path("uid", "The attachment uid."), - confirm(), - ), - ), - Action( - name="add_comment_attachment", - method="POST", - path="/issues/{number}/comments/{cid}/attachments", - summary="Attach already-uploaded files to an issue comment (issue must be open)", - params=( - path("number", "The issue ticket number."), - path("cid", "The Gitea comment id."), - body("attachment_uids", ATTACHMENTS, required=True), - ), - ), - Action( - name="delete_comment_attachment", - method="DELETE", - path="/issues/{number}/comments/{cid}/attachments/{uid}", - summary="Delete a file from an issue comment (your own, or any when administrator). Soft delete, confirmation required", - params=( - path("number", "The issue ticket number."), - path("cid", "The Gitea comment id."), - path("uid", "The attachment uid."), - confirm(), - ), - ), - Action( - name="list_gists", - method="GET", - path="/gists", - summary="List gists", - requires_auth=False, - params=( - query("language", "Filter by language."), - query("user_uid", "Filter by owner uid."), - query("search", "Search gist title, description, and author username."), - query("before", "Pagination cursor."), - ), - ), - Action( - name="view_gist", - method="GET", - path="/gists/{gist_slug}", - summary="View a gist by slug", - requires_auth=False, - params=( - path( - "gist_slug", - "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", - ), - ), - ), - Action( - name="create_gist", - method="POST", - path="/gists/create", - summary="Create a gist", - params=( - body("title", "Gist title.", required=True), - body("source_code", "Gist source code.", required=True), - body("description", "Gist description."), - body("language", "Programming language."), - body("attachment_uids", ATTACHMENTS), - ), - ), - Action( - name="edit_gist", - method="POST", - path="/gists/edit/{gist_slug}", - summary="Edit a gist", - params=( - path( - "gist_slug", - "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", - ), - body("title", "Gist title.", required=True), - body("source_code", "Gist source code.", required=True), - body("description", "Gist description."), - body("language", "Programming language."), - ), - ), - Action( - name="delete_gist", - method="POST", - path="/gists/delete/{gist_slug}", - summary="Delete a gist (your own, or any gist when you are an administrator). Soft delete, confirmation required", - params=( - path( - "gist_slug", - "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", - ), - confirm(), - ), - ), - Action( - name="list_news", - method="GET", - path="/news", - summary="List news articles", - requires_auth=False, - params=(query("before", "Pagination cursor."),), - ), - Action( - name="view_news", - method="GET", - path="/news/{news_slug}", - summary="View a news article by slug", - requires_auth=False, - params=( - path( - "news_slug", - "Exact news slug copied from a /news/... link in a listing response; do not build it from the title.", - ), - ), - ), - Action( - name="upload_file", - method="POST", - path="/uploads/upload", - summary="Upload a local file and obtain its attachment uid", - params=(upload("file", "Local filesystem path of the file to upload."),), - ), - Action( - name="attach_url", - method="POST", - path="/uploads/upload-url", - summary="Download a file from a public URL and store it as an attachment, returning its uid", - description=( - "Fetches the file at the given URL on the server (size-capped, SSRF-guarded) and stores it " - "as an attachment exactly like an upload, returning {uid, url, mime_type, ...}. Pass the " - "returned uid in attachment_uids when you create_post, create_project, create_gist, " - "create_comment, create_issue, or send_message to attach it. The file type is taken from the " - "URL or its Content-Type; supply 'filename' (with an allowed extension) when the URL has no " - "usable name. Use this to attach an image or file straight from the internet." - ), - params=( - body("url", "Public http(s) URL of the file to download and attach.", required=True), - body( - "filename", - "Optional filename with an allowed extension, used when the URL has no clear name.", - ), - ), - ), - Action( - name="delete_attachment", - method="DELETE", - path="/uploads/delete/{attachment_uid}", - summary="Delete an uploaded attachment (your own, or anyone's when you are an administrator). Soft delete, confirmation required", - params=(path("attachment_uid", "Uid of the attachment."), confirm()), - ), - Action( - name="admin_overview", - method="GET", - path="/admin", - summary="View the admin overview", - requires_admin=True, - ), - Action( - name="site_analytics", - method="GET", - path="/admin/analytics", - summary="Site-wide aggregate analytics in one call (admin only)", - description=( - "Returns JSON: total members, active users in the last 24h/7d/30d, signed_in_now, " - "new signups (24h/7d/30d), content totals (posts, comments, gists, projects, " - "news), and top authors. Use this for any 'how many'/'how active'/count question " - "instead of paging through admin_list_users. signed_in_now counts members holding an " - "unexpired session (logged in within the session lifetime, default 7-30 days), NOT a " - "real-time online count - see signed_in_definition in the response and never report it " - "as 'currently online' or 'logged in right now'." - ), - params=(query("top_n", "How many top authors to include (1-50)."),), - requires_admin=True, - ), - Action( - name="ai_usage", - method="GET", - path="/admin/ai-usage/data", - summary="AI gateway usage, cost, latency, and reliability metrics (admin only)", - description=( - "Returns JSON for a bounded window: request volume and throughput, token usage with " - "averages and percentiles, latency (upstream, gateway overhead, queue wait, connect), " - "error rates by category, cost in USD (per model, per caller, input vs output, projected " - "monthly burn, caching savings), caller behavior, and an hourly breakdown. Use this for " - "any question about AI spend, token consumption, gateway performance, or errors." - ), - params=( - query("hours", "Lookback window in hours (1-168, default 48)."), - query("top_n", "How many rows in each top-N breakdown (default 10)."), - ), - requires_admin=True, - ), - Action( - name="audit_log", - method="GET", - path="/admin/audit-log", - summary="Query the platform audit trail with filters (admin only)", - description=( - "Returns JSON: a paginated, newest-first list of audit events (every state-changing " - "action on the platform, with actor, origin, target, old/new value, and result), plus " - "pagination, the active filters, and an 'options' object listing every available value " - "for each filter (category, event_key, actor_role, origin, result) so you can both " - "filter and discover valid filter values from a single call. Use this for any audit, " - "history, 'who did X', 'what changed', or 'show denied/failed actions' question. Filter " - "by event_key (exact), category, actor_role, actor_uid, origin (web, api, devii, cli, " - "service, scheduler), or result (success, failure, denied); narrow by date_from/date_to " - "(ISO date bounds, inclusive) or free-text q over summary, event key, and target." - ), - params=( - query("page", "Page number (1-based)."), - query("event_key", "Filter by exact event key (e.g. admin.setting.update)."), - query("category", "Filter by category (auth, content, admin, container, ...)."), - query("actor_role", "Filter by actor role at action time."), - query("actor_uid", "Filter by acting user uid."), - query("origin", "Filter by origin (web, api, devii, cli, service, scheduler)."), - query("result", "Filter by result (success, failure, denied)."), - query("q", "Free-text search over summary, event key, and target."), - query("date_from", "ISO date lower bound (inclusive)."), - query("date_to", "ISO date upper bound (inclusive)."), - ), - requires_admin=True, - ), - Action( - name="audit_event", - method="GET", - path="/admin/audit-log/{uid}", - summary="Get one audit event with its related-object links (admin only)", - description=( - "Returns JSON: the full audit event row plus every related-object link (actor, target, " - "parent, project, instance, setting, ...). Use after audit_log to inspect a single event " - "in detail." - ), - params=(path("uid", "Audit event uid."),), - requires_admin=True, - ), - Action( - name="bot_monitor", - method="GET", - path="/admin/bots/data", - summary="Live screenshot monitor of every running bot persona (admin only)", - description=( - "Returns JSON: one entry per running bot slot with its username, persona, current " - "action and status text, last page url, frame age in seconds, whether it is active, " - "and a frame_url to the latest low-quality screenshot. Use this to see what the bot " - "fleet is doing right now. Frames live only in the worker running the Bots service." - ), - requires_admin=True, - ), - Action( - name="admin_list_users", - method="GET", - path="/admin/users", - summary="List users for administration", - params=(query("page", "Page number."),), - requires_admin=True, - ), - Action( - name="admin_set_user_role", - method="POST", - path="/admin/users/{uid}/role", - summary="Set a user's role", - params=( - path("uid", "User uid."), - body("role", "New role.", required=True), - ), - requires_admin=True, - ), - Action( - name="admin_set_user_password", - method="POST", - path="/admin/users/{uid}/password", - summary="Set a user's password", - params=( - path("uid", "User uid."), - body("password", "New password.", required=True), - ), - requires_admin=True, - ), - Action( - name="admin_toggle_user", - method="POST", - path="/admin/users/{uid}/toggle", - summary="Toggle a user's active state", - params=(path("uid", "User uid."),), - requires_admin=True, - ), - Action( - name="admin_get_settings", - method="GET", - path="/admin/settings", - summary="View site settings", - requires_admin=True, - ), - Action( - name="admin_save_settings", - method="POST", - path="/admin/settings", - summary="Save site settings", - params=( - body("site_name", "Site name."), - body("site_description", "Site description."), - body("site_tagline", "Site tagline."), - body("max_upload_size_mb", "Maximum upload size in megabytes."), - body("allowed_file_types", "Allowed file types."), - body("max_attachments_per_resource", "Maximum attachments per resource."), - body("rate_limit_per_minute", "Rate limit per minute."), - body("rate_limit_window_seconds", "Rate limit window in seconds."), - body("session_max_age_days", "Session maximum age in days."), - body("session_remember_days", "Remember-me duration in days."), - body("registration_open", "Whether registration is open."), - body("maintenance_mode", "Whether maintenance mode is on."), - body("maintenance_message", "Maintenance message."), - ), - requires_admin=True, - ), - Action( - name="admin_list_news", - method="GET", - path="/admin/news", - summary="List news for administration", - params=(query("page", "Page number."),), - requires_admin=True, - ), - Action( - name="admin_toggle_news", - method="POST", - path="/admin/news/{uid}/toggle", - summary="Toggle a news article", - params=(path("uid", "News uid."),), - requires_admin=True, - ), - Action( - name="admin_publish_news", - method="POST", - path="/admin/news/{uid}/publish", - summary="Publish a news article", - params=(path("uid", "News uid."),), - requires_admin=True, - ), - Action( - name="admin_landing_news", - method="POST", - path="/admin/news/{uid}/landing", - summary="Set a news article as landing content", - params=(path("uid", "News uid."),), - requires_admin=True, - ), - Action( - name="admin_delete_news", - method="POST", - path="/admin/news/{uid}/delete", - summary="Delete a news article (admin only). Soft delete, confirmation required", - params=(path("uid", "News uid."), confirm()), - requires_admin=True, - ), - Action( - name="admin_list_services", - method="GET", - path="/admin/services", - summary="View managed services", - requires_admin=True, - ), - Action( - name="admin_services_data", - method="GET", - path="/admin/services/data", - summary="Get live status, metrics, and log tail for every background service", - requires_admin=True, - ), - Action( - name="admin_service_status", - method="GET", - path="/admin/services/{name}/data", - summary="Get live status, metrics, and log tail for one background service", - params=(path("name", "Service name (e.g. news, bots, openai)."),), - requires_admin=True, - ), - Action( - name="admin_start_service", - method="POST", - path="/admin/services/{name}/start", - summary="Start a managed service", - params=(path("name", "Service name."),), - requires_admin=True, - ), - Action( - name="admin_stop_service", - method="POST", - path="/admin/services/{name}/stop", - summary="Stop a managed service", - params=(path("name", "Service name."),), - requires_admin=True, - ), - Action( - name="admin_run_service", - method="POST", - path="/admin/services/{name}/run", - summary="Run a managed service once", - params=(path("name", "Service name."),), - requires_admin=True, - ), - Action( - name="admin_clear_service_logs", - method="POST", - path="/admin/services/{name}/clear-logs", - summary="Clear a managed service's logs", - params=(path("name", "Service name."),), - requires_admin=True, - ), - Action( - name="admin_config_service", - method="POST", - path="/admin/services/{name}/config", - summary="Update a managed service's configuration", - params=(path("name", "Service name."),), - freeform_body=True, - requires_admin=True, - ), - Action( - name="admin_user_ai_usage", - method="GET", - path="/admin/users/{uid}/ai-usage", - summary="Get a user's AI gateway usage (admin only)", - description="Returns per-user AI usage data including token counts, cost, and request volume for the given lookback window.", - params=( - path("uid", "User uid."), - query("hours", "Lookback window in hours (default 24)."), - ), - requires_admin=True, - ), - Action( - name="admin_reset_user_ai_quota", - method="POST", - path="/admin/users/{uid}/reset-ai-quota", - summary="Reset a user's AI quota (admin only)", - description="Clears the AI quota ledger for a specific user, allowing them to use AI features again.", - params=(path("uid", "User uid."), confirm()), - requires_admin=True, - ), - Action( - name="admin_media_purge", - method="POST", - path="/admin/media/{uid}/purge", - summary="Permanently remove soft-deleted media (admin only)", - description="Permanently deletes a soft-deleted attachment from the database and filesystem.", - params=(path("uid", "Attachment uid to purge."), confirm()), - requires_admin=True, - ), - Action( - name="admin_reset_guest_ai_quota", - method="POST", - path="/admin/ai-quota/reset-guests", - summary="Reset all guest AI quotas (admin only)", - description="Clears the AI quota ledger for all guest sessions.", - params=(confirm(),), - requires_admin=True, - ), - Action( - name="admin_reset_all_ai_quota", - method="POST", - path="/admin/ai-quota/reset-all", - summary="Reset ALL AI quotas including member quotas (admin only)", - description="Clears the AI quota ledger for every user and guest. Use with caution.", - params=(confirm(),), - requires_admin=True, - ), - Action( - name="backups_overview", - method="GET", - path="/admin/backups/data", - summary="Backup dashboard: storage usage, backups, and schedules (admin only)", - description=( - "Returns JSON: per-path storage usage (database, uploads, attachments, project files, " - "keys, zips, deepsearch, container workspaces, backups), total data-directory size, total " - "stored-backup size and count, disk usage (total/used/free/percent), every backup archive " - "(target, status, size, file count, sha256 checksum, created/completed time, download_url), " - "and every configured backup schedule. Use this for any question about backups, data " - "footprint, disk space, or what is scheduled." - ), - requires_admin=True, - ), - Action( - name="backup_run", - method="POST", - path="/admin/backups/run", - summary="Start a backup of a data target (admin only)", - description=( - "Enqueues an async backup job and returns its job uid and status_url. Target is one of: " - "database (consistent SQLite snapshot plus Devii databases), uploads (attachments and " - "project files), keys (VAPID keys and config), or full (database, uploads, and keys in one " - "archive). Poll backup_status with the returned uid until status is done, then read the " - "download_url. The job runs off the request path and never blocks the server." - ), - params=( - body("target", "One of database, uploads, keys, full.", required=True), - ), - requires_admin=True, - ), - Action( - name="backup_status", - method="GET", - path="/admin/backups/jobs/{uid}", - summary="Check the status of a backup job (admin only)", - description=( - "Returns JSON for one backup job: status (pending, running, done, failed), target, " - "backup_uid, download_url (when done), sha256, archive size, file count, and timestamps. " - "Poll this after backup_run." - ), - params=(path("uid", "Backup job uid returned by backup_run."),), - requires_admin=True, - ), - Action( - name="backup_delete", - method="POST", - path="/admin/backups/{uid}/delete", - summary="Permanently delete a backup archive (admin only)", - description=( - "Removes a backup archive file and its record. This is irreversible and reclaims disk " - "space. The uid is a backup uid (from backups_overview), not a job uid." - ), - params=(path("uid", "Backup uid to delete."), confirm()), - requires_admin=True, - ), - Action( - name="backup_schedule_create", - method="POST", - path="/admin/backups/schedules/create", - summary="Create a recurring backup schedule (admin only)", - description=( - "Creates a schedule that fires backups automatically. kind is 'interval' (use every_seconds, " - "minimum 60) or 'cron' (use a 5-field cron expression in 'minute hour dom month dow'). " - "keep_last rotates older backups of this schedule, keeping only the newest N (0 keeps all). " - "target is one of database, uploads, keys, full." - ), - params=( - body("name", "Human-readable schedule name.", required=True), - body("target", "One of database, uploads, keys, full.", required=True), - body("kind", "interval or cron.", required=True), - body("every_seconds", "Seconds between runs when kind=interval (min 60)."), - body("cron", "Cron expression when kind=cron, e.g. '0 3 * * *'."), - body("keep_last", "Keep only the newest N backups of this schedule (0 = all)."), - ), - requires_admin=True, - ), - Action( - name="backup_schedule_delete", - method="POST", - path="/admin/backups/schedules/{uid}/delete", - summary="Delete a backup schedule (admin only)", - description=( - "Removes a backup schedule so it stops firing. Existing backup archives are kept; only the " - "schedule is deleted." - ), - params=(path("uid", "Backup schedule uid."), confirm()), - requires_admin=True, - ), - Action( - name="restore_media", - method="POST", - path="/media/{uid}/restore", - summary="Restore a soft-deleted media attachment (admin only)", - description="Restores a previously soft-deleted attachment, making it visible again.", - params=(path("uid", "Attachment uid to restore."),), - requires_admin=True, - ), - Action( - name="db_list_tables", - method="GET", - path="/dbapi/tables", - summary="List database tables exposed by the database API (primary administrator only)", - description=( - "Returns every table reachable through the database API with its row count and " - "whether it uses soft deletes. Use this to discover what data exists before " - "querying or designing a query." - ), - params=(), - requires_admin=True, - requires_primary_admin=True, - ), - Action( - name="db_table_schema", - method="GET", - path="/dbapi/{table}/schema", - summary="Show a table's columns and types (primary administrator only)", - description="Returns the column names, types, row count, and soft-delete flag for one table.", - params=(path("table", "Table name (from db_list_tables)."),), - requires_admin=True, - requires_primary_admin=True, - ), - Action( - name="db_list_rows", - method="GET", - path="/dbapi/{table}", - summary="List rows of a table with keyset pagination (primary administrator only)", - description=( - "Browses rows newest-first. Soft-deleted rows are excluded unless include_deleted is " - "true. For filtered or joined questions prefer db_query or db_design_query." - ), - params=( - path("table", "Table name."), - query("limit", "Maximum rows (1-500, default 25)."), - query("search", "Free-text search over common text columns."), - query("before", "Keyset cursor: return rows older than this created_at/id value."), - Param( - name="include_deleted", - location="query", - description="Include soft-deleted rows.", - required=False, - type="boolean", - ), - ), - requires_admin=True, - requires_primary_admin=True, - ), - Action( - name="db_get_row", - method="GET", - path="/dbapi/{table}/{key}/{value}", - summary="Fetch one row by a key column (primary administrator only)", - description="Returns a single row where key column equals value (key is usually 'uid').", - params=( - path("table", "Table name."), - path("key", "Key column to match (usually 'uid')."), - path("value", "Value of the key column."), - ), - requires_admin=True, - requires_primary_admin=True, - ), - Action( - name="db_query", - method="POST", - path="/dbapi/query", - summary="Run a read-only SQL SELECT and return rows (primary administrator only)", - description=( - "Executes a SINGLE validated SELECT statement read-only and returns the rows. Only " - "SELECT is allowed; INSERT/UPDATE/DELETE/DDL are rejected. The database API is " - "read-only and cannot change data in any way. The response may include a 'suspicious' " - "list (e.g. a SELECT with no WHERE/JOIN/LIMIT that scans a whole table); when present, " - "surface that warning to the user before trusting the results." - ), - params=( - body("sql", "A single SELECT statement.", required=True), - body("dialect", "Optional source SQL dialect (default sqlite)."), - ), - requires_admin=True, - requires_primary_admin=True, - read_only=True, - ), - Action( - name="db_design_query", - method="POST", - path="/dbapi/nl", - summary="Design a SQL SELECT from a natural-language question (primary administrator only)", - description=( - "Turns a plain-language question about one table into a validated read-only SELECT. " - "It auto-adds 'deleted_at IS NULL' for soft-delete tables unless apply_soft_delete is " - "false. By default it only returns the SQL; pass execute=true to also run it read-only " - "and return rows. Show the user the SQL and any 'suspicious' notes." - ), - params=( - body("question", "The natural-language request.", required=True), - body("table", "Target table the question is about.", required=True), - Param( - name="apply_soft_delete", - location="body", - description="Add deleted_at IS NULL for soft-delete tables (default true).", - required=False, - type="boolean", - ), - body("dialect", "Optional target SQL dialect (default sqlite)."), - Param( - name="execute", - location="body", - description="Also run the validated query read-only and return rows.", - required=False, - type="boolean", - ), - ), - requires_admin=True, - requires_primary_admin=True, - read_only=True, - ), - Action( - name="gateway_providers", - method="GET", - path="/admin/gateway/providers", - summary="List OpenAI gateway providers (admin only)", - description=( - "Returns JSON: the named upstream providers used by model routes, plus the implicit " - "'default' provider read from the gateway service config." - ), - handler="http", - requires_admin=True, - read_only=True, - ), - Action( - name="gateway_provider_set", - method="POST", - path="/admin/gateway/providers", - summary="Create or update a gateway provider (admin only)", - description=( - "Adds or updates a named upstream provider. base_url is the OpenAI-compatible " - "chat-completions endpoint; the embeddings endpoint is derived from it." - ), - handler="http", - requires_admin=True, - params=( - body("name", "Provider name (letters, numbers, hyphen, underscore).", required=True), - body("base_url", "Chat-completions endpoint URL."), - body("api_key", "Upstream API key."), - Param( - name="is_active", - location="body", - description="Whether the provider is active ('1' or '0').", - required=False, - type="boolean", - ), - ), - ), - Action( - name="gateway_provider_delete", - method="DELETE", - path="/admin/gateway/providers/{name}", - summary="Delete a gateway provider (admin only, confirmation required)", - handler="http", - requires_admin=True, - params=(path("name", "Provider name."), confirm()), - ), - Action( - name="gateway_models", - method="GET", - path="/admin/gateway/models", - summary="List OpenAI gateway model routes (admin only)", - description=( - "Returns JSON: every source-model route with its provider, target model, kind " - "(chat/embed), optional vision model, context window, and per-model pricing economy." - ), - handler="http", - requires_admin=True, - read_only=True, - ), - Action( - name="gateway_model_set", - method="POST", - path="/admin/gateway/models", - summary="Create or update a gateway model route (admin only)", - description=( - "Maps a requested source_model onto a provider + target_model, each with its own " - "pricing. kind is 'chat' or 'embed'. A vision_model adds image-to-text augmentation " - "for chat routes. Prices are USD per 1,000,000 tokens; chat uses cache-hit/cache-miss/" - "output, embeddings use input, vision uses input/output." - ), - handler="http", - requires_admin=True, - params=( - body("source_model", "Model name clients request.", required=True), - body("provider", "Provider name, or blank for the default upstream."), - body("target_model", "Model name sent upstream.", required=True), - body("kind", "Route kind: 'chat' or 'embed'."), - body("vision_provider", "Provider for image description, or blank for the route provider."), - body("vision_model", "Vision model name (blank disables the merge)."), - Param(name="context_window", location="body", description="Max context tokens (0 = unknown).", required=False, type="integer"), - Param(name="price_cache_hit_per_m", location="body", description="USD per 1M cache-hit input tokens.", required=False, type="number"), - Param(name="price_cache_miss_per_m", location="body", description="USD per 1M cache-miss input tokens.", required=False, type="number"), - Param(name="price_output_per_m", location="body", description="USD per 1M output tokens.", required=False, type="number"), - Param(name="price_input_per_m", location="body", description="USD per 1M input tokens (embed/vision).", required=False, type="number"), - Param(name="is_active", location="body", description="Whether the route is active ('1' or '0').", required=False, type="boolean"), - ), - ), - Action( - name="gateway_model_delete", - method="DELETE", - path="/admin/gateway/models/{source_model}", - summary="Delete a gateway model route (admin only, confirmation required)", - handler="http", - requires_admin=True, - params=(path("source_model", "Source model name."), confirm()), - ), - Action( - name="game_state", - method="GET", - path="/game/state", - summary="Get the current user's Code Farm state", - description=( - "Returns JSON for the signed-in user's Code Farm: coins, level/XP, CI tier, " - "plot grid (each plot's state, crop, remaining build seconds), and the plantable crops." - ), - handler="http", - requires_auth=True, - read_only=True, - ), - Action( - name="game_leaderboard", - method="GET", - path="/game/leaderboard", - summary="List the top Code Farm players", - handler="http", - requires_auth=False, - read_only=True, - ), - Action( - name="game_view_farm", - method="GET", - path="/game/farm/{username}", - summary="View another player's Code Farm", - handler="http", - requires_auth=False, - read_only=True, - params=(path("username", "The farm owner's username."),), - ), - Action( - name="game_plant", - method="POST", - path="/game/plant", - summary="Plant a crop in an empty plot on your Code Farm", - handler="http", - requires_auth=True, - params=( - Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), - body("crop", "Crop key, e.g. shell, python, webapp, api, rust, haskell, kernel.", required=True), - ), - ), - Action( - name="game_harvest", - method="POST", - path="/game/harvest", - summary="Harvest a finished build from a plot on your Code Farm", - handler="http", - requires_auth=True, - params=( - Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), - ), - ), - Action( - name="game_buy_plot", - method="POST", - path="/game/buy-plot", - summary="Buy a new plot on your Code Farm", - handler="http", - requires_auth=True, - ), - Action( - name="game_upgrade_ci", - method="POST", - path="/game/upgrade", - summary="Upgrade your Code Farm CI tier for faster builds", - handler="http", - requires_auth=True, - ), - Action( - name="game_water", - method="POST", - path="/game/farm/{username}/water", - summary="Water a growing build on another player's Code Farm", - handler="http", - requires_auth=True, - params=( - path("username", "The farm owner's username."), - Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), - ), - ), - Action( - name="game_steal", - method="POST", - path="/game/farm/{username}/steal", - summary="Steal a ready build from another player's Code Farm once its protection window has passed (limited to once per hour per neighbour)", - handler="http", - requires_auth=True, - params=( - path("username", "The farm owner's username."), - Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), - ), - ), - Action( - name="game_fertilize", - method="POST", - path="/game/fertilize", - summary="Spend coins to halve a growing build's remaining time on your Code Farm (cost scales with the build's harvest value, so it is a pure time-skip, never a profit)", - handler="http", - requires_auth=True, - params=( - Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), - ), - ), - Action( - name="game_daily", - method="POST", - path="/game/daily", - summary="Claim the daily Code Farm coin bonus (builds a streak)", - handler="http", - requires_auth=True, - ), - Action( - name="game_upgrade_perk", - method="POST", - path="/game/perk", - summary="Upgrade a permanent Code Farm perk (yield, growth, discount, xp)", - handler="http", - requires_auth=True, - params=( - body("perk", "Perk key: yield, growth, discount, or xp.", required=True), - ), - ), - Action( - name="game_claim_quest", - method="POST", - path="/game/quests/claim", - summary="Claim a completed daily Code Farm quest reward", - handler="http", - requires_auth=True, - params=( - body("quest", "Quest kind: plant, harvest, water, or earn.", required=True), - ), - ), - Action( - name="game_prestige", - method="POST", - path="/game/prestige", - summary="Refactor (prestige) your Code Farm for a permanent coin bonus and Stars (requires level 10)", - handler="http", - requires_auth=True, - ), - Action( - name="game_upgrade_legacy", - method="POST", - path="/game/legacy", - summary="Spend Stars on a permanent Legacy upgrade that survives refactor (autoharvest, multiplier, speed, plots, defense)", - handler="http", - requires_auth=True, - params=( - body( - "key", - "Legacy key: autoharvest, multiplier, speed, plots, or defense.", - required=True, - ), - ), - ), -) - -PLATFORM_CATALOG = Catalog(actions=ACTIONS) diff --git a/devplacepy/services/devii/actions/catalog/__init__.py b/devplacepy/services/devii/actions/catalog/__init__.py new file mode 100644 index 00000000..ed383016 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/__init__.py @@ -0,0 +1,52 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action, Catalog +from .admin import ADMIN_ACTIONS +from .auth import AUTH_ACTIONS +from .comments import COMMENTS_ACTIONS +from .dbapi import DBAPI_ACTIONS +from .engagement import ENGAGEMENT_ACTIONS +from .game import GAME_ACTIONS +from .gateway import GATEWAY_ACTIONS +from .gists import GIST_ACTIONS +from .issues import ISSUE_ACTIONS +from .jobs import JOB_ACTIONS +from .messages import MESSAGE_ACTIONS +from .news import NEWS_ACTIONS +from .notifications import NOTIFICATION_ACTIONS +from .posts import POSTS_ACTIONS +from .profile import PROFILE_ACTIONS +from .project_files import PROJECT_FILE_ACTIONS +from .projects import PROJECTS_ACTIONS +from .social import SOCIAL_ACTIONS +from .tools import TOOLS_ACTIONS +from .uploads import UPLOAD_ACTIONS + +ACTIONS: tuple[Action, ...] = ( + AUTH_ACTIONS + + POSTS_ACTIONS + + COMMENTS_ACTIONS + + PROJECTS_ACTIONS + + PROJECT_FILE_ACTIONS + + JOB_ACTIONS + + TOOLS_ACTIONS + + PROFILE_ACTIONS + + MESSAGE_ACTIONS + + NOTIFICATION_ACTIONS + + ENGAGEMENT_ACTIONS + + SOCIAL_ACTIONS + + ISSUE_ACTIONS + + GIST_ACTIONS + + NEWS_ACTIONS + + UPLOAD_ACTIONS + + ADMIN_ACTIONS + + DBAPI_ACTIONS + + GATEWAY_ACTIONS + + GAME_ACTIONS +) + +PLATFORM_CATALOG = Catalog(actions=ACTIONS) + +__all__ = ["ACTIONS", "PLATFORM_CATALOG"] diff --git a/devplacepy/services/devii/actions/catalog/_shared.py b/devplacepy/services/devii/actions/catalog/_shared.py new file mode 100644 index 00000000..7041292a --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/_shared.py @@ -0,0 +1,41 @@ +# retoor + +from __future__ import annotations + +from ..spec import Param + + +def path(name: str, description: str, required: bool = True) -> Param: + return Param(name=name, location="path", description=description, required=required) + + +def query(name: str, description: str, required: bool = False) -> Param: + return Param( + name=name, location="query", description=description, required=required + ) + + +def body(name: str, description: str, required: bool = False) -> Param: + return Param(name=name, location="body", description=description, required=required) + + +def upload(name: str, description: str) -> Param: + return Param(name=name, location="file", description=description, required=True) + + +def confirm() -> Param: + return Param( + name="confirm", + location="body", + description=( + "Set to true ONLY after the user has explicitly confirmed this irreversible action. " + "Leave it unset on the first call: the tool refuses and tells you to confirm with the " + "user, and only a repeat call with confirm=true actually performs it." + ), + required=False, + type="boolean", + ) + + +ATTACHMENTS = "Comma separated attachment uids returned by upload_file." +TARGET_TYPE = "Target type, e.g. post, comment, project, gist." diff --git a/devplacepy/services/devii/actions/catalog/admin.py b/devplacepy/services/devii/actions/catalog/admin.py new file mode 100644 index 00000000..9f6378a2 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/admin.py @@ -0,0 +1,425 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, confirm, path, query + + +ADMIN_ACTIONS: tuple[Action, ...] = ( + Action( + name="admin_overview", + method="GET", + path="/admin", + summary="View the admin overview", + requires_admin=True, + ), + Action( + name="site_analytics", + method="GET", + path="/admin/analytics", + summary="Site-wide aggregate analytics in one call (admin only)", + description=( + "Returns JSON: total members, active users in the last 24h/7d/30d, signed_in_now, " + "new signups (24h/7d/30d), content totals (posts, comments, gists, projects, " + "news), and top authors. Use this for any 'how many'/'how active'/count question " + "instead of paging through admin_list_users. signed_in_now counts members holding an " + "unexpired session (logged in within the session lifetime, default 7-30 days), NOT a " + "real-time online count - see signed_in_definition in the response and never report it " + "as 'currently online' or 'logged in right now'." + ), + params=(query("top_n", "How many top authors to include (1-50)."),), + requires_admin=True, + ), + Action( + name="ai_usage", + method="GET", + path="/admin/ai-usage/data", + summary="AI gateway usage, cost, latency, and reliability metrics (admin only)", + description=( + "Returns JSON for a bounded window: request volume and throughput, token usage with " + "averages and percentiles, latency (upstream, gateway overhead, queue wait, connect), " + "error rates by category, cost in USD (per model, per caller, input vs output, projected " + "monthly burn, caching savings), caller behavior, and an hourly breakdown. Use this for " + "any question about AI spend, token consumption, gateway performance, or errors." + ), + params=( + query("hours", "Lookback window in hours (1-168, default 48)."), + query("top_n", "How many rows in each top-N breakdown (default 10)."), + ), + requires_admin=True, + ), + Action( + name="audit_log", + method="GET", + path="/admin/audit-log", + summary="Query the platform audit trail with filters (admin only)", + description=( + "Returns JSON: a paginated, newest-first list of audit events (every state-changing " + "action on the platform, with actor, origin, target, old/new value, and result), plus " + "pagination, the active filters, and an 'options' object listing every available value " + "for each filter (category, event_key, actor_role, origin, result) so you can both " + "filter and discover valid filter values from a single call. Use this for any audit, " + "history, 'who did X', 'what changed', or 'show denied/failed actions' question. Filter " + "by event_key (exact), category, actor_role, actor_uid, origin (web, api, devii, cli, " + "service, scheduler), or result (success, failure, denied); narrow by date_from/date_to " + "(ISO date bounds, inclusive) or free-text q over summary, event key, and target." + ), + params=( + query("page", "Page number (1-based)."), + query("event_key", "Filter by exact event key (e.g. admin.setting.update)."), + query("category", "Filter by category (auth, content, admin, container, ...)."), + query("actor_role", "Filter by actor role at action time."), + query("actor_uid", "Filter by acting user uid."), + query("origin", "Filter by origin (web, api, devii, cli, service, scheduler)."), + query("result", "Filter by result (success, failure, denied)."), + query("q", "Free-text search over summary, event key, and target."), + query("date_from", "ISO date lower bound (inclusive)."), + query("date_to", "ISO date upper bound (inclusive)."), + ), + requires_admin=True, + ), + Action( + name="audit_event", + method="GET", + path="/admin/audit-log/{uid}", + summary="Get one audit event with its related-object links (admin only)", + description=( + "Returns JSON: the full audit event row plus every related-object link (actor, target, " + "parent, project, instance, setting, ...). Use after audit_log to inspect a single event " + "in detail." + ), + params=(path("uid", "Audit event uid."),), + requires_admin=True, + ), + Action( + name="bot_monitor", + method="GET", + path="/admin/bots/data", + summary="Live screenshot monitor of every running bot persona (admin only)", + description=( + "Returns JSON: one entry per running bot slot with its username, persona, current " + "action and status text, last page url, frame age in seconds, whether it is active, " + "and a frame_url to the latest low-quality screenshot. Use this to see what the bot " + "fleet is doing right now. Frames live only in the worker running the Bots service." + ), + requires_admin=True, + ), + Action( + name="admin_list_users", + method="GET", + path="/admin/users", + summary="List users for administration", + params=(query("page", "Page number."),), + requires_admin=True, + ), + Action( + name="admin_set_user_role", + method="POST", + path="/admin/users/{uid}/role", + summary="Set a user's role", + params=( + path("uid", "User uid."), + body("role", "New role.", required=True), + ), + requires_admin=True, + ), + Action( + name="admin_set_user_password", + method="POST", + path="/admin/users/{uid}/password", + summary="Set a user's password", + params=( + path("uid", "User uid."), + body("password", "New password.", required=True), + ), + requires_admin=True, + ), + Action( + name="admin_toggle_user", + method="POST", + path="/admin/users/{uid}/toggle", + summary="Toggle a user's active state", + params=(path("uid", "User uid."),), + requires_admin=True, + ), + Action( + name="admin_get_settings", + method="GET", + path="/admin/settings", + summary="View site settings", + requires_admin=True, + ), + Action( + name="admin_save_settings", + method="POST", + path="/admin/settings", + summary="Save site settings", + params=( + body("site_name", "Site name."), + body("site_description", "Site description."), + body("site_tagline", "Site tagline."), + body("max_upload_size_mb", "Maximum upload size in megabytes."), + body("allowed_file_types", "Allowed file types."), + body("max_attachments_per_resource", "Maximum attachments per resource."), + body("rate_limit_per_minute", "Rate limit per minute."), + body("rate_limit_window_seconds", "Rate limit window in seconds."), + body("session_max_age_days", "Session maximum age in days."), + body("session_remember_days", "Remember-me duration in days."), + body("registration_open", "Whether registration is open."), + body("maintenance_mode", "Whether maintenance mode is on."), + body("maintenance_message", "Maintenance message."), + ), + requires_admin=True, + ), + Action( + name="admin_list_news", + method="GET", + path="/admin/news", + summary="List news for administration", + params=(query("page", "Page number."),), + requires_admin=True, + ), + Action( + name="admin_toggle_news", + method="POST", + path="/admin/news/{uid}/toggle", + summary="Toggle a news article", + params=(path("uid", "News uid."),), + requires_admin=True, + ), + Action( + name="admin_publish_news", + method="POST", + path="/admin/news/{uid}/publish", + summary="Publish a news article", + params=(path("uid", "News uid."),), + requires_admin=True, + ), + Action( + name="admin_landing_news", + method="POST", + path="/admin/news/{uid}/landing", + summary="Set a news article as landing content", + params=(path("uid", "News uid."),), + requires_admin=True, + ), + Action( + name="admin_delete_news", + method="POST", + path="/admin/news/{uid}/delete", + summary="Delete a news article (admin only). Soft delete, confirmation required", + params=(path("uid", "News uid."), confirm()), + requires_admin=True, + ), + Action( + name="admin_list_services", + method="GET", + path="/admin/services", + summary="View managed services", + requires_admin=True, + ), + Action( + name="admin_services_data", + method="GET", + path="/admin/services/data", + summary="Get live status, metrics, and log tail for every background service", + requires_admin=True, + ), + Action( + name="admin_service_status", + method="GET", + path="/admin/services/{name}/data", + summary="Get live status, metrics, and log tail for one background service", + params=(path("name", "Service name (e.g. news, bots, openai)."),), + requires_admin=True, + ), + Action( + name="admin_start_service", + method="POST", + path="/admin/services/{name}/start", + summary="Start a managed service", + params=(path("name", "Service name."),), + requires_admin=True, + ), + Action( + name="admin_stop_service", + method="POST", + path="/admin/services/{name}/stop", + summary="Stop a managed service", + params=(path("name", "Service name."),), + requires_admin=True, + ), + Action( + name="admin_run_service", + method="POST", + path="/admin/services/{name}/run", + summary="Run a managed service once", + params=(path("name", "Service name."),), + requires_admin=True, + ), + Action( + name="admin_clear_service_logs", + method="POST", + path="/admin/services/{name}/clear-logs", + summary="Clear a managed service's logs", + params=(path("name", "Service name."),), + requires_admin=True, + ), + Action( + name="admin_config_service", + method="POST", + path="/admin/services/{name}/config", + summary="Update a managed service's configuration", + params=(path("name", "Service name."),), + freeform_body=True, + requires_admin=True, + ), + Action( + name="admin_user_ai_usage", + method="GET", + path="/admin/users/{uid}/ai-usage", + summary="Get a user's AI gateway usage (admin only)", + description="Returns per-user AI usage data including token counts, cost, and request volume for the given lookback window.", + params=( + path("uid", "User uid."), + query("hours", "Lookback window in hours (default 24)."), + ), + requires_admin=True, + ), + Action( + name="admin_reset_user_ai_quota", + method="POST", + path="/admin/users/{uid}/reset-ai-quota", + summary="Reset a user's AI quota (admin only)", + description="Clears the AI quota ledger for a specific user, allowing them to use AI features again.", + params=(path("uid", "User uid."), confirm()), + requires_admin=True, + ), + Action( + name="admin_media_purge", + method="POST", + path="/admin/media/{uid}/purge", + summary="Permanently remove soft-deleted media (admin only)", + description="Permanently deletes a soft-deleted attachment from the database and filesystem.", + params=(path("uid", "Attachment uid to purge."), confirm()), + requires_admin=True, + ), + Action( + name="admin_reset_guest_ai_quota", + method="POST", + path="/admin/ai-quota/reset-guests", + summary="Reset all guest AI quotas (admin only)", + description="Clears the AI quota ledger for all guest sessions.", + params=(confirm(),), + requires_admin=True, + ), + Action( + name="admin_reset_all_ai_quota", + method="POST", + path="/admin/ai-quota/reset-all", + summary="Reset ALL AI quotas including member quotas (admin only)", + description="Clears the AI quota ledger for every user and guest. Use with caution.", + params=(confirm(),), + requires_admin=True, + ), + Action( + name="backups_overview", + method="GET", + path="/admin/backups/data", + summary="Backup dashboard: storage usage, backups, and schedules (admin only)", + description=( + "Returns JSON: per-path storage usage (database, uploads, attachments, project files, " + "keys, zips, deepsearch, container workspaces, backups), total data-directory size, total " + "stored-backup size and count, disk usage (total/used/free/percent), every backup archive " + "(target, status, size, file count, sha256 checksum, created/completed time, download_url), " + "and every configured backup schedule. Use this for any question about backups, data " + "footprint, disk space, or what is scheduled." + ), + requires_admin=True, + ), + Action( + name="backup_run", + method="POST", + path="/admin/backups/run", + summary="Start a backup of a data target (admin only)", + description=( + "Enqueues an async backup job and returns its job uid and status_url. Target is one of: " + "database (consistent SQLite snapshot plus Devii databases), uploads (attachments and " + "project files), keys (VAPID keys and config), or full (database, uploads, and keys in one " + "archive). Poll backup_status with the returned uid until status is done, then read the " + "download_url. The job runs off the request path and never blocks the server." + ), + params=( + body("target", "One of database, uploads, keys, full.", required=True), + ), + requires_admin=True, + ), + Action( + name="backup_status", + method="GET", + path="/admin/backups/jobs/{uid}", + summary="Check the status of a backup job (admin only)", + description=( + "Returns JSON for one backup job: status (pending, running, done, failed), target, " + "backup_uid, download_url (when done), sha256, archive size, file count, and timestamps. " + "Poll this after backup_run." + ), + params=(path("uid", "Backup job uid returned by backup_run."),), + requires_admin=True, + ), + Action( + name="backup_delete", + method="POST", + path="/admin/backups/{uid}/delete", + summary="Permanently delete a backup archive (admin only)", + description=( + "Removes a backup archive file and its record. This is irreversible and reclaims disk " + "space. The uid is a backup uid (from backups_overview), not a job uid." + ), + params=(path("uid", "Backup uid to delete."), confirm()), + requires_admin=True, + ), + Action( + name="backup_schedule_create", + method="POST", + path="/admin/backups/schedules/create", + summary="Create a recurring backup schedule (admin only)", + description=( + "Creates a schedule that fires backups automatically. kind is 'interval' (use every_seconds, " + "minimum 60) or 'cron' (use a 5-field cron expression in 'minute hour dom month dow'). " + "keep_last rotates older backups of this schedule, keeping only the newest N (0 keeps all). " + "target is one of database, uploads, keys, full." + ), + params=( + body("name", "Human-readable schedule name.", required=True), + body("target", "One of database, uploads, keys, full.", required=True), + body("kind", "interval or cron.", required=True), + body("every_seconds", "Seconds between runs when kind=interval (min 60)."), + body("cron", "Cron expression when kind=cron, e.g. '0 3 * * *'."), + body("keep_last", "Keep only the newest N backups of this schedule (0 = all)."), + ), + requires_admin=True, + ), + Action( + name="backup_schedule_delete", + method="POST", + path="/admin/backups/schedules/{uid}/delete", + summary="Delete a backup schedule (admin only)", + description=( + "Removes a backup schedule so it stops firing. Existing backup archives are kept; only the " + "schedule is deleted." + ), + params=(path("uid", "Backup schedule uid."), confirm()), + requires_admin=True, + ), + Action( + name="restore_media", + method="POST", + path="/media/{uid}/restore", + summary="Restore a soft-deleted media attachment (admin only)", + description="Restores a previously soft-deleted attachment, making it visible again.", + params=(path("uid", "Attachment uid to restore."),), + requires_admin=True, + ), +) diff --git a/devplacepy/services/devii/actions/catalog/auth.py b/devplacepy/services/devii/actions/catalog/auth.py new file mode 100644 index 00000000..072281cb --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/auth.py @@ -0,0 +1,72 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, path + + +AUTH_ACTIONS: tuple[Action, ...] = ( + Action( + name="auth_status", + method="GET", + path="", + summary="Report whether the current session is authenticated", + handler="status", + requires_auth=False, + ), + Action( + name="login", + method="POST", + path="/auth/login", + summary="Authenticate with email and password and start a session", + handler="login", + requires_auth=False, + params=( + body("email", "Account email address.", required=True), + body("password", "Account password.", required=True), + body("remember_me", "Keep the session alive longer ('on' or '')."), + ), + ), + Action( + name="logout", + method="GET", + path="/auth/logout", + summary="End the current session", + handler="logout", + requires_auth=True, + ), + Action( + name="signup", + method="POST", + path="/auth/signup", + summary="Create a new account", + requires_auth=False, + params=( + body("username", "Desired username.", required=True), + body("email", "Email address.", required=True), + body("password", "Password (minimum six characters).", required=True), + body("confirm_password", "Password confirmation.", required=True), + ), + ), + Action( + name="forgot_password", + method="POST", + path="/auth/forgot-password", + summary="Request a password reset email", + requires_auth=False, + params=(body("email", "Account email address.", required=True),), + ), + Action( + name="reset_password", + method="POST", + path="/auth/reset-password/{token}", + summary="Reset a password using a reset token", + requires_auth=False, + params=( + path("token", "Reset token from the email link."), + body("password", "New password.", required=True), + body("confirm_password", "New password confirmation.", required=True), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/comments.py b/devplacepy/services/devii/actions/catalog/comments.py new file mode 100644 index 00000000..3eac99ff --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/comments.py @@ -0,0 +1,41 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path + + +COMMENTS_ACTIONS: tuple[Action, ...] = ( + Action( + name="create_comment", + method="POST", + path="/comments/create", + summary="Create a comment on a post or other target", + params=( + body("content", "Comment body.", required=True), + body("post_uid", "Uid of the post being commented on."), + body("target_uid", "Uid of the target when not a post."), + body("target_type", TARGET_TYPE), + body("parent_uid", "Parent comment uid for replies."), + body("attachment_uids", ATTACHMENTS), + ), + ), + Action( + name="edit_comment", + method="POST", + path="/comments/edit/{comment_uid}", + summary="Edit the body of one of your own comments", + params=( + path("comment_uid", "Uid of the comment."), + body("content", "New comment body, 3-1000 characters.", required=True), + ), + ), + Action( + name="delete_comment", + method="POST", + path="/comments/delete/{comment_uid}", + summary="Delete a comment (your own, or any comment when you are an administrator). Soft delete, confirmation required", + params=(path("comment_uid", "Uid of the comment."), confirm()), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/dbapi.py b/devplacepy/services/devii/actions/catalog/dbapi.py new file mode 100644 index 00000000..f245c6ba --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/dbapi.py @@ -0,0 +1,127 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action, Param +from ._shared import body, path, query + + +DBAPI_ACTIONS: tuple[Action, ...] = ( + Action( + name="db_list_tables", + method="GET", + path="/dbapi/tables", + summary="List database tables exposed by the database API (primary administrator only)", + description=( + "Returns every table reachable through the database API with its row count and " + "whether it uses soft deletes. Use this to discover what data exists before " + "querying or designing a query." + ), + params=(), + requires_admin=True, + requires_primary_admin=True, + ), + Action( + name="db_table_schema", + method="GET", + path="/dbapi/{table}/schema", + summary="Show a table's columns and types (primary administrator only)", + description="Returns the column names, types, row count, and soft-delete flag for one table.", + params=(path("table", "Table name (from db_list_tables)."),), + requires_admin=True, + requires_primary_admin=True, + ), + Action( + name="db_list_rows", + method="GET", + path="/dbapi/{table}", + summary="List rows of a table with keyset pagination (primary administrator only)", + description=( + "Browses rows newest-first. Soft-deleted rows are excluded unless include_deleted is " + "true. For filtered or joined questions prefer db_query or db_design_query." + ), + params=( + path("table", "Table name."), + query("limit", "Maximum rows (1-500, default 25)."), + query("search", "Free-text search over common text columns."), + query("before", "Keyset cursor: return rows older than this created_at/id value."), + Param( + name="include_deleted", + location="query", + description="Include soft-deleted rows.", + required=False, + type="boolean", + ), + ), + requires_admin=True, + requires_primary_admin=True, + ), + Action( + name="db_get_row", + method="GET", + path="/dbapi/{table}/{key}/{value}", + summary="Fetch one row by a key column (primary administrator only)", + description="Returns a single row where key column equals value (key is usually 'uid').", + params=( + path("table", "Table name."), + path("key", "Key column to match (usually 'uid')."), + path("value", "Value of the key column."), + ), + requires_admin=True, + requires_primary_admin=True, + ), + Action( + name="db_query", + method="POST", + path="/dbapi/query", + summary="Run a read-only SQL SELECT and return rows (primary administrator only)", + description=( + "Executes a SINGLE validated SELECT statement read-only and returns the rows. Only " + "SELECT is allowed; INSERT/UPDATE/DELETE/DDL are rejected. The database API is " + "read-only and cannot change data in any way. The response may include a 'suspicious' " + "list (e.g. a SELECT with no WHERE/JOIN/LIMIT that scans a whole table); when present, " + "surface that warning to the user before trusting the results." + ), + params=( + body("sql", "A single SELECT statement.", required=True), + body("dialect", "Optional source SQL dialect (default sqlite)."), + ), + requires_admin=True, + requires_primary_admin=True, + read_only=True, + ), + Action( + name="db_design_query", + method="POST", + path="/dbapi/nl", + summary="Design a SQL SELECT from a natural-language question (primary administrator only)", + description=( + "Turns a plain-language question about one table into a validated read-only SELECT. " + "It auto-adds 'deleted_at IS NULL' for soft-delete tables unless apply_soft_delete is " + "false. By default it only returns the SQL; pass execute=true to also run it read-only " + "and return rows. Show the user the SQL and any 'suspicious' notes." + ), + params=( + body("question", "The natural-language request.", required=True), + body("table", "Target table the question is about.", required=True), + Param( + name="apply_soft_delete", + location="body", + description="Add deleted_at IS NULL for soft-delete tables (default true).", + required=False, + type="boolean", + ), + body("dialect", "Optional target SQL dialect (default sqlite)."), + Param( + name="execute", + location="body", + description="Also run the validated query read-only and return rows.", + required=False, + type="boolean", + ), + ), + requires_admin=True, + requires_primary_admin=True, + read_only=True, + ), +) diff --git a/devplacepy/services/devii/actions/catalog/engagement.py b/devplacepy/services/devii/actions/catalog/engagement.py new file mode 100644 index 00000000..ba346923 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/engagement.py @@ -0,0 +1,80 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import TARGET_TYPE, body, path, query + + +ENGAGEMENT_ACTIONS: tuple[Action, ...] = ( + Action( + name="vote", + method="POST", + path="/votes/{target_type}/{target_uid}", + summary="Cast or toggle a vote on a target", + description="Re-sending the same value removes the vote. Returns the net/up/down tally.", + ajax=True, + params=( + path("target_type", TARGET_TYPE), + path( + "target_uid", + "Uid of the target, copied from a listing response; do not invent it.", + ), + body( + "value", + "Vote value: 1 to upvote, -1 to downvote (re-send to remove).", + required=True, + ), + ), + ), + Action( + name="react", + method="POST", + path="/reactions/{target_type}/{target_uid}", + summary="Toggle an emoji reaction on a target", + description="Re-sending the same emoji removes it. Returns reaction counts.", + ajax=True, + params=( + path("target_type", TARGET_TYPE), + path( + "target_uid", + "Uid of the target, copied from a listing response; do not invent it.", + ), + body("emoji", "One of the allowed reaction emoji.", required=True), + ), + ), + Action( + name="list_bookmarks", + method="GET", + path="/bookmarks/saved", + summary="List saved bookmarks", + params=(query("before", "Pagination cursor."),), + ), + Action( + name="toggle_bookmark", + method="POST", + path="/bookmarks/{target_type}/{target_uid}", + summary="Toggle a bookmark on a target", + description="Re-sending removes the bookmark. Returns {saved: true|false}.", + ajax=True, + params=( + path("target_type", TARGET_TYPE), + path( + "target_uid", + "Uid of the target, copied from a listing response; do not invent it.", + ), + ), + ), + Action( + name="vote_poll", + method="POST", + path="/polls/{poll_uid}/vote", + summary="Vote in a poll", + description="Casts or changes your vote on a poll option. Returns the option tallies.", + ajax=True, + params=( + path("poll_uid", "Poll uid."), + body("option_uid", "Chosen option uid.", required=True), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/game.py b/devplacepy/services/devii/actions/catalog/game.py new file mode 100644 index 00000000..542ab785 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/game.py @@ -0,0 +1,169 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action, Param +from ._shared import body, path + + +GAME_ACTIONS: tuple[Action, ...] = ( + Action( + name="game_state", + method="GET", + path="/game/state", + summary="Get the current user's Code Farm state", + description=( + "Returns JSON for the signed-in user's Code Farm: coins, level/XP, CI tier, " + "plot grid (each plot's state, crop, remaining build seconds), and the plantable crops." + ), + handler="http", + requires_auth=True, + read_only=True, + ), + Action( + name="game_leaderboard", + method="GET", + path="/game/leaderboard", + summary="List the top Code Farm players", + handler="http", + requires_auth=False, + read_only=True, + ), + Action( + name="game_view_farm", + method="GET", + path="/game/farm/{username}", + summary="View another player's Code Farm", + handler="http", + requires_auth=False, + read_only=True, + params=(path("username", "The farm owner's username."),), + ), + Action( + name="game_plant", + method="POST", + path="/game/plant", + summary="Plant a crop in an empty plot on your Code Farm", + handler="http", + requires_auth=True, + params=( + Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), + body("crop", "Crop key, e.g. shell, python, webapp, api, rust, haskell, kernel.", required=True), + ), + ), + Action( + name="game_harvest", + method="POST", + path="/game/harvest", + summary="Harvest a finished build from a plot on your Code Farm", + handler="http", + requires_auth=True, + params=( + Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), + ), + ), + Action( + name="game_buy_plot", + method="POST", + path="/game/buy-plot", + summary="Buy a new plot on your Code Farm", + handler="http", + requires_auth=True, + ), + Action( + name="game_upgrade_ci", + method="POST", + path="/game/upgrade", + summary="Upgrade your Code Farm CI tier for faster builds", + handler="http", + requires_auth=True, + ), + Action( + name="game_water", + method="POST", + path="/game/farm/{username}/water", + summary="Water a growing build on another player's Code Farm", + handler="http", + requires_auth=True, + params=( + path("username", "The farm owner's username."), + Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), + ), + ), + Action( + name="game_steal", + method="POST", + path="/game/farm/{username}/steal", + summary="Steal a ready build from another player's Code Farm once its protection window has passed (limited to once per hour per neighbour)", + handler="http", + requires_auth=True, + params=( + path("username", "The farm owner's username."), + Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), + ), + ), + Action( + name="game_fertilize", + method="POST", + path="/game/fertilize", + summary="Spend coins to halve a growing build's remaining time on your Code Farm (cost scales with the build's harvest value, so it is a pure time-skip, never a profit)", + handler="http", + requires_auth=True, + params=( + Param(name="slot", location="body", description="Plot slot index.", required=True, type="integer"), + ), + ), + Action( + name="game_daily", + method="POST", + path="/game/daily", + summary="Claim the daily Code Farm coin bonus (builds a streak)", + handler="http", + requires_auth=True, + ), + Action( + name="game_upgrade_perk", + method="POST", + path="/game/perk", + summary="Upgrade a permanent Code Farm perk (yield, growth, discount, xp)", + handler="http", + requires_auth=True, + params=( + body("perk", "Perk key: yield, growth, discount, or xp.", required=True), + ), + ), + Action( + name="game_claim_quest", + method="POST", + path="/game/quests/claim", + summary="Claim a completed daily Code Farm quest reward", + handler="http", + requires_auth=True, + params=( + body("quest", "Quest kind: plant, harvest, water, or earn.", required=True), + ), + ), + Action( + name="game_prestige", + method="POST", + path="/game/prestige", + summary="Refactor (prestige) your Code Farm for a permanent coin bonus and Stars (requires level 10)", + handler="http", + requires_auth=True, + ), + Action( + name="game_upgrade_legacy", + method="POST", + path="/game/legacy", + summary="Spend Stars on a permanent Legacy upgrade that survives refactor (autoharvest, multiplier, speed, plots, defense)", + handler="http", + requires_auth=True, + params=( + body( + "key", + "Legacy key: autoharvest, multiplier, speed, plots, or defense.", + required=True, + ), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/gateway.py b/devplacepy/services/devii/actions/catalog/gateway.py new file mode 100644 index 00000000..b0cdc7a3 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/gateway.py @@ -0,0 +1,106 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action, Param +from ._shared import body, confirm, path + + +GATEWAY_ACTIONS: tuple[Action, ...] = ( + Action( + name="gateway_providers", + method="GET", + path="/admin/gateway/providers", + summary="List OpenAI gateway providers (admin only)", + description=( + "Returns JSON: the named upstream providers used by model routes, plus the implicit " + "'default' provider read from the gateway service config." + ), + handler="http", + requires_admin=True, + read_only=True, + ), + Action( + name="gateway_provider_set", + method="POST", + path="/admin/gateway/providers", + summary="Create or update a gateway provider (admin only)", + description=( + "Adds or updates a named upstream provider. base_url is the OpenAI-compatible " + "chat-completions endpoint; the embeddings endpoint is derived from it." + ), + handler="http", + requires_admin=True, + params=( + body("name", "Provider name (letters, numbers, hyphen, underscore).", required=True), + body("base_url", "Chat-completions endpoint URL."), + body("api_key", "Upstream API key."), + Param( + name="is_active", + location="body", + description="Whether the provider is active ('1' or '0').", + required=False, + type="boolean", + ), + ), + ), + Action( + name="gateway_provider_delete", + method="DELETE", + path="/admin/gateway/providers/{name}", + summary="Delete a gateway provider (admin only, confirmation required)", + handler="http", + requires_admin=True, + params=(path("name", "Provider name."), confirm()), + ), + Action( + name="gateway_models", + method="GET", + path="/admin/gateway/models", + summary="List OpenAI gateway model routes (admin only)", + description=( + "Returns JSON: every source-model route with its provider, target model, kind " + "(chat/embed), optional vision model, context window, and per-model pricing economy." + ), + handler="http", + requires_admin=True, + read_only=True, + ), + Action( + name="gateway_model_set", + method="POST", + path="/admin/gateway/models", + summary="Create or update a gateway model route (admin only)", + description=( + "Maps a requested source_model onto a provider + target_model, each with its own " + "pricing. kind is 'chat' or 'embed'. A vision_model adds image-to-text augmentation " + "for chat routes. Prices are USD per 1,000,000 tokens; chat uses cache-hit/cache-miss/" + "output, embeddings use input, vision uses input/output." + ), + handler="http", + requires_admin=True, + params=( + body("source_model", "Model name clients request.", required=True), + body("provider", "Provider name, or blank for the default upstream."), + body("target_model", "Model name sent upstream.", required=True), + body("kind", "Route kind: 'chat' or 'embed'."), + body("vision_provider", "Provider for image description, or blank for the route provider."), + body("vision_model", "Vision model name (blank disables the merge)."), + Param(name="context_window", location="body", description="Max context tokens (0 = unknown).", required=False, type="integer"), + Param(name="price_cache_hit_per_m", location="body", description="USD per 1M cache-hit input tokens.", required=False, type="number"), + Param(name="price_cache_miss_per_m", location="body", description="USD per 1M cache-miss input tokens.", required=False, type="number"), + Param(name="price_output_per_m", location="body", description="USD per 1M output tokens.", required=False, type="number"), + Param(name="price_input_per_m", location="body", description="USD per 1M input tokens (embed/vision).", required=False, type="number"), + Param(name="is_active", location="body", description="Whether the route is active ('1' or '0').", required=False, type="boolean"), + ), + ), + Action( + name="gateway_model_delete", + method="DELETE", + path="/admin/gateway/models/{source_model}", + summary="Delete a gateway model route (admin only, confirmation required)", + handler="http", + requires_admin=True, + params=(path("source_model", "Source model name."), confirm()), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/gists.py b/devplacepy/services/devii/actions/catalog/gists.py new file mode 100644 index 00000000..bae75ea9 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/gists.py @@ -0,0 +1,78 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import ATTACHMENTS, body, confirm, path, query + + +GIST_ACTIONS: tuple[Action, ...] = ( + Action( + name="list_gists", + method="GET", + path="/gists", + summary="List gists", + requires_auth=False, + params=( + query("language", "Filter by language."), + query("user_uid", "Filter by owner uid."), + query("search", "Search gist title, description, and author username."), + query("before", "Pagination cursor."), + ), + ), + Action( + name="view_gist", + method="GET", + path="/gists/{gist_slug}", + summary="View a gist by slug", + requires_auth=False, + params=( + path( + "gist_slug", + "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", + ), + ), + ), + Action( + name="create_gist", + method="POST", + path="/gists/create", + summary="Create a gist", + params=( + body("title", "Gist title.", required=True), + body("source_code", "Gist source code.", required=True), + body("description", "Gist description."), + body("language", "Programming language."), + body("attachment_uids", ATTACHMENTS), + ), + ), + Action( + name="edit_gist", + method="POST", + path="/gists/edit/{gist_slug}", + summary="Edit a gist", + params=( + path( + "gist_slug", + "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", + ), + body("title", "Gist title.", required=True), + body("source_code", "Gist source code.", required=True), + body("description", "Gist description."), + body("language", "Programming language."), + ), + ), + Action( + name="delete_gist", + method="POST", + path="/gists/delete/{gist_slug}", + summary="Delete a gist (your own, or any gist when you are an administrator). Soft delete, confirmation required", + params=( + path( + "gist_slug", + "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.", + ), + confirm(), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/issues.py b/devplacepy/services/devii/actions/catalog/issues.py new file mode 100644 index 00000000..2761fa39 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/issues.py @@ -0,0 +1,156 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import ATTACHMENTS, body, confirm, path, query + + +ISSUE_ACTIONS: tuple[Action, ...] = ( + Action( + name="list_issues", + method="GET", + path="/issues", + summary="List issue tickets from the Gitea tracker", + requires_auth=False, + params=( + query("state", "Filter by state: open, closed, or all."), + query("page", "1-based page number."), + ), + ), + Action( + name="create_issue", + method="POST", + path="/issues/create", + summary="Report an issue. It is rewritten by AI and filed on the tracker.", + description=( + "Queues a background issue creation job and returns {uid, status_url}. Poll the " + "status_url with issue_job_status until status is 'done', then show the user the " + "issue_url pointing at the filed ticket." + ), + params=( + body("title", "Issue title.", required=True), + body("description", "Issue description.", required=True), + ), + ), + Action( + name="planning_report_generate", + method="POST", + path="/issues/planning", + summary="Queue an admin planning report of open tickets.", + description=( + "Queues a background job that builds a phased, ordered markdown implementation " + "document for open tickets and returns {uid, status_url}. Pass 'numbers' as a " + "comma-separated list of issue numbers to plan only those tickets; omit it to plan " + "every open ticket. Each ticket's full description is reproduced verbatim, both " + "inline and in a 'Source Tickets (verbatim)' appendix, so the document is " + "self-contained. Poll the status_url until status is 'done', then show the markdown " + "and the download_url. Admin only." + ), + params=( + body( + "numbers", + "Comma-separated issue numbers to include (e.g. '1,4,7'). Omit for all open tickets.", + ), + ), + requires_auth=True, + requires_admin=True, + ), + Action( + name="issue_job_status", + method="GET", + path="/issues/jobs/{uid}", + summary="Poll an issue creation job and obtain the filed issue ticket URL once finished", + description=( + "Returns the job status. When status is 'done', issue_url and number are populated; " + "while 'pending' or 'running', poll again shortly." + ), + params=(path("uid", "Issue job uid returned by create_issue."),), + requires_auth=False, + ), + Action( + name="view_issue", + method="GET", + path="/issues/{number}", + summary="View an issue ticket and its developer updates", + requires_auth=False, + params=(path("number", "The issue ticket number from a /issues listing."),), + ), + Action( + name="comment_issue", + method="POST", + path="/issues/{number}/comment", + summary="Post a comment on an issue ticket, attributed to the current user", + params=( + path("number", "The issue ticket number."), + body("body", "Comment text.", required=True), + ), + ), + Action( + name="set_issue_status", + method="POST", + path="/issues/{number}/status", + summary="Change an issue ticket status (admin only)", + requires_admin=True, + params=( + path("number", "The issue ticket number."), + body("status", "New status: open or closed.", required=True), + ), + ), + Action( + name="list_issue_attachments", + method="GET", + path="/issues/{number}/attachments", + summary="List the files attached to an issue ticket", + requires_auth=False, + params=(path("number", "The issue ticket number."),), + ), + Action( + name="add_issue_attachment", + method="POST", + path="/issues/{number}/attachments", + summary="Attach already-uploaded files to an open issue ticket", + description=( + "Upload files first with upload_file, then pass their uids here. Only works while " + "the issue is open; the files are mirrored to the tracker." + ), + params=( + path("number", "The issue ticket number."), + body("attachment_uids", ATTACHMENTS, required=True), + ), + ), + Action( + name="delete_issue_attachment", + method="DELETE", + path="/issues/{number}/attachments/{uid}", + summary="Delete a file from an open issue ticket (your own, or any when administrator). Soft delete, confirmation required", + params=( + path("number", "The issue ticket number."), + path("uid", "The attachment uid."), + confirm(), + ), + ), + Action( + name="add_comment_attachment", + method="POST", + path="/issues/{number}/comments/{cid}/attachments", + summary="Attach already-uploaded files to an issue comment (issue must be open)", + params=( + path("number", "The issue ticket number."), + path("cid", "The Gitea comment id."), + body("attachment_uids", ATTACHMENTS, required=True), + ), + ), + Action( + name="delete_comment_attachment", + method="DELETE", + path="/issues/{number}/comments/{cid}/attachments/{uid}", + summary="Delete a file from an issue comment (your own, or any when administrator). Soft delete, confirmation required", + params=( + path("number", "The issue ticket number."), + path("cid", "The Gitea comment id."), + path("uid", "The attachment uid."), + confirm(), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/jobs.py b/devplacepy/services/devii/actions/catalog/jobs.py new file mode 100644 index 00000000..5b2e681d --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/jobs.py @@ -0,0 +1,77 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, path, query + + +JOB_ACTIONS: tuple[Action, ...] = ( + Action( + name="zip_project", + method="POST", + path="/projects/{project_slug}/zip", + summary="Build a downloadable zip archive of a whole project", + description=( + "Queues a background zip job and returns {uid, status_url}. Poll the status_url with " + "zip_status until status is 'done', then give the user the download_url." + ), + params=(path("project_slug", "Project slug or uid."),), + requires_auth=False, + ), + Action( + name="project_files_zip", + method="POST", + path="/projects/{project_slug}/files/zip", + summary="Build a downloadable zip archive of a project subdirectory", + description=( + "Queues a background zip job for a subpath inside the project and returns " + "{uid, status_url}. Poll the status_url with zip_status until status is 'done', " + "then give the user the download_url. When path is empty the whole project is zipped." + ), + params=( + path("project_slug", "Project slug or uid."), + query("path", "Relative subpath inside the project to zip; empty for the whole project."), + ), + requires_auth=False, + ), + Action( + name="zip_status", + method="GET", + path="/zips/{uid}", + summary="Check a zip job and obtain its download link once finished", + description=( + "Returns the job status and stats. When status is 'done', download_url points at the " + "ready archive; while 'pending' or 'running', poll again shortly." + ), + params=(path("uid", "Zip job uid returned by zip_project."),), + requires_auth=False, + ), + Action( + name="fork_project", + method="POST", + path="/projects/{project_slug}/fork", + summary="Fork a project into a new project owned by the current user", + description=( + "Queues a background fork job and returns {uid, status_url}. Poll the status_url with " + "fork_status until status is 'done', then give the user the project_url of the new fork." + ), + params=( + path("project_slug", "Slug or uid of the project to fork."), + body("title", "Title for the new forked project.", required=True), + ), + requires_auth=True, + ), + Action( + name="fork_status", + method="GET", + path="/forks/{uid}", + summary="Check a fork job and obtain the new project once finished", + description=( + "Returns the job status. When status is 'done', project_url points at the new forked " + "project; while 'pending' or 'running', poll again shortly." + ), + params=(path("uid", "Fork job uid returned by fork_project."),), + requires_auth=False, + ), +) diff --git a/devplacepy/services/devii/actions/catalog/messages.py b/devplacepy/services/devii/actions/catalog/messages.py new file mode 100644 index 00000000..0738d8d9 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/messages.py @@ -0,0 +1,38 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import ATTACHMENTS, body, query + + +MESSAGE_ACTIONS: tuple[Action, ...] = ( + Action( + name="list_messages", + method="GET", + path="/messages", + summary="View direct message conversations", + params=( + query("with_uid", "Open a conversation with a user uid."), + query("search", "Search conversations."), + ), + ), + Action( + name="search_message_users", + method="GET", + path="/messages/search", + summary="Search users to message", + params=(query("q", "Search query."),), + ), + Action( + name="send_message", + method="POST", + path="/messages/send", + summary="Send a direct message", + params=( + body("content", "Message body.", required=True), + body("receiver_uid", "Recipient user uid.", required=True), + body("attachment_uids", ATTACHMENTS), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/news.py b/devplacepy/services/devii/actions/catalog/news.py new file mode 100644 index 00000000..7cecaa7e --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/news.py @@ -0,0 +1,31 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import path, query + + +NEWS_ACTIONS: tuple[Action, ...] = ( + Action( + name="list_news", + method="GET", + path="/news", + summary="List news articles", + requires_auth=False, + params=(query("before", "Pagination cursor."),), + ), + Action( + name="view_news", + method="GET", + path="/news/{news_slug}", + summary="View a news article by slug", + requires_auth=False, + params=( + path( + "news_slug", + "Exact news slug copied from a /news/... link in a listing response; do not build it from the title.", + ), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/notifications.py b/devplacepy/services/devii/actions/catalog/notifications.py new file mode 100644 index 00000000..60eb7ac5 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/notifications.py @@ -0,0 +1,44 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import path, query + + +NOTIFICATION_ACTIONS: tuple[Action, ...] = ( + Action( + name="list_notifications", + method="GET", + path="/notifications", + summary="List notifications", + params=(query("before", "Pagination cursor."),), + ), + Action( + name="notification_counts", + method="GET", + path="/notifications/counts", + summary="Get unread notification and message counts", + requires_auth=False, + ), + Action( + name="open_notification", + method="GET", + path="/notifications/open/{notification_uid}", + summary="Open a notification and follow it", + params=(path("notification_uid", "Notification uid."),), + ), + Action( + name="mark_notification_read", + method="POST", + path="/notifications/mark-read/{notification_uid}", + summary="Mark a single notification read", + params=(path("notification_uid", "Notification uid."),), + ), + Action( + name="mark_all_notifications_read", + method="POST", + path="/notifications/mark-all-read", + summary="Mark all notifications read", + ), +) diff --git a/devplacepy/services/devii/actions/catalog/posts.py b/devplacepy/services/devii/actions/catalog/posts.py new file mode 100644 index 00000000..ea32e4c7 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/posts.py @@ -0,0 +1,90 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import ATTACHMENTS, body, confirm, path, query + + +POSTS_ACTIONS: tuple[Action, ...] = ( + Action( + name="view_feed", + method="GET", + path="/feed", + summary="View the activity feed", + requires_auth=False, + params=( + query("tab", "Feed tab to view."), + query("topic", "Filter by topic."), + query("search", "Search post title, content, and author username."), + query("before", "Pagination cursor."), + ), + ), + Action( + name="create_post", + method="POST", + path="/posts/create", + summary="Create a new post", + params=( + body("content", "Post body text.", required=True), + body("title", "Optional post title."), + body("topic", "Optional topic."), + body("project_uid", "Attach the post to a project uid."), + body("attachment_uids", ATTACHMENTS), + body("poll_question", "Optional poll question."), + body( + "poll_options", + "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.", + ), + ), + ), + Action( + name="view_post", + method="GET", + path="/posts/{post_slug}", + summary="View a single post by slug", + requires_auth=False, + params=( + path( + "post_slug", + "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", + ), + ), + ), + Action( + name="edit_post", + method="POST", + path="/posts/edit/{post_slug}", + summary="Edit an existing post", + params=( + path( + "post_slug", + "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", + ), + body("content", "Updated post body.", required=True), + body("title", "Updated title."), + body("topic", "Updated topic."), + body( + "poll_question", + "Optional poll question. Adds a poll to a post that does not already have one.", + ), + body( + "poll_options", + "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.", + ), + ), + ), + Action( + name="delete_post", + method="POST", + path="/posts/delete/{post_slug}", + summary="Delete a post (your own, or any post when you are an administrator). Soft delete, confirmation required", + params=( + path( + "post_slug", + "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.", + ), + confirm(), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/profile.py b/devplacepy/services/devii/actions/catalog/profile.py new file mode 100644 index 00000000..dd547ccf --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/profile.py @@ -0,0 +1,79 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, confirm, path, query + + +PROFILE_ACTIONS: tuple[Action, ...] = ( + Action( + name="search_users", + method="GET", + path="/profile/search", + summary="Search for users by name", + params=(query("q", "Search query."),), + ), + Action( + name="my_profile", + method="GET", + path="/profile", + summary="View the current user's own profile", + params=(query("tab", "Profile tab."),), + ), + Action( + name="view_profile", + method="GET", + path="/profile/{username}", + summary="View a user profile", + requires_auth=False, + params=( + path("username", "Username to view."), + query("tab", "Profile tab."), + ), + ), + Action( + name="update_profile", + method="POST", + path="/profile/update", + summary="Update the current user's profile", + params=( + body("bio", "Profile biography."), + body("location", "Location."), + body("git_link", "Git profile link."), + body("website", "Personal website."), + ), + ), + Action( + name="regenerate_api_key", + method="POST", + path="/profile/regenerate-api-key", + summary="Issue a new API key and invalidate the current one", + description=( + "Returns the new api_key. Warning: this immediately invalidates any key currently " + "used for authentication, so confirm with the user before calling it." + ), + params=( + body( + "confirm", + "Must be true, set only after the user has explicitly confirmed.", + required=True, + ), + ), + ), + Action( + name="regenerate_avatar", + method="POST", + path="/profile/{username}/regenerate-avatar", + summary="Generate a new random avatar for a user", + description=( + "Replaces the user's avatar with a freshly generated random one and returns the new " + "avatar_url. Owner-or-admin only. Irreversible: the previous avatar is gone for good, " + "so confirm with the user before calling it." + ), + params=( + path("username", "Username whose avatar to regenerate."), + confirm(), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/project_files.py b/devplacepy/services/devii/actions/catalog/project_files.py new file mode 100644 index 00000000..8185df79 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/project_files.py @@ -0,0 +1,179 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, confirm, path, query, upload + + +PROJECT_FILE_ACTIONS: tuple[Action, ...] = ( + Action( + name="project_list_files", + method="GET", + path="/projects/{project_slug}/files", + summary="List every file and directory in a project filesystem", + description="Returns the flat list of nodes (path, type, size, mime). Use it to inspect the project tree before reading or writing files.", + params=(path("project_slug", "Project slug or uid."),), + requires_auth=False, + ), + Action( + name="project_read_file", + method="GET", + path="/projects/{project_slug}/files/raw", + summary="Read one file from a project filesystem", + description="Returns the file metadata plus the text content. Binary files return a url instead of content.", + params=( + path("project_slug", "Project slug or uid."), + query( + "path", + "Relative file path inside the project, e.g. src/main.py.", + required=True, + ), + ), + requires_auth=False, + ), + Action( + name="project_write_file", + method="POST", + path="/projects/{project_slug}/files/write", + summary="Create or overwrite a whole text file in a project (parent directories are created automatically)", + description=( + "Replaces the ENTIRE file with the content you send. Creating a new file needs no prior " + "read, but overwriting an existing one requires reading it first (project_read_file). " + "For an existing file prefer the surgical line tools (project_replace_lines, " + "project_insert_lines, project_delete_lines, project_append_file) instead of rewriting it, " + "and never batch several writes in one turn. Use this only to create a new file or fully " + "rewrite a small one. Missing parent directories are created recursively." + ), + params=( + path("project_slug", "Project slug or uid."), + body("path", "Relative file path, e.g. src/app/main.py.", required=True), + body("content", "Full file content.", required=True), + ), + ), + Action( + name="project_read_lines", + method="GET", + path="/projects/{project_slug}/files/lines", + summary="Read a 1-indexed line range of a text file in a project", + description=( + "Returns {path, start, end, total_lines, lines, content} for the requested range. Use it " + "to inspect part of a large file before editing, and to learn total_lines so you can target " + "the right range with the line-edit tools." + ), + params=( + path("project_slug", "Project slug or uid."), + query("path", "Relative file path inside the project.", required=True), + query("start", "First line to read (1-indexed, default 1)."), + query("end", "Last line to read (inclusive); omit for end of file."), + ), + requires_auth=False, + ), + Action( + name="project_replace_lines", + method="POST", + path="/projects/{project_slug}/files/replace-lines", + summary="Replace an inclusive 1-indexed line range of a text file with new content", + description=( + "Surgically rewrites lines start..end (inclusive) with content (which may be any number of " + "lines, or empty to delete the range). The preferred way to edit an existing file: it leaves " + "the rest of the file untouched and never overflows the model output limit." + ), + params=( + path("project_slug", "Project slug or uid."), + body("path", "Relative file path.", required=True), + body("start", "First line to replace (1-indexed).", required=True), + body("end", "Last line to replace (inclusive).", required=True), + body("content", "Replacement text for those lines (empty deletes them)."), + ), + ), + Action( + name="project_insert_lines", + method="POST", + path="/projects/{project_slug}/files/insert-lines", + summary="Insert content before a 1-indexed line of a text file", + description=( + "Inserts content before line 'at' without touching existing lines. Use at=1 to prepend and " + "at=total_lines+1 to insert at the end." + ), + params=( + path("project_slug", "Project slug or uid."), + body("path", "Relative file path.", required=True), + body( + "at", + "Insert before this 1-indexed line (1 prepends, total+1 appends).", + required=True, + ), + body("content", "Text to insert.", required=True), + ), + ), + Action( + name="project_delete_lines", + method="POST", + path="/projects/{project_slug}/files/delete-lines", + summary="Delete an inclusive 1-indexed line range from a text file", + params=( + path("project_slug", "Project slug or uid."), + body("path", "Relative file path.", required=True), + body("start", "First line to delete (1-indexed).", required=True), + body("end", "Last line to delete (inclusive).", required=True), + ), + ), + Action( + name="project_append_file", + method="POST", + path="/projects/{project_slug}/files/append", + summary="Append content to the end of a text file in a project", + description="Adds content as new lines at the end of the file. Use it to grow a large file across turns without resending the whole thing.", + params=( + path("project_slug", "Project slug or uid."), + body("path", "Relative file path.", required=True), + body("content", "Text to append.", required=True), + ), + ), + Action( + name="project_upload_file", + method="POST", + path="/projects/{project_slug}/files/upload", + summary="Upload a local file into a project directory (parents created automatically)", + params=( + path("project_slug", "Project slug or uid."), + upload("file", "Local filesystem path of the file to upload."), + body("path", "Target directory inside the project, empty for the root."), + ), + ), + Action( + name="project_make_dir", + method="POST", + path="/projects/{project_slug}/files/mkdir", + summary="Create a directory (and parents) in a project filesystem", + params=( + path("project_slug", "Project slug or uid."), + body( + "path", "Relative directory path, e.g. src/components.", required=True + ), + ), + ), + Action( + name="project_move_file", + method="POST", + path="/projects/{project_slug}/files/move", + summary="Move or rename a file or directory within a project", + params=( + path("project_slug", "Project slug or uid."), + body("from_path", "Existing path.", required=True), + body("to_path", "New path.", required=True), + ), + ), + Action( + name="project_delete_file", + method="POST", + path="/projects/{project_slug}/files/delete", + summary="Delete a file or directory (recursive) from a project filesystem (project owner, or any project when you are an administrator). Soft delete, confirmation required", + params=( + path("project_slug", "Project slug or uid."), + body("path", "Relative path to delete.", required=True), + confirm(), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/projects.py b/devplacepy/services/devii/actions/catalog/projects.py new file mode 100644 index 00000000..b6f0aae6 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/projects.py @@ -0,0 +1,136 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import ATTACHMENTS, body, confirm, path, query + + +PROJECTS_ACTIONS: tuple[Action, ...] = ( + Action( + name="list_projects", + method="GET", + path="/projects", + summary="List projects", + requires_auth=False, + params=( + query("tab", "Projects tab."), + query("search", "Search project title, description, and author username."), + query("user_uid", "Filter by owner uid."), + query("project_type", "Filter by project type."), + query("before", "Pagination cursor."), + ), + ), + Action( + name="view_project", + method="GET", + path="/projects/{project_slug}", + summary="View a project by slug", + requires_auth=False, + params=( + path( + "project_slug", + "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", + ), + ), + ), + Action( + name="create_project", + method="POST", + path="/projects/create", + summary="Create a new project", + params=( + body("title", "Project title.", required=True), + body("description", "Project description.", required=True), + body("release_date", "Release date."), + body("demo_date", "Demo date."), + body("project_type", "Project type."), + body("platforms", "Supported platforms."), + body("status", "Project status."), + body("attachment_uids", ATTACHMENTS), + ), + ), + Action( + name="edit_project", + method="POST", + path="/projects/edit/{project_slug}", + summary="Edit an existing project", + params=( + path( + "project_slug", + "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", + ), + body("title", "Updated project title.", required=True), + body("description", "Updated project description.", required=True), + body("release_date", "Updated release date."), + body("demo_date", "Updated demo date."), + body("project_type", "Updated project type."), + body("platforms", "Updated supported platforms."), + body("status", "Updated project status."), + ), + ), + Action( + name="delete_project", + method="POST", + path="/projects/delete/{project_slug}", + summary="Delete a project and all its files (your own, or any project when you are an administrator). Soft delete, confirmation required", + params=( + path( + "project_slug", + "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.", + ), + confirm(), + ), + ), + Action( + name="project_set_private", + method="POST", + path="/projects/{project_slug}/private", + summary="Mark a project private (owner-only) or public", + description=( + "Set value=true to hide the project from everyone except its owner (and administrators), " + "or value=false to make it public again. This is a significant change: you MUST ask the " + "user for explicit confirmation BEFORE calling it, and only pass confirm=true once they " + "have agreed. Only the project owner may change this." + ), + params=( + path("project_slug", "Project slug or uid."), + body( + "value", + "true to make the project private, false to make it public.", + required=True, + ), + body( + "confirm", + "Must be true, set only after the user has explicitly confirmed.", + required=True, + ), + ), + ), + Action( + name="project_set_readonly", + method="POST", + path="/projects/{project_slug}/readonly", + summary="Mark a project read-only (immutable files) or writable again", + description=( + "Set value=true to make every file in the project immutable - no writes, edits, line " + "edits, moves, deletes, or uploads succeed afterwards, from anyone including you - or " + "value=false to allow changes again. This is a significant change: you MUST ask the user " + "for explicit confirmation BEFORE calling it, and only pass confirm=true once they have " + "agreed. Only the project owner may change this." + ), + params=( + path("project_slug", "Project slug or uid."), + body( + "value", + "true to make the project read-only, false to make it writable.", + required=True, + ), + body( + "confirm", + "Must be true, set only after the user has explicitly confirmed.", + required=True, + ), + ), + ), +) diff --git a/devplacepy/services/devii/actions/catalog/social.py b/devplacepy/services/devii/actions/catalog/social.py new file mode 100644 index 00000000..01bda99e --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/social.py @@ -0,0 +1,106 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import confirm, path, query + + +SOCIAL_ACTIONS: tuple[Action, ...] = ( + Action( + name="follow_user", + method="POST", + path="/follow/{username}", + summary="Follow a user", + params=(path("username", "Username to follow."),), + ), + Action( + name="unfollow_user", + method="POST", + path="/follow/unfollow/{username}", + summary="Unfollow a user", + params=(path("username", "Username to unfollow."),), + ), + Action( + name="block_user", + method="POST", + path="/block/{username}", + summary="Block a user so their posts, comments and messages are hidden and they cannot notify you", + params=(path("username", "Username to block."),), + ), + Action( + name="unblock_user", + method="POST", + path="/block/unblock/{username}", + summary="Unblock a user", + params=(path("username", "Username to unblock."),), + ), + Action( + name="mute_user", + method="POST", + path="/mute/{username}", + summary="Mute a user so they can no longer create notifications for you", + params=(path("username", "Username to mute."),), + ), + Action( + name="unmute_user", + method="POST", + path="/mute/unmute/{username}", + summary="Unmute a user", + params=(path("username", "Username to unmute."),), + ), + Action( + name="list_followers", + method="GET", + path="/profile/{username}/followers", + summary="List the users who follow a profile", + requires_auth=False, + params=( + path("username", "Username whose followers to list."), + query("page", "Page number, 25 per page."), + ), + ), + Action( + name="list_following", + method="GET", + path="/profile/{username}/following", + summary="List the users a profile follows", + requires_auth=False, + params=( + path("username", "Username whose following to list."), + query("page", "Page number, 25 per page."), + ), + ), + Action( + name="list_media", + method="GET", + path="/profile/{username}", + summary="List a user's uploaded media, newest first", + requires_auth=False, + params=( + path("username", "Username whose media to list."), + query("tab", "Profile tab (defaults to media for this action)."), + query("page", "Page number, 24 per page."), + ), + ), + Action( + name="delete_media", + method="POST", + path="/media/{uid}/delete", + summary="Delete an uploaded media attachment (your own, or anyone's when you are an administrator)", + description=( + "Removes the attachment from the user's Media tab and from any post, project, or " + "gist where it was attached. Soft delete (restorable from admin trash). Show the user " + "the exact media, get explicit confirmation, then call again with confirm=true." + ), + requires_auth=True, + params=(path("uid", "Attachment uid to delete."), confirm()), + ), + Action( + name="view_leaderboard", + method="GET", + path="/leaderboard", + summary="View the leaderboard", + requires_auth=False, + ), +) diff --git a/devplacepy/services/devii/actions/catalog/tools.py b/devplacepy/services/devii/actions/catalog/tools.py new file mode 100644 index 00000000..1ca28992 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/tools.py @@ -0,0 +1,111 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, path + + +TOOLS_ACTIONS: tuple[Action, ...] = ( + Action( + name="seo_diagnostics", + method="POST", + path="/tools/seo/run", + summary="Run an SEO audit on a URL or sitemap", + description=( + "Queues a background SEO diagnostics job and returns {uid, status_url}. Poll the " + "status_url with seo_status until status is 'done', then share the score, grade and " + "report_url. mode='url' audits a single page; mode='sitemap' crawls up to max_pages." + ), + params=( + body("url", "The page URL or sitemap.xml URL to audit.", required=True), + body("mode", "'url' for a single page or 'sitemap' to crawl a sitemap."), + body("max_pages", "Maximum pages to crawl in sitemap mode (1-50)."), + ), + requires_auth=False, + ), + Action( + name="seo_status", + method="GET", + path="/tools/seo/{uid}", + summary="Check an SEO audit and obtain its score once finished", + description=( + "Returns the audit status. When status is 'done', score, grade and report_url are " + "populated; while 'pending' or 'running', poll again shortly." + ), + params=(path("uid", "SEO job uid returned by seo_diagnostics."),), + requires_auth=False, + ), + Action( + name="seo_report", + method="GET", + path="/tools/seo/{uid}/report", + summary="Read a finished SEO audit report", + description=( + "Returns the full audit for a finished SEO job: overall score and grade, per-category " + "scores, per-check results, per-page details, and site-wide checks. Use it after " + "seo_status reports status 'done' to explain what passed, what failed, and how to improve." + ), + params=(path("uid", "SEO job uid returned by seo_diagnostics."),), + requires_auth=False, + ), + Action( + name="seo_meta_status", + method="GET", + path="/tools/seo-meta/{target_type}/{target_uid}", + summary="Read the SEO metadata generated for a content item", + description=( + "Returns the clean SEO title, description and keywords for a published content " + "item. target_type is one of post, project, gist, news or issue and target_uid " + "is its uid (or issue number). When the AI value is not ready yet a plain-content " + "default is returned with status 'pending'." + ), + params=( + path("target_type", "One of post, project, gist, news or issue."), + path("target_uid", "The content uid (or issue number)."), + ), + requires_auth=False, + ), + Action( + name="deepsearch", + method="POST", + path="/tools/deepsearch/run", + summary="Start a deep multi-agent web research job", + description=( + "Queues a background DeepSearch job and returns {uid, status_url}. Poll the " + "status_url with deepsearch_status until status is 'done', then share the " + "score, confidence and session_url. depth (1-4) and max_pages (1-30) control " + "how widely it crawls." + ), + params=( + body("query", "The research question to investigate.", required=True), + body("depth", "Research depth 1-4 (default 2)."), + body("max_pages", "Maximum sources to crawl 1-30 (default 12)."), + ), + requires_auth=False, + ), + Action( + name="deepsearch_status", + method="GET", + path="/tools/deepsearch/{uid}", + summary="Check a DeepSearch job and obtain its metrics once finished", + description=( + "Returns the research status. When status is 'done', score, confidence, " + "source_diversity and session_url are populated; otherwise poll again shortly." + ), + params=(path("uid", "DeepSearch job uid returned by deepsearch."),), + requires_auth=False, + ), + Action( + name="deepsearch_session", + method="GET", + path="/tools/deepsearch/{uid}/session", + summary="Read a finished DeepSearch report", + description=( + "Returns the full cited report for a finished DeepSearch: summary, findings, " + "gaps, sources and metrics." + ), + params=(path("uid", "DeepSearch job uid returned by deepsearch."),), + requires_auth=False, + ), +) diff --git a/devplacepy/services/devii/actions/catalog/uploads.py b/devplacepy/services/devii/actions/catalog/uploads.py new file mode 100644 index 00000000..3a65ec09 --- /dev/null +++ b/devplacepy/services/devii/actions/catalog/uploads.py @@ -0,0 +1,45 @@ +# retoor + +from __future__ import annotations + +from ..spec import Action +from ._shared import body, confirm, path, upload + + +UPLOAD_ACTIONS: tuple[Action, ...] = ( + Action( + name="upload_file", + method="POST", + path="/uploads/upload", + summary="Upload a local file and obtain its attachment uid", + params=(upload("file", "Local filesystem path of the file to upload."),), + ), + Action( + name="attach_url", + method="POST", + path="/uploads/upload-url", + summary="Download a file from a public URL and store it as an attachment, returning its uid", + description=( + "Fetches the file at the given URL on the server (size-capped, SSRF-guarded) and stores it " + "as an attachment exactly like an upload, returning {uid, url, mime_type, ...}. Pass the " + "returned uid in attachment_uids when you create_post, create_project, create_gist, " + "create_comment, create_issue, or send_message to attach it. The file type is taken from the " + "URL or its Content-Type; supply 'filename' (with an allowed extension) when the URL has no " + "usable name. Use this to attach an image or file straight from the internet." + ), + params=( + body("url", "Public http(s) URL of the file to download and attach.", required=True), + body( + "filename", + "Optional filename with an allowed extension, used when the URL has no clear name.", + ), + ), + ), + Action( + name="delete_attachment", + method="DELETE", + path="/uploads/delete/{attachment_uid}", + summary="Delete an uploaded attachment (your own, or anyone's when you are an administrator). Soft delete, confirmation required", + params=(path("attachment_uid", "Uid of the attachment."), confirm()), + ), +)