├── scAdditionalAPIs ├── .gitignore ├── main │ ├── requirements.txt │ └── main.py ├── proxy │ ├── requirements.txt │ └── main.py ├── free-proxy │ ├── requirements.txt │ └── main.py ├── comments-api │ ├── requirements.txt │ ├── API.py │ └── main.py ├── README.md └── Spacefile ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── stale.yml │ ├── codeql.yml │ └── python-publish.yml ├── scratchconnect ├── Warnings.py ├── scScratchTerminal.py ├── scOnlineIDE.py ├── Exceptions.py ├── __init__.py ├── scEncoder.py ├── User.py ├── scCloudEvents.py ├── scImage.py ├── Forum.py ├── TurbowarpCloudConnection.py ├── CloudConnection.py ├── scCloudStorage.py ├── UserCommon.py ├── scCloudRequests.py ├── ScratchConnect.py ├── Studio.py └── Project.py ├── docs ├── assets │ └── images │ │ ├── get_session_id_chrome.png │ │ └── get_session_id_firefox.png ├── login_in_replit.md ├── index.md ├── session_id.md ├── getting_started.md └── the_scratchconnect_class.md ├── .gitignore ├── scScratchTerminal ├── __init__.py └── Terminal.py ├── LICENSE ├── setup.py ├── mkdocs.yml └── CLOUD_REQUESTS.md /scAdditionalAPIs/.gitignore: -------------------------------------------------------------------------------- 1 | .space -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Sid72020123 -------------------------------------------------------------------------------- /scAdditionalAPIs/main/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi -------------------------------------------------------------------------------- /scAdditionalAPIs/proxy/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | fastapi -------------------------------------------------------------------------------- /scAdditionalAPIs/free-proxy/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | fastapi -------------------------------------------------------------------------------- /scAdditionalAPIs/comments-api/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | bs4 3 | fastapi -------------------------------------------------------------------------------- /scratchconnect/Warnings.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Warnings File 3 | """ 4 | 5 | 6 | def warn(message: str) -> None: 7 | print(message) 8 | -------------------------------------------------------------------------------- /docs/assets/images/get_session_id_chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sid72020123/scratchconnect/HEAD/docs/assets/images/get_session_id_chrome.png -------------------------------------------------------------------------------- /docs/assets/images/get_session_id_firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sid72020123/scratchconnect/HEAD/docs/assets/images/get_session_id_firefox.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | test.py 3 | password.txt 4 | session_id.txt 5 | scImage.png 6 | scratchconnect/__pycache__ 7 | scChart/__pycache__ 8 | scScratchTerminal/__pycache__ -------------------------------------------------------------------------------- /scAdditionalAPIs/README.md: -------------------------------------------------------------------------------- 1 | # ScratchConnect Additional APIs 2 | 3 | This directory contains the source code of additional APIs used by the library. 4 | 5 | All the APIs here are hosted on [deta.space](https://deta.space)! -------------------------------------------------------------------------------- /scAdditionalAPIs/main/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/") 6 | def root(): 7 | return {"Error": False, "Message": "ScratchConnect Additional APIs", "Credits": "Made by @Sid72020123 on Scratch"} 8 | 9 | -------------------------------------------------------------------------------- /scScratchTerminal/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Terminal Feature for ScratchConnect Python Library. Version: 1.1 3 | Made by @Sid72020123 on Scratch 4 | 5 | This package uses Rich Python Library 6 | """ 7 | 8 | from scScratchTerminal.Terminal import Terminal 9 | -------------------------------------------------------------------------------- /scratchconnect/scScratchTerminal.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Terminal File 3 | """ 4 | 5 | from scratchconnect.Exceptions import DependencyException 6 | 7 | 8 | def _terminal(sc): 9 | try: 10 | import rich 11 | from scScratchTerminal import Terminal 12 | return Terminal(sc) 13 | except ModuleNotFoundError: 14 | raise DependencyException( 15 | "The dependencies required for the Terminal feature to work were not found. Please install them using the command: 'pip install scratchconnect[terminal]'") 16 | -------------------------------------------------------------------------------- /scAdditionalAPIs/Spacefile: -------------------------------------------------------------------------------- 1 | v: 0 2 | app_name: scAPIs 3 | micros: 4 | - name: main 5 | src: /main 6 | engine: python3.9 7 | public: true 8 | primary: true 9 | 10 | - name: proxy 11 | src: /proxy 12 | engine: python3.9 13 | public: true 14 | path: proxy 15 | 16 | - name: free-proxy 17 | src: /free-proxy 18 | engine: python3.9 19 | public: true 20 | path: free-proxy 21 | 22 | - name: comments-api 23 | src: /comments-api 24 | engine: python3.9 25 | public: true 26 | path: comments 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | - update-v5.0 8 | permissions: 9 | contents: write 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.x 18 | - uses: actions/cache@v2 19 | with: 20 | key: ${{ github.ref }} 21 | path: .cache 22 | - run: pip install mkdocs-material 23 | - run: mkdocs gh-deploy --force 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: '18 21 * * *' 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v3 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: 'Stale issue message' 20 | stale-pr-message: 'Stale pull request message' 21 | stale-issue-label: 'no-issue-activity' 22 | stale-pr-label: 'no-pr-activity' 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "50 3 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /scratchconnect/scOnlineIDE.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code to make it work in Online IDEs 3 | Don't use this code 4 | """ 5 | 6 | from urllib.parse import urlparse 7 | import requests 8 | from requests import request 9 | 10 | headers = {"library": "ScratchConnect.py"} 11 | scratch_endpoints = ["api.scratch.mit.edu", "scratch.mit.edu", "cdn2.scratch.mit.edu"] 12 | 13 | proxy_url = "https://apis.scratchconnect.eu.org/proxy/get?url=" 14 | 15 | 16 | def _get(url, params=None, **kwargs): 17 | if urlparse(url).netloc in scratch_endpoints: 18 | url = proxy_url + url 19 | kwargs["headers"] = headers 20 | return request("GET", url, params=params, **kwargs) 21 | 22 | 23 | def _session_get(self, url, **kwargs): 24 | if urlparse(url).netloc in scratch_endpoints: 25 | url = proxy_url + url 26 | kwargs["headers"] = headers 27 | return request("GET", url, **kwargs) 28 | 29 | 30 | def _change_request_url(): 31 | requests.get = _get 32 | requests.Session.get = _session_get 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Siddhesh Chavan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scAdditionalAPIs/free-proxy/main.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | from requests import get 3 | from fastapi import FastAPI, Request, Response 4 | 5 | app = FastAPI() 6 | 7 | @app.get("/") 8 | def root(): 9 | return {"Error": False, "Message": "Free Proxy is Running!", "Credits": "Made by @Sid72020123 on Scratch"} 10 | 11 | 12 | @app.get("/get") 13 | def proxy(url: str, request: Request, response: Response): 14 | headers = { 15 | "User-Agent": f"Proxy - {randint(1, 100)}!" 16 | } 17 | request_headers = dict(request.headers) 18 | args = dict(request.query_params.multi_items()) 19 | args.pop("url") 20 | try: 21 | r = get(url, params=args, headers=headers) 22 | raw_headers = r.raw.headers.items() 23 | excluded_headers = ["content-encoding", "content-length"] 24 | headers = {} 25 | for (name, value) in raw_headers: 26 | if name.lower() not in excluded_headers: 27 | headers[name] = value 28 | return Response(content=r.content, status_code=r.status_code, headers=headers) 29 | except Exception as E: 30 | return Response(content="Internal Server Error - " + str(E), status_code=500) 31 | -------------------------------------------------------------------------------- /scratchconnect/Exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Exceptions File 3 | """ 4 | 5 | 6 | class InvalidInfo(Exception): 7 | """ 8 | Raised when the username or password is invalid 9 | """ 10 | pass 11 | 12 | 13 | class InvalidUser(Exception): 14 | """ 15 | Raised when the username is invalid 16 | """ 17 | pass 18 | 19 | 20 | class InvalidStudio(Exception): 21 | """ 22 | Raised when the studio is invalid 23 | """ 24 | pass 25 | 26 | 27 | class InvalidProject(Exception): 28 | """ 29 | Raised when the project is invalid 30 | """ 31 | pass 32 | 33 | 34 | class UnauthorizedAction(Exception): 35 | """ 36 | Raised when the action is unauthorized 37 | """ 38 | pass 39 | 40 | 41 | class InvalidCloudValue(Exception): 42 | """ 43 | Raised when the cloud value is invalid 44 | """ 45 | pass 46 | 47 | 48 | class InvalidForumTopic(Exception): 49 | """ 50 | Raised when the forum topic is invalid 51 | """ 52 | pass 53 | 54 | 55 | class DependencyException(Exception): 56 | """ 57 | Raised when the dependencies for ScratchConnect are not installed or found 58 | """ 59 | -------------------------------------------------------------------------------- /scratchconnect/__init__.py: -------------------------------------------------------------------------------- 1 | __name__ = "scratchconnect" 2 | __version__ = "5.0" 3 | __author__ = "Siddhesh Chavan" 4 | __documentation__ = "https://sid72020123.github.io/scratchconnect/" 5 | __doc__ = f""" 6 | ScratchConnect is a Python Library to connect Scratch API and much more. 7 | This library can show/fetch the statistics of Users, Projects, Studios, Forums and also connect and set cloud variables of a project! 8 | Import Statement: 9 | import scratchconnect 10 | Documentation(Tutorial): 11 | For documentation, go to {__documentation__} 12 | Required Libraries: 13 | requests*, re*, json*, time*, traceback*, threading*, urllib*, PIL*, websocket-client 14 | * -> In-built 15 | Optional Libraries: 16 | rich - For Terminal feature 17 | Change Log: 18 | View all the change log at: https://github.com/Sid72020123/scratchconnect#change-log 19 | Credits: 20 | View all the contributors: https://github.com/Sid72020123/scratchconnect#contributors 21 | """ 22 | 23 | print_name = "ScratchConnect" 24 | print(f"{print_name} v{__version__} - {__documentation__}") 25 | 26 | from scratchconnect.ScratchConnect import ScratchConnect 27 | from scratchconnect import Exceptions 28 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | classifiers = [ 4 | 'Development Status :: 5 - Production/Stable', 5 | 'Intended Audience :: Education', 6 | 'Operating System :: OS Independent', 7 | 'License :: OSI Approved :: MIT License', 8 | 'Programming Language :: Python :: 3' 9 | ] 10 | 11 | setup( 12 | name="scratchconnect", 13 | version="5.0", 14 | description="Python Library to connect Scratch API and much more. This library can show the statistics of Users, Projects, Studios, Forums and also connect and set cloud variables of a project!", 15 | long_description=open("README.md").read(), 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/Sid72020123/scratchconnect/", 18 | author="Siddhesh Chavan", 19 | author_email="siddheshchavan2020@gmail.com", 20 | license="MIT", 21 | classifiers=classifiers, 22 | keywords=['connect scratch', 'scratch api', 'api', 'scratch', 'bot', 'scratch bot', 23 | 'scratch cloud', 'scratch cloud variables', 'scratch data', 'scratch stats', 'cloud requests', 24 | 'fast cloud requests'], 25 | packages=["scratchconnect"], 26 | install_requires=['requests', 'websocket-client', 'Pillow'], 27 | extras_require={ 28 | 'terminal': ['scScratchTerminal'] 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /scAdditionalAPIs/proxy/main.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | from requests import get 3 | from fastapi import FastAPI, Request, Response 4 | 5 | app = FastAPI() 6 | 7 | @app.get("/") 8 | def root(): 9 | return {"Error": False, "Message": "ScratchConnect Proxy is Running!", "Credits": "Made by @Sid72020123 on Scratch"} 10 | 11 | 12 | @app.get("/get") 13 | def proxy(url: str, request: Request, response: Response): 14 | headers = { 15 | "User-Agent": f"Proxy - {randint(1, 100)}!" 16 | } 17 | request_headers = dict(request.headers) 18 | args = dict(request.query_params.multi_items()) 19 | args.pop("url") 20 | try: 21 | if request_headers["library"] == "ScratchConnect.py": 22 | try: 23 | r = get(url, params=args, headers=headers) 24 | raw_headers = r.raw.headers.items() 25 | excluded_headers = ["content-encoding", "content-length"] 26 | headers = {} 27 | for (name, value) in raw_headers: 28 | if name.lower() not in excluded_headers: 29 | headers[name] = value 30 | return Response(content=r.content, status_code=r.status_code, headers=headers) 31 | except Exception as E: 32 | return Response(content="Internal Server Error - " + str(E), status_code=500) 33 | else: 34 | return Response(content="Access Denied!", status_code=403) 35 | except KeyError: 36 | return Response(content="Access Denied!", status_code=403) -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: ScratchConnect Docs (WIP) 2 | repo_url: https://github.com/Sid72020123/scratchconnect/ 3 | repo_name: Sid72020123/scratchconnect 4 | 5 | nav: 6 | - Introduction and Installation: 'index.md' 7 | - Getting Started: 'getting_started.md' 8 | - Login/Usage in Replit: 'login_in_replit.md' 9 | - 'API Reference': 10 | - The ScratchConnect class: 'the_scratchconnect_class.md' 11 | 12 | theme: 13 | name: material 14 | icon: 15 | repo: fontawesome/brands/github 16 | palette: 17 | - media: "(prefers-color-scheme: light)" 18 | scheme: default 19 | primary: amber 20 | accent: yellow 21 | toggle: 22 | icon: material/brightness-7 23 | name: Switch to dark mode 24 | - media: "(prefers-color-scheme: dark)" 25 | scheme: slate 26 | primary: amber 27 | accent: yellow 28 | toggle: 29 | icon: material/brightness-4 30 | name: Switch to light mode 31 | font: 32 | text: Roboto 33 | code: Roboto Mono 34 | features: 35 | - navigation.instant 36 | - navigation.expand 37 | - navigation.top 38 | - navigation.footer 39 | - content.code.copy 40 | 41 | extra: 42 | social: 43 | - icon: fontawesome/brands/github 44 | link: https://github.com/Sid72020123/scratchconnect/ 45 | - icon: fontawesome/brands/python 46 | link: https://pypi.org/project/scratchconnect/ 47 | 48 | plugins: 49 | - search 50 | 51 | markdown_extensions: 52 | - admonition 53 | - pymdownx.details 54 | - pymdownx.superfences 55 | - pymdownx.inlinehilite 56 | - toc: 57 | permalink: true 58 | -------------------------------------------------------------------------------- /docs/login_in_replit.md: -------------------------------------------------------------------------------- 1 | # Login in Replit 2 | 3 | ## Setup 4 | 5 | To login on [replit](https://replit.com), you first need to store the session ID of you account in an `environment` variable! 6 | 7 | Follow the steps: 8 | 9 | 1. Read the [session ID](/session_id) guide to get your session ID 10 | 2. **Store the session ID in an `environment` variable in replit.** Read [replit's guide](https://docs.replit.com/programming-ide/workspace-features/storing-sensitive-information-environment-variables) if you need any help 11 | 3. Read/Call the value of your environment variable in your code. A small guide can be seen [here](https://docs.replit.com/programming-ide/workspace-features/storing-sensitive-information-environment-variables#python) 12 | 13 | And you're done! Now you can use the Actual login as documented below: 14 | 15 | ## Actual Replit Login 16 | 17 | To login in [replit](https://replit.com) and other online IDEs, using the following feature: 18 | 19 | ```python title="login_in_replit.py" 20 | import scratchconnect 21 | 22 | # Set a cookie dictionary: 23 | 24 | cookie = { 25 | "Username": "", # Replace the placeholder with your username 26 | "SessionID": "" # Replace the placeholder with your session ID 27 | } 28 | 29 | session = scratchconnect.ScratchConnect(online_ide_cookie=cookie) # Pass the "cookie" dictionary to the online_ide_cookie parameter of the class 30 | ``` 31 | 32 | !!! note "Important Note" 33 | If you login in [replit](https://replit.com), using the `Online IDE Login` feature, only the `GET` type of requests are performed, i.e., you won't be able to perform the interactions to the [Scratch website](https://scratch.mit.edu) like following a user, posting a comment, etc. 34 | 35 | This is because this login feature uses a proxy to request the [Scratch API](https://api.scratch.mit.edu) and your login and sensitive user information suchh as `password` and `session_id`, etc. is not sent to the proxy. This is done to keep your data safe. -------------------------------------------------------------------------------- /scratchconnect/scEncoder.py: -------------------------------------------------------------------------------- 1 | """ 2 | The scratchconnect Encoder File 3 | """ 4 | import string 5 | 6 | ALL_CHARS = list(string.ascii_uppercase + string.ascii_lowercase + 7 | string.digits + string.punctuation + ' ') 8 | 9 | 10 | class Encoder: 11 | """ 12 | DON'T USE THIS 13 | """ 14 | 15 | def __init__(self): 16 | pass 17 | 18 | def encode(self, text, default=" "): 19 | text = str(text) 20 | number = "" 21 | for i in range(0, len(text)): 22 | try: 23 | char = text[i] 24 | index = str(ALL_CHARS.index(char) + 1) 25 | if int(index) < 10: 26 | index = '0' + index 27 | except ValueError: 28 | index = str(ALL_CHARS.index(default) + 1) 29 | if int(index) < 10: 30 | index = '0' + index 31 | number += index 32 | return number 33 | 34 | def decode(self, encoded_code): 35 | encoded_code = str(encoded_code) 36 | i = 0 37 | text = "" 38 | while i < int(len(encoded_code) - 1 / 2): 39 | index = int(encoded_code[i] + encoded_code[i + 1]) - 1 40 | text += ALL_CHARS[index] 41 | i += 2 42 | return text 43 | 44 | def encode_list(self, data, default=" "): 45 | if type(data) != list: 46 | raise TypeError( 47 | "To encode a list, the data should be in list form. To encode a text use the encode() function") 48 | encoded = "" 49 | for i in data: 50 | encoded += f"{self.encode(i, default=default)}00" 51 | return encoded 52 | 53 | def decode_list(self, encoded_list_data): 54 | decoded = [] 55 | i = 0 56 | text = "" 57 | while i < int(len(encoded_list_data) - 1 / 2): 58 | code = encoded_list_data[i] + encoded_list_data[i + 1] 59 | index = int(code) - 1 60 | if code == "00": 61 | decoded.append(text) 62 | text = "" 63 | else: 64 | text += ALL_CHARS[index] 65 | i += 2 66 | return decoded 67 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Docs still in WIP... 2 | 3 | # Introduction and Installation 4 | 5 | ## Introduction 6 | 7 | ![banner](https://u.cubeupload.com/Sid72020123/scratchconnect.png) 8 | 9 | ScratchConnect is a simple, easy-to-use Scratch API wrapper for **Python** 10 | 11 | It is used to _connect_ the Scratch API and get/fetch the information and stats of users, studios, projects, forums, etc. from the [Scratch website](https://scratch.mit.edu/) 12 | 13 | Other than just fetching the content, it can also perform some actions like: 14 | 15 | * Following a user, studio or a forum topic 16 | * Posting comments on a user's profile or a studio and a project 17 | * Setting/Changing the cloud variables of a Project directly just using the Python code 18 | * And much more... 19 | 20 | !!! warning 21 | To use this library, you should have the basic knowledge of Python. Using the library without any knowledge can be risky! 22 | 23 | **Your login information and cookie values are kept safe and are not sent to any other API or website other than the trusted Scratch APIs** 24 | 25 | **The source code of the library can be found on [Github](https://github.com/Sid72020123/scratchconnect)** 26 | 27 | ## Requirements 28 | * [Python](https://python.org/) version `3.6+`. *Possibly download the latest version* 29 | * Possibly a [Scratch](https://scratch.mit.edu/) account *(In case you need to perform some actions on the site)* 30 | 31 | ## Installation 32 | 33 | To install the library, you can do either one of these: 34 | 35 | ### Using pip 36 | 37 | Type the following command in your `command prompt` or `terminal`: 38 | ``` 39 | pip install scratchconnect 40 | ``` 41 | 42 | ### Directly using the Python Code 43 | 44 | Run the following code in Python: 45 | ```python title="install.py" 46 | import os 47 | os.system("pip install scratchconnect") 48 | ``` 49 | 50 | ??? note "Troubles while Installing?" 51 | If you have any trobules while installing the library, then visit [this link](https://packaging.python.org/en/latest/tutorials/installing-packages/) 52 | 53 | !!! note 54 | Make sure you update the library from time to time so that you install and use the latest stable version 55 | 56 | ## Getting Started 57 | 58 | If you are a beginner, check out the [Getting Started](/getting_started) guide -------------------------------------------------------------------------------- /scAdditionalAPIs/comments-api/API.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | 4 | 5 | def _get_comments(URL): 6 | DATA = [] 7 | 8 | page_contents = requests.get(URL).content 9 | 10 | soup = BeautifulSoup(page_contents, "html.parser") 11 | 12 | comments = soup.find_all("li", {"class": "top-level-reply"}) 13 | 14 | if len(comments) == 0: 15 | return None 16 | 17 | for comment in comments: 18 | comment_id = comment.find("div", {"class": "comment"})['data-comment-id'] 19 | user = comment.find("a", {"id": "comment-user"})['data-comment-user'] 20 | content = str(comment.find("div", {"class": "content"}).text).strip() 21 | time = comment.find("span", {"class": "time"})['title'] 22 | 23 | ALL_REPLIES = [] 24 | replies = comment.find_all("li", {"class": "reply"}) 25 | if len(replies) > 0: 26 | hasReplies = True 27 | else: 28 | hasReplies = False 29 | for reply in replies: 30 | r_comment_id = reply.find("div", {"class": "comment"})['data-comment-id'] 31 | r_user = reply.find("a", {"id": "comment-user"})['data-comment-user'] 32 | r_content = str(reply.find("div", {"class": "content"}).text).strip().replace("\n", "").replace( 33 | " ", " ") 34 | r_time = reply.find("span", {"class": "time"})['title'] 35 | reply_data = { 36 | 'CommentID': r_comment_id, 37 | 'User': r_user, 38 | 'Content': r_content, 39 | 'Timestamp': r_time 40 | } 41 | ALL_REPLIES.append(reply_data) 42 | 43 | main_comment = { 44 | 'CommentID': comment_id, 45 | 'User': user, 46 | 'Content': content, 47 | 'Timestamp': time, 48 | 'hasReplies?': hasReplies, 49 | 'Replies': ALL_REPLIES 50 | } 51 | DATA.append(main_comment) 52 | return DATA 53 | 54 | 55 | def get_user_comments(username, page=1): 56 | URL = f"https://scratch.mit.edu/site-api/comments/user/{username}/?page={page}" 57 | return _get_comments(URL=URL) 58 | 59 | 60 | def get_studio_comments(studio_id, page=1): 61 | URL = f"https://scratch.mit.edu/site-api/comments/gallery/{studio_id}/?page={page}" 62 | return _get_comments(URL=URL) 63 | 64 | def get_project_comments(project_id, page=1): 65 | URL = f"https://scratch.mit.edu/site-api/comments/project/{project_id}/?page={page}" 66 | return _get_comments(URL=URL) -------------------------------------------------------------------------------- /scratchconnect/User.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Users File 3 | """ 4 | 5 | from requests.models import Response 6 | import json 7 | 8 | from scratchconnect.UserCommon import UserCommon 9 | from scratchconnect.scOnlineIDE import _change_request_url 10 | from scratchconnect import Exceptions 11 | 12 | _website = "scratch.mit.edu" 13 | _api = f"https://api.{_website}" 14 | 15 | 16 | class User(UserCommon): 17 | def __init__(self, username, client_username, session, logged_in, online_ide): 18 | """ 19 | The User Class to connect a Scratch user. 20 | :param username: The username 21 | """ 22 | super().__init__(username, session, 23 | online_ide) # Get other properties and methods from the parent(UserCommon) class 24 | self.username = username 25 | self.client_username = client_username 26 | self._logged_in = logged_in 27 | self._user_link = f"{_api}/users/{self.username}" 28 | self.session = session 29 | if online_ide: 30 | _change_request_url() 31 | self.update_data() 32 | 33 | def post_comment(self, content: str, commentee_id: int = "", parent_id: int = "") -> Response: 34 | """ 35 | Post a comment on the user's profile 36 | :param content: The comment 37 | """ 38 | if self._logged_in is False: 39 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 40 | data = { 41 | "commentee_id": commentee_id, 42 | "content": content, 43 | "parent_id": parent_id, 44 | } 45 | return self.session.post(f"https://scratch.mit.edu/site-api/comments/user/{self.username}/add/", 46 | data=json.dumps(data)) 47 | 48 | def reply_comment(self, content: str, comment_id: int) -> Response: 49 | """ 50 | Reply a comment 51 | :param content: The message 52 | :param comment_id: The ID of the comment you want to reply 53 | """ 54 | if self._logged_in is False: 55 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 56 | return self.post_comment(content=content, parent_id=comment_id) 57 | 58 | def report(self, field: str) -> Response: 59 | """ 60 | Report an user 61 | :param field: The field or the reason of report. 62 | """ 63 | if self._logged_in is False: 64 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 65 | if self.username == self.client_username: 66 | raise Exceptions.UnauthorizedAction("You are not allowed to do that!") 67 | data = {"selected_field": field} 68 | return self.session.post(f"https://scratch.mit.edu/site-api/users/all/{self.username}/report/", 69 | data=json.dumps(data)) 70 | -------------------------------------------------------------------------------- /docs/session_id.md: -------------------------------------------------------------------------------- 1 | # Session ID Guide 2 | 3 | ???+ question "What is session ID?" 4 | 5 | Sesssion ID is a type of cookie value that is set by the Scratch website on your local computer 6 | 7 | The website uses the Session ID as a type of security check, i.e, for example, if you follow a user or post a comment, the Scratch website first checks if the session ID of your account is correct and then allows you to perform that action 8 | 9 | The Scratch API not only uses the session ID to validate/check the requests but also certain other cookie values which are equally important as the session ID but the ScratchConnect library requires only the session ID using which it fetches the other cookie types! 10 | 11 | *Note: The session ID automatically changes (maybe sometimes) when you login to the Scratch website* 12 | 13 | ## Steps to get the Session ID 14 | 15 | Following are the 2 ways using which you can get the session ID of your account: 16 | 17 | ### Using the Python Code 18 | 19 | You can login using ScratchConnect locally on your computer, using the code below: 20 | 21 | ```python title="get_session_id.py" 22 | import scratchconnect 23 | 24 | # Replace the placeholders below with you actual Scratch information 25 | 26 | session = scratchconnect.ScratchConnect("Username", "Password") 27 | 28 | with open("session_id.txt", "w") as file: 29 | file.write(session.session_id) # Write the session ID in a text file called "session_id.txt" 30 | 31 | # You can open the text file and read/copy the session ID 32 | ``` 33 | 34 | ### From your browser 35 | 36 | Follow the steps: 37 | 38 | *(It is advised to follow only the steps below when you open the developer tools to keep your account secure, if you don't know what you're doing. Remember: Never share your session ID with anyone!)* 39 | 40 | 1. Login to the [Scratch](https://scratch.mit.edu/) website 41 | 2. Open the developer tools by right clicking anywhere on the page and by clicking the *"Inspect"* option. You can open the developer tools on `Windows` OS using the shortcut `Control + Shift + C` and on `Mac` OS using the shortcut `Command + Option + C` depending on your browser 42 | 3. Once you're done, follow the steps according to your browser: 43 | 44 | > For `Firefox` browser: 45 | > > Click the `Storage` tab; In the `Cookies` section click the Scratch website's name. There you will find a key with the name as `scratchsessionsid`. Copy the value corresponding value to that key. This is your session ID 46 | 47 | > > Image *(The cookie values are censored(in red) for security)*: ![image](/assets/images/get_session_id_firefox.png) 48 | 49 | 50 | 51 | > For `Chrome` browser: 52 | > > Click the `Application` tab; In the `Storage` section, click the `Cookies` option and then the Scratch website's name. There you will find a key with the name as `scratchsessionsid`. Copy the value corresponding value to that key. This is your session ID 53 | 54 | > > Image *(The cookie values are censored(in red) for security)*: ![image](/assets/images/get_session_id_chrome.png) 55 | 56 | ## Final Important Note 57 | 58 | The session ID is very important. **Do NOT share it with anyone.** 59 | 60 | Also, remember to store the session ID in `environment` variables if you are using/running the code in online IDEs like [replit](https://replit.com)! 61 | 62 | While sharing your code, make sure that you remove the sensitive information from your file! -------------------------------------------------------------------------------- /scratchconnect/scCloudEvents.py: -------------------------------------------------------------------------------- 1 | """ 2 | The ScratchConnect Cloud Events Class 3 | """ 4 | 5 | import time 6 | from threading import Thread 7 | 8 | 9 | class CloudEvents: 10 | def __init__(self, cloud_object_type, cloud_object): 11 | self.t = None 12 | if cloud_object_type in ["Scratch", "Turbowarp"]: 13 | self.cloud_object_type = cloud_object_type 14 | else: 15 | raise TypeError("Invalid Cloud Type Provided!") 16 | self.cloud_object = cloud_object 17 | self._event_functions = { 18 | "connect": None, 19 | "create": None, 20 | "delete": None, 21 | "set": None, 22 | "disconnect": None 23 | } 24 | self.run = False 25 | 26 | def on(self, e_type: str): 27 | """ 28 | Decorator 29 | """ 30 | 31 | def wrapper(func): 32 | if e_type not in list(self._event_functions.keys()): 33 | raise TypeError( 34 | f"Invalid Event Type '{e_type}'. Use only one from {list(self._event_functions.keys())}") 35 | else: 36 | self._event_functions[e_type] = func 37 | 38 | return wrapper 39 | 40 | def emit(self, e_type, **data): 41 | """ 42 | Don't use this! 43 | """ 44 | func = self._event_functions[e_type] 45 | if func is not None: 46 | if e_type in ['connect', 'disconnect']: 47 | func() 48 | else: 49 | func(data) 50 | 51 | def _event(self, up): 52 | """ 53 | Don't use this! 54 | """ 55 | data = "" 56 | while self.run: 57 | live_data = self.cloud_object.get_variable_data()[0] 58 | if data != live_data: 59 | data = live_data 60 | emit_action = "" 61 | if self.cloud_object_type == "Scratch": 62 | action = data['Action'].split("_")[0] 63 | if action == "set": 64 | emit_action = "set" 65 | elif action == "del": 66 | emit_action = "delete" 67 | elif action == "create": 68 | emit_action = "create" 69 | elif self.cloud_object_type == "Turbowarp": 70 | emit_action = data["method"] 71 | 72 | if self.cloud_object_type == "Scratch": 73 | self.emit(emit_action, user=data['User'], action=data['Action'], 74 | variable_name=data['Name'], value=data['Value'], 75 | timestamp=data['Timestamp']) 76 | elif self.cloud_object_type == "Turbowarp": 77 | self.emit(emit_action, action=data['method'], 78 | variable_name=data['name'], value=data['value']) 79 | time.sleep(up) 80 | 81 | def start(self, update_time: int = 1) -> None: 82 | """ 83 | Start the events loop 84 | :param update_time: The update time 85 | """ 86 | self.cloud_object._make_connection() 87 | self.run = True 88 | self.t = Thread(target=self._event, args=(update_time,)) 89 | self.t.start() 90 | 91 | def stop(self) -> None: 92 | """ 93 | Stop the events loop 94 | """ 95 | self.run = False 96 | -------------------------------------------------------------------------------- /scAdditionalAPIs/comments-api/main.py: -------------------------------------------------------------------------------- 1 | from API import * 2 | from fastapi import FastAPI 3 | 4 | description = "Simple API to get the comments of a Scratch User, Studio and Project in JSON format!" 5 | 6 | tags_metadata = [ 7 | { 8 | "name": "root", 9 | "description": "Root Page.", 10 | }, 11 | { 12 | "name": "user", 13 | "description": "Get the comments of a Scratch User.", 14 | }, 15 | { 16 | "name": "studio", 17 | "description": "Get the comments of a Scratch Studio.", 18 | }, 19 | { 20 | "name": "project", 21 | "description": "Get the comments of a Scratch Project.", 22 | }, 23 | ] 24 | 25 | app = FastAPI( 26 | title="Scratch Comments API", 27 | description=description, 28 | version="1.0", 29 | docs_url="/docs", 30 | openapi_tags=tags_metadata 31 | ) 32 | 33 | 34 | @app.get("/", tags=["root"]) 35 | async def root(): 36 | return {"Name": "Scratch Comments API", "Version": "1.0", 37 | "Description": "API to get the comment data of a Scratch User, Studio, Project in JSON format", 38 | "Documentation": "Go to /docs endpoint", "Made by": "Sid72020123"} 39 | 40 | 41 | @app.get("/user/", tags=["user"]) 42 | async def user(username: str, limit: int = 5, page: int = 1): 43 | data = [] 44 | try: 45 | comment_data = get_user_comments(username=username, page=page) 46 | if comment_data is None: 47 | return {"Error": True, "Info": "User Not Found or has no comments"} 48 | l = 0 49 | p = page 50 | while l < limit: 51 | try: 52 | comment = comment_data[l] 53 | data.append(comment) 54 | except IndexError: 55 | p += 1 56 | comment_data = get_user_comments(username=username, page=p) 57 | if comment_data is None: 58 | break 59 | l += 1 60 | except: 61 | return {"Error": True, "Message": "An Error Occurred!"} 62 | return data 63 | 64 | 65 | @app.get("/studio/", tags=["studio"]) 66 | async def studio(id: int, limit: int = 5, page: int = 1): 67 | data = [] 68 | try: 69 | comment_data = get_studio_comments(studio_id=id, page=page) 70 | if comment_data is None: 71 | return {"Error": True, "Info": "Studio Not Found or has no comments"} 72 | l = 0 73 | p = page 74 | while l < limit: 75 | try: 76 | comment = comment_data[l] 77 | data.append(comment) 78 | except IndexError: 79 | p += 1 80 | comment_data = get_studio_comments(studio_id=id, page=page) 81 | if comment_data is None: 82 | break 83 | l += 1 84 | except: 85 | return {"Error": True, "Message": "An Error Occurred!"} 86 | return data 87 | 88 | 89 | @app.get("/project/", tags=["project"]) 90 | async def project(id: int, limit: int = 5, page: int = 1): 91 | data = [] 92 | try: 93 | comment_data = get_project_comments(project_id=id, page=page) 94 | if comment_data is None: 95 | return {"Error": True, "Info": "Project Not Found or has no comments"} 96 | l = 0 97 | p = page 98 | while l < limit: 99 | try: 100 | comment = comment_data[l] 101 | data.append(comment) 102 | except IndexError: 103 | p += 1 104 | comment_data = get_project_comments(project_id=id, page=page) 105 | if comment_data is None: 106 | break 107 | l += 1 108 | except: 109 | return {"Error": True, "Message": "An Error Occurred!"} 110 | return data -------------------------------------------------------------------------------- /scratchconnect/scImage.py: -------------------------------------------------------------------------------- 1 | """ 2 | ScratchConnect Image Class 3 | """ 4 | 5 | import requests 6 | from PIL import Image as pyImage 7 | from scratchconnect.scOnlineIDE import _change_request_url 8 | 9 | _scratch_api = "https://api.scratch.mit.edu/" 10 | 11 | 12 | class Image: 13 | def __init__(self, online_ide): 14 | self.length = [] 15 | self._image_size = None 16 | self.image_success = None 17 | self.name = None 18 | self.hex_values = "" 19 | if online_ide: 20 | _change_request_url() 21 | 22 | def _shorten_code(self, r, g, b): 23 | """ 24 | Don't use this 25 | """ 26 | decimal = str((r * 256 * 256) + (g * 256) + b) 27 | code = ("0" * (8 - len(decimal))) + str(decimal) 28 | return code 29 | 30 | def download_image(self, url: str, name: str) -> None: 31 | """ 32 | Download the image 33 | """ 34 | self.name = name 35 | try: 36 | response = requests.get(url).content 37 | with open(f"{self.name}.png", 'wb') as file: 38 | file.write(response) 39 | self.image_success = True 40 | except: 41 | self.image_success = False 42 | 43 | def resize_image(self, size: tuple, name: str, maintain_aspect_ratio: bool = True) -> None: 44 | """ 45 | Resize the given image 46 | :param size: The size (in tuple format) 47 | :param name: The new name of the image you want to save 48 | :param maintain_aspect_ratio: Set it to "True" if you want to maintain the aspect ratio of the image while resizing 49 | """ 50 | image = pyImage.open(f"{self.name}.png") 51 | if maintain_aspect_ratio: 52 | image.thumbnail(size) 53 | image.save(f"{name}.png") 54 | else: 55 | new_image = image.resize(size) 56 | new_image.save(f"{name}.png") 57 | self.name = name 58 | 59 | def get_user_image(self, query: str, size: int = 32, name: str = "scImage") -> None: 60 | """ 61 | Get the image of a user on Scratch 62 | """ 63 | user_id = requests.get(f"{_scratch_api}users/{query}").json()["id"] 64 | url = f"https://cdn2.scratch.mit.edu/get_image/user/{user_id}_{size}x{size}.png?v=" 65 | self.download_image(url, name) 66 | 67 | def get_studio_image(self, studio_id: int, name: str = "scImage") -> None: 68 | """ 69 | Get the image of a studio on Scratch 70 | """ 71 | url = f"https://uploads.scratch.mit.edu/get_image/gallery/{studio_id}_170x100.png" 72 | self.download_image(url, name) 73 | 74 | def get_project_image(self, project_id: str, size: int = 32, name: str = "scImage") -> None: 75 | """ 76 | Get the image of a project on Scratch 77 | """ 78 | url = f"https://uploads.scratch.mit.edu/get_image/project/{project_id}_{size}x{size}.png" 79 | self.download_image(url, name) 80 | 81 | def encode_image(self) -> bool | None: 82 | """ 83 | Encode the image data to numbers. Use the function get_data() to get the encoded data 84 | """ 85 | try: 86 | if self.image_success in [False, None]: 87 | return False 88 | image = pyImage.open(f"{self.name}.png").convert('RGBA') 89 | 90 | # Making the transparent background of the image white rather than black 91 | background = pyImage.new('RGBA', image.size, (255, 255, 255)) 92 | alpha_composite = pyImage.alpha_composite(background, image) 93 | alpha_composite.save(f"{self.name}.png", 'PNG') 94 | 95 | image = pyImage.open(f"{self.name}.png").convert('RGBA') 96 | self._image_size = image.size 97 | try: 98 | is_animated = image.is_animated 99 | except: 100 | is_animated = False 101 | if is_animated: 102 | image.seek(0) 103 | self._image_size = image.size 104 | frame = image.convert("RGB").getdata() 105 | image_data = list(frame) 106 | else: 107 | image_data = list(image.convert("RGB").getdata()) 108 | hex_values = "" 109 | for pixel_color in image_data: 110 | hex_values += self._shorten_code(pixel_color[0], pixel_color[1], pixel_color[2]) 111 | self.hex_values = hex_values 112 | return True 113 | except Exception as E: 114 | print(E) 115 | return None 116 | 117 | def get_image_data(self) -> str: 118 | """ 119 | Get the encoded image data 120 | """ 121 | return self.hex_values 122 | 123 | def get_size(self) -> tuple: 124 | """ 125 | Get the size of the image 126 | """ 127 | return self._image_size 128 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Basic Setup/Login 4 | 5 | Once you have installed `ScratchConnect`, import that using the `import` statement in Python: 6 | 7 | ```python title="simple_import.py" 8 | import scratchconnect # Import the library 9 | 10 | # Replace the "Username" and "Password" placeholders below with you actual Scratch username and password! 11 | session = scratchconnect.ScratchConnect("Username", "Password") # Create a new object of the ScratchConnect class 12 | ``` 13 | 14 | !!! warning "Important Note regarding the usage in online IDEs" 15 | If you are using online IDEs like [replit](https://replit.com), please use the `environment` variables to store the sensitive information about your Scratch accounts like the `username`, `password` and `session id`. 16 | 17 | ## Cookie Login 18 | 19 | ??? question "How to get the session ID?" 20 | To get the session ID of your account, please read the [Session ID](session_id.md) guide 21 | 22 | To login with cookie *(In case you don't want to login with your password)*, you can use the cookie login. Example: 23 | 24 | ```python title="cookie_login.py" 25 | import scratchconnect 26 | 27 | # Set a cookie dictionary: 28 | 29 | cookie = { 30 | "Username": "", # Replace the placeholder with your username 31 | "SessionID": "" # Replace the placeholder with your session ID 32 | } 33 | 34 | session = scratchconnect.Scratchconnect(cookie=cookie) # Pass the "cookie" dictionary to the cookie parameter of the class 35 | ``` 36 | 37 | **Or more better example would be to store the session ID in a file and then read that in the program. To store the session ID in a file, use [this](/session_id/#using-the-python-code) code. Example:** 38 | 39 | ```python title="cookie_login.py" 40 | import scratchconnect 41 | 42 | session_id = "" 43 | with open("session_id.txt", "r") as file: 44 | session_id = file.read() 45 | 46 | cookie = { 47 | "Username": "", # Replace the placeholder with your username 48 | "SessionID": session_id 49 | } 50 | 51 | session = scratchconnect.Scratchconnect(cookie=cookie) # Pass the "cookie" dictionary to the cookie parameter of the class 52 | ``` 53 | 54 | !!! note 55 | **Once you login using a cookie, the library may give a warning just to inform you that some features of the library may not work if the cookie values are wrong!** 56 | 57 | !!! warning "Important Note regarding the use of Cookie Login" 58 | If you are using cookie login in online IDEs like [replit](https://replit.com), please use the `environment` variables to store the sensitive information about your Scratch accounts like the `username`, `password` and `session id`. 59 | 60 | ## Advanced Cookie Login 61 | 62 | ??? question "How to get the session ID?" 63 | To get the session ID of your account, please read the [Session ID](session_id.md) guide 64 | 65 | This feature is the combination of both the `Basic` and the `Cookie` login 66 | 67 | Using this feature, the library will automatically login using a cookie in case the basic login was not successful 68 | 69 | Example to use this: 70 | 71 | ```python title="advanced_cookie_login.py" 72 | import scratchconnect 73 | 74 | cookie = { 75 | "Username": "", 76 | "SessionID": "" 77 | } 78 | 79 | session = scratchconnect.ScratchConnect(username="Username", password="Password", cookie=cookie, auto_cookie_login=True) # Set the "auto_cookie_login" parameter of the ScratchConnect class to "True" to enable the advanced login 80 | ``` 81 | 82 | The above code will perform the cookie login if the basic login was unsuccessful! 83 | 84 | !!! note 85 | **Once you login using a cookie, the library may give a warning just to inform you that some features of the library may not work if the cookie values are wrong!** 86 | 87 | !!! warning "Important Note regarding the use of Advanced Cookie Login" 88 | If you are using cookie login in online IDEs like [replit](https://replit.com), please use the `environment` variables to store the sensitive information about your Scratch accounts like the `username`, `password` and `session id`. 89 | 90 | ## No login! 91 | 92 | If you don't want to perform the actions such as posting a comment, following a user, etc. and only want to `GET` the data from the Scratch API, then you can use the library without any login! 93 | 94 | See an example: 95 | 96 | ```python title="no_login.py" 97 | import scratchconnect 98 | 99 | session = scratchconnect.ScratchConnect() # Leave all the parameters empty to use the library without login! 100 | ``` 101 | 102 | !!! note 103 | **If you use the library without login, it will just give a `warning` that the basic and cookie login is failed. This is normal and you can still continue running the code/ using the library.** 104 | 105 | ## Login in replit 106 | 107 | To login in [replit](https://replit.com), you have to use another type of login as the Scratch API had recently blocked the requests coming from [replit](https://replit.com) 108 | 109 | Read the [Login in replit](/login_in_replit) guide 110 | 111 | !!! note "Important Note" 112 | ScratchConnect will not allow a **banned** account to login and will raise an error. -------------------------------------------------------------------------------- /scratchconnect/Forum.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Forum File 3 | """ 4 | import requests 5 | from requests.models import Response 6 | 7 | from scratchconnect.scOnlineIDE import _change_request_url 8 | from scratchconnect import Exceptions 9 | 10 | _website = "scratch.mit.edu" 11 | _login = f"https://{_website}/login/" 12 | _api = f"https://api.{_website}" 13 | 14 | 15 | class Forum: 16 | def __init__(self, id, client_username, headers, logged_in, online_ide, session): 17 | """ 18 | The Main Forum Class 19 | :param id: The id of the forum 20 | """ 21 | self.f_id = str(id) 22 | self.client_username = client_username 23 | self.headers = headers 24 | self._logged_in = logged_in 25 | self.session = session 26 | if online_ide: 27 | _change_request_url() 28 | self.update_data() 29 | 30 | def update_data(self) -> None: 31 | """ 32 | Update the data 33 | """ 34 | try: 35 | data = requests.get(f"https://scratchdb.lefty.one/v3/forum/topic/info/{self.f_id}").json() 36 | except KeyError: 37 | raise Exceptions.InvalidForumTopic(f"Forum with ID - '{self.f_id}' doesn't exist!") 38 | self.f_title = data['title'] 39 | self.f_category = data['category'] 40 | self.f_is_closed = data['closed'] == 1 41 | self.f_is_deleted = data['deleted'] == 1 42 | self.f_time = data['time'] 43 | self.f_post_count = data["post_count"] 44 | 45 | def id(self) -> str: 46 | """ 47 | Returns the id of the forum 48 | """ 49 | return self.f_id 50 | 51 | def title(self) -> str: 52 | """ 53 | Returns the title of the forum 54 | """ 55 | return self.f_title 56 | 57 | def category(self) -> str: 58 | """ 59 | Returns the category of the forum 60 | """ 61 | return self.f_category 62 | 63 | def is_closed(self) -> bool: 64 | """ 65 | Returns whether the forum is closed or not 66 | """ 67 | return self.f_is_closed 68 | 69 | def is_deleted(self) -> bool: 70 | """ 71 | Returns whether the forum is deleted or not 72 | """ 73 | return self.f_is_deleted 74 | 75 | def time(self) -> dict: 76 | """ 77 | Returns the activity of the forum 78 | """ 79 | return self.f_time 80 | 81 | def post_count(self) -> int: 82 | """ 83 | Returns the total post count of the forum 84 | """ 85 | return self.f_post_count 86 | 87 | def follow(self) -> Response: 88 | """ 89 | Follow a Forum 90 | """ 91 | if self._logged_in is False: 92 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 93 | self.headers['referer'] = f"https://scratch.mit.edu/discuss/topic/{self.id}/" 94 | return self.session.post(f"https://scratch.mit.edu/discuss/subscription/topic/{self.id}/add/", 95 | headers=self.headers) 96 | 97 | def unfollow(self) -> Response: 98 | """ 99 | Unfollow a Forum 100 | """ 101 | if self._logged_in is False: 102 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 103 | self.headers['referer'] = f"https://scratch.mit.edu/discuss/topic/{self.id}/" 104 | return self.session.post(f"https://scratch.mit.edu/discuss/subscription/topic/{self.id}/delete/", 105 | headers=self.headers) 106 | 107 | def posts(self, page: int = 0, order: str = "newest") -> list: 108 | """ 109 | Get the post in Forum Topic of a specified page. Images and some other stuff will not appear! 110 | :param page: The page. Default (start) value is 0 111 | :param order: Order to sort posts by, defaults to "newest", possible options include "oldest" 112 | The data os fetched from Scratch DB. So, it may not be up-to-date! 113 | """ 114 | return requests.get( 115 | f"https://scratchdb.lefty.one/v3/forum/topic/posts/{self.id()}/{page}?o={order}").json() 116 | 117 | def ocular_reactions(self, post_id: int) -> dict: 118 | """ 119 | Get the ocular reactions 120 | :param post_id: The id of the post 121 | """ 122 | return requests.get(f"https://my-ocular.jeffalo.net/api/reactions/{post_id}").json() 123 | 124 | def topic_post_history(self, usernames: str = "total", segment: str = "1", range: str = "30") -> dict: 125 | """ 126 | Get the post history of the topic 127 | :param usernames: Values like "total" -> Gives all the data of the users who posted in that topic, "detail" -> Gives individual user's data, or you can also put any username 128 | :param segment: The length of time between each segment, defaults to 1 day. Possible special cases include year(365) or month(30) 129 | :param range: Range of how far back to get history, defaults to 30 days. Possible special cases include year(365) or month(30) 130 | """ 131 | return requests.get( 132 | f"https://scratchdb.lefty.one/v3/forum/topic/graph/{self.f_id}/{usernames}?segment={segment}&range={range}").json() 133 | -------------------------------------------------------------------------------- /scratchconnect/TurbowarpCloudConnection.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Turbowarp Cloud Variables File. 3 | Go to https://scratch.mit.edu/projects/578255313/ for the Scratch Encode/Decode Engine! 4 | """ 5 | import ssl # Used if the Certificate is expired specially in Windows 10 machine 6 | import json 7 | import websocket 8 | from ssl import SSLEOFError, SSLError 9 | 10 | from scratchconnect import Exceptions 11 | from scratchconnect.scEncoder import Encoder 12 | from scratchconnect.scCloudEvents import CloudEvents 13 | 14 | _website = "scratch.mit.edu" 15 | _login = f"https://{_website}/login/" 16 | _api = f"api.{_website}" 17 | 18 | 19 | class TurbowarpCloudConnection: 20 | def __init__(self, project_id, username): 21 | """ 22 | Main class to connect turbowarp cloud variables 23 | """ 24 | self.project_id = project_id 25 | self.username = username 26 | self._cloud_d = None 27 | self._event = CloudEvents("Turbowarp", self) 28 | self._make_connection() 29 | self.encoder = Encoder() 30 | 31 | def _send_packet(self, packet): 32 | """ 33 | Don't use this 34 | """ 35 | self._ws.send(json.dumps(packet) + "\n") 36 | 37 | def _make_connection(self): 38 | """ 39 | Don't use this 40 | """ 41 | self._ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE}) 42 | self._ws.connect("wss://clouddata.turbowarp.org", 43 | origin="https://turbowarp.org", 44 | enable_multithread=True) 45 | self._send_packet( 46 | { 47 | "method": "handshake", 48 | "user": self.username, 49 | "project_id": str(self.project_id), 50 | } 51 | ) 52 | self._event.emit('connect') 53 | 54 | def get_variable_data(self) -> list: 55 | """ 56 | Returns the cloud variable data 57 | """ 58 | self.set_cloud_variable("@sc_temp", "123") 59 | data = [] 60 | for d in self._cloud_d.split("\n"): 61 | one = json.loads(d) 62 | if one["name"] != "☁ @sc_temp": 63 | data.append(one) 64 | if len(data) == 0: 65 | return None 66 | return data 67 | 68 | def get_cloud_variable_value(self, variable_name: str, limit: int) -> list: 69 | value = [] 70 | data = self.get_variable_data() 71 | for d in data: 72 | if d["name"] == f"☁ {variable_name}": 73 | value.append(d["value"]) 74 | return value[0:limit] 75 | 76 | def set_cloud_variable(self, variable_name: str, value: int | str) -> bool: 77 | """ 78 | Set a cloud variable 79 | :param variable_name: Variable name 80 | :param value: Variable value 81 | """ 82 | if str(value).isdigit() and value == '': 83 | raise Exceptions.InvalidCloudValue(f"The Cloud Value should be a set of digits and not '{value}'!") 84 | 85 | try: 86 | if len(str(value)) > 256: 87 | raise ValueError( 88 | "Turbowarp has Cloud Variable Limit of 256 Characters per variable. Try making the value shorter!") 89 | if str(variable_name.strip())[0] != "☁": 90 | n = f"☁ {variable_name.strip()}" 91 | else: 92 | n = f"{variable_name.strip()}" 93 | packet = { 94 | "method": "set", 95 | "name": n, 96 | "value": str(value), 97 | "user": self.username, 98 | "project_id": str(self.project_id), 99 | } 100 | self._send_packet(packet) 101 | self._cloud_d = self._ws.recv() 102 | return True 103 | except (ConnectionAbortedError, BrokenPipeError, SSLEOFError, SSLError): 104 | self._event.emit('disconnect') 105 | self._make_connection() 106 | return False 107 | 108 | def encode(self, text: str, default: str = " ") -> str: 109 | """ 110 | Encode a text. For example: A -> 1 111 | Go to https://scratch.mit.edu/projects/578255313/ for the Scratch Engine! 112 | :param text: The text to encode 113 | :param default: The default value to encode when the character found is not accepted by the encoder 114 | """ 115 | return self.encoder.encode(text, default=default) 116 | 117 | def decode(self, encoded_text: str | int) -> str: 118 | """ 119 | Decode a text. For example: 1 -> A 120 | Go to https://scratch.mit.edu/projects/578255313/ for the Scratch Engine! 121 | :param encoded_text: The text to decode 122 | """ 123 | return self.encoder.decode(encoded_text) 124 | 125 | def encode_list(self, data: list, default: str = " ") -> str: 126 | """ 127 | Encode a Python List 128 | :param data: The list to encode 129 | :param default: The default value to encode when the character found is not accepted by the encoder 130 | """ 131 | return self.encoder.encode_list(data, default=default) 132 | 133 | def decode_list(self, encoded_data: str | int) -> list: 134 | """ 135 | Decode a Python List 136 | :param encoded_data: The data to be decoded 137 | """ 138 | return self.encoder.decode_list(encoded_data) 139 | 140 | def create_cloud_event(self) -> CloudEvents: 141 | """ 142 | Create a Cloud Event 143 | """ 144 | return self._event 145 | -------------------------------------------------------------------------------- /CLOUD_REQUESTS.md: -------------------------------------------------------------------------------- 1 | # ScratchConnect Cloud Requests 2 | 3 | This new feature was first released in version ```4.0.0``` of the ScratchConnect Python Library! 4 | 5 | I was inspired to make this feature from @TimMcCool's scratchattach Python library. 6 | 7 | **Using this feature, you will be able to send data to-and-from the Scratch project and your Python program.** 8 | 9 | **Features:** 10 | * Send any length of data without any data loss! (Tested in a good internet connection) 11 | * Send arguments faster... 12 | * You can send any number of data length (or almost unlimited characters of data) to the Scratch Project in much less time! 13 | * This feature is more fast compared to the Cloud Storage feature of this library 14 | * This feature can handle many requests being sent at the same time using a queue system 15 | * It has a built-in way to encode and send images to the Scratch Project! 16 | * Other than ```str``` and ```int``` return data types, it also supports ```list``` and ```dict```(only the dictionary values are sent to the project). ```int``` values are not encoded which reduces the response time 17 | * It also has an events feature 18 | * And more... 19 | 20 | **This feature also needs a Scratch script/code to make it work. The Scratch script can be found [here](https://scratch.mit.edu/projects/801780213/)** 21 | 22 | Download that project on your computer and then later upload it on the Scratch Website. 23 | 24 | In that Scratch Project, click "See Inside" and scroll to the extreme top-left to read the instructions. Please read them first before using the code... 25 | 26 | ## Setup: 27 | You have to make a setup using your Python program, like: 28 | 29 | ### Basic Setup: 30 | ```python 31 | import scratchconnect 32 | 33 | user = scratchconnect.ScratchConnect(username="Username", password="Password") 34 | project = user.connect_project("") # Connect a project 35 | 36 | cloud_requests = project.create_cloud_requests(handle_all_errors=True) # Create a new cloud request. Set the "handle_all_errors" parameter to True if you want to handle all errors... 37 | ``` 38 | 39 | ### Enable Logs: 40 | This feature also has logs where it prints the events. See the following code to enable them: 41 | ```python 42 | import scratchconnect 43 | 44 | user = scratchconnect.ScratchConnect(username="Username", password="Password") 45 | project = user.connect_project("") 46 | 47 | cloud_requests = project.create_cloud_requests(handle_all_errors=True, print_logs=True) # Enable the logs by setting the "print_logs" parameter to True 48 | ``` 49 | 50 | ## Examples of Cloud Requests: 51 | ### Simple Ping - Pong: 52 | 53 | **Python:** 54 | 55 | ```python 56 | import scratchconnect 57 | 58 | user = scratchconnect.ScratchConnect(username="Username", password="Password") 59 | project = user.connect_project("") 60 | 61 | cloud_requests = project.create_cloud_requests(handle_all_errors=True, print_logs=True) 62 | 63 | @cloud_requests.request("ping") # Create a new request name and type 64 | def function1(): 65 | return "pong" # Return a response 66 | 67 | 68 | cloud_requests.start(update_time=1) # Start the Cloud Requests loop. Here "update_time" is the time after which the cloud request loop should look for a new request 69 | ``` 70 | 71 | Run the above code and make a new request from your Scratch project with the name "ping" 72 | 73 | **Scratch:** 74 | 75 | Click the following blocks of code to send the request: 76 | 77 | ![image](https://user-images.githubusercontent.com/70252606/208305060-5e55be1b-7304-4f77-9358-d4c0dedd319f.png) 78 | 79 | After the request is complete, you will see the data being received in the project: 80 | 81 | ![image](https://user-images.githubusercontent.com/70252606/208305350-d72f9c48-6835-4437-a550-9999cc85bec3.png) 82 | 83 | 84 | ### Sending Simple stats of a user: 85 | 86 | **Python** 87 | 88 | ```python 89 | import scratchconnect 90 | 91 | user = scratchconnect.ScratchConnect(username="Username", password="Password") 92 | project = user.connect_project("") 93 | 94 | cloud_requests = project.create_cloud_requests(handle_all_errors=True, print_logs=True) 95 | 96 | @cloud_requests.request("user-stats") 97 | def get_user_data(username): # Add a parameter to your function 98 | try: 99 | u = user.connect_user(username) # Connect a user on Scratch 100 | return u.all_data() # Return the data of the user. Note: this function returns the data in dict form and the cloud requests only sends the values of that dict in list form 101 | except scratchconnect.Exceptions.InvalidUser: # Return a message when the user was not found 102 | return "User doesn't exist!" 103 | 104 | 105 | cloud_requests.start() 106 | ``` 107 | 108 | **Scratch** 109 | 110 | Scratch code: 111 | 112 | ![image](https://user-images.githubusercontent.com/70252606/208306894-0310b348-b9d6-4638-bc30-7587fe8b28c9.png) 113 | 114 | ### Sending PFP of any user to the project: 115 | 116 | **Python** 117 | 118 | ```python 119 | import scratchconnect 120 | 121 | user = scratchconnect.ScratchConnect(username="Username", password="Password") 122 | project = user.connect_project("") 123 | 124 | cloud_requests = project.create_cloud_requests(handle_all_errors=True, print_logs=True) 125 | 126 | image = user.create_new_image() # Create a new scImage object 127 | 128 | 129 | @cloud_requests.request("user-pfp") 130 | def get_user_pfp(username, 131 | size=50): # You can also add any number of arguments to your function! And default values too! 132 | image.get_user_image(query=username, size=size) 133 | return image # Directly return the image class as a response 134 | 135 | 136 | cloud_requests.start() 137 | ``` 138 | And done! Run the script of your Scratch project and all the response data will be stored in the "IMAGE" list on Scratch. 139 | 140 | **Scratch** 141 | 142 | Scratch Code: 143 | 144 | ![image](https://user-images.githubusercontent.com/70252606/208308315-ede9f7cd-fafa-4020-9583-b6e466fe2b21.png) 145 | 146 | 147 | ## Cloud Events: 148 | This feature also has Cloud Events. See example: 149 | ```python 150 | import scratchconnect 151 | 152 | user = scratchconnect.ScratchConnect(username="Username", password="Password") 153 | project = user.connect_project("") 154 | 155 | cloud_requests = project.create_cloud_requests(handle_all_errors=True, print_logs=True) 156 | 157 | @cloud_requests.event("connect") # Do something when the cloud requests is connected 158 | def connected(): 159 | print("Connected Cloud Requests!") 160 | 161 | 162 | @cloud_requests.event("new_request") # Do something when a new request is made 163 | def new_request(): 164 | print("New Request!", cloud_requests.get_request_info()) 165 | 166 | 167 | @cloud_requests.event("error") # Do something when there was an error 168 | def error(): 169 | print("Error!") 170 | 171 | 172 | cloud_requests.start() 173 | ``` 174 | 175 | ## Thank you! 176 | I am excited to see what you create! 177 | -------------------------------------------------------------------------------- /scScratchTerminal/Terminal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main File 3 | """ 4 | 5 | import json 6 | from rich.console import Console 7 | from rich.theme import Theme 8 | 9 | _name = "Scratch Terminal" 10 | _version = "v1.0" 11 | _logo_style = "bold blue" 12 | _version_style = "italic #ffffff" 13 | _command = "[bold yellow]scratch> " 14 | Commands = { 15 | "info": "Prints the Scratch Terminal Info", 16 | "help": "This help", 17 | "console_info": "Prints the info of your Console/Terminal", 18 | "news": "Prints Scratch News", 19 | "user ": "Prints Scratch User data", 20 | "studio ": "Prints Scratch Studio data", 21 | "project ": "Prints Scratch Project data", 22 | } 23 | 24 | 25 | class Terminal: 26 | def __init__(self, sc): 27 | """ 28 | Main Terminal Class 29 | """ 30 | self.sc = sc 31 | 32 | def start(self): 33 | """ 34 | Start the loop 35 | """ 36 | self.run = True 37 | self._start_loop() 38 | 39 | def _print_console_information(self): 40 | """ 41 | Don't use this 42 | """ 43 | data = { 44 | "Size": list(self.console.size), 45 | "Encoding": str(self.console.encoding), 46 | "Is Terminal?": str(self.console.is_terminal), 47 | "Color System": str(self.console.color_system) 48 | } 49 | self._print_data(data_type="Console", data=dict(data)) 50 | 51 | def _error(self, message): 52 | """ 53 | Don't use this 54 | """ 55 | self.console.print(message, style="bold red") 56 | 57 | def _print_info(self): 58 | """ 59 | Don't use this 60 | """ 61 | self.console.rule("[bold red]Scratch Terminal Information") 62 | string = { 63 | "Name": _name, 64 | "Version": _version, 65 | "Description": "Scratch Terminal is a CLI Terminal programmed in Python to get the data of Scratch User, Studio, Project, etc. in the Terminal!", 66 | "Made By": "[blue underline]@Sid72020123[/] on [blue underline]Scratch[/]" 67 | } 68 | for i in string: 69 | self.console.print(i, style="bold cyan", end=": ") 70 | self.console.print(string[i], style="italic green") 71 | print() 72 | 73 | def _print_help(self): 74 | """ 75 | Don't use this 76 | """ 77 | self.console.rule("[bold red]Scratch Terminal Help") 78 | self.console.print("You can use the following Commands:", style="italic red") 79 | for i in Commands: 80 | self.console.print(i, style="bold cyan", end=": ") 81 | self.console.print(Commands[i], style="italic green") 82 | print() 83 | 84 | def _print_data(self, data_type, data): 85 | """ 86 | Don't use this 87 | """ 88 | self.console.rule(f"[bold red]{data_type} Information") 89 | for i in data: 90 | self.console.print(f"{i}:", style="bold cyan") 91 | if isinstance(data[i], str) and data_type in ["User", "Stduio", "Project"]: 92 | self.console.print(f"\t{data[i]}".replace("\n", "\n\t").title(), style="italic green") 93 | else: 94 | self.console.print(f"\t{data[i]}", style="italic green") 95 | print() 96 | 97 | def _get_sub_command(self, command): 98 | """ 99 | Don't use this 100 | """ 101 | try: 102 | result = command[1] 103 | except: 104 | self._error(f"The '{command[0]}' is missing some required arguments!") 105 | result = None 106 | return result 107 | 108 | def _start_loop(self): 109 | """ 110 | Don't use this 111 | """ 112 | self.console = Console(theme=Theme(inherit=False), soft_wrap=False) 113 | self.console.print(_name, style=_logo_style, end=" ") 114 | self.console.print(_version, style=_version_style) 115 | while self.run: 116 | user_input = self.console.input(_command) 117 | command_data = self._split_command(user_input) 118 | if len(command_data) <= 0: 119 | command = "" 120 | else: 121 | command = command_data[0] 122 | if command == "": 123 | pass 124 | elif command == "exit": 125 | self.console.print("Stopping Scratch Terminal...", style="bold red") 126 | self.run = False 127 | self.console.print("Successfully Stopped the Terminal!", style="bold red") 128 | elif command == "console_info": 129 | with self.console.status("Checking Console Information..."): 130 | self._print_console_information() 131 | elif command == "info": 132 | self._print_info() 133 | elif command == "help": 134 | self._print_help() 135 | elif command == "news": 136 | with self.console.status("Fetching Scratch News..."): 137 | data = self.sc.site_news() 138 | news = {} 139 | for item in data: 140 | news[item["headline"]] = item["copy"] 141 | self._print_data(data_type="News", data=news) 142 | elif command == "user": 143 | sub_command = self._get_sub_command(command_data) 144 | if sub_command is not None: 145 | with self.console.status("Fetching User Data..."): 146 | try: 147 | user = self.sc.connect_user(sub_command) 148 | data = user.all_data() 149 | except: 150 | data = None 151 | if data is not None: 152 | self._print_data(data_type="User", data=user.all_data()) 153 | else: 154 | self._error("The Username is Invalid or some required data was not found about that user.") 155 | elif command == "studio": 156 | sub_command = self._get_sub_command(command_data) 157 | if sub_command is not None: 158 | with self.console.status("Fetching Studio Data..."): 159 | try: 160 | studio = self.sc.connect_studio(sub_command) 161 | data = studio.all_data() 162 | except: 163 | data = None 164 | if data is not None: 165 | self._print_data(data_type="Studio", data=studio.all_data()) 166 | else: 167 | self._error( 168 | "The Studio ID is Invalid or some required data was not found about that studio.") 169 | elif command == "project": 170 | sub_command = self._get_sub_command(command_data) 171 | if sub_command is not None: 172 | with self.console.status("Fetching Project Data..."): 173 | try: 174 | project = self.sc.connect_project(sub_command) 175 | data = project.all_data() 176 | except: 177 | data = None 178 | if data is not None: 179 | self._print_data(data_type="Project", data=project.all_data()) 180 | else: 181 | self._error( 182 | "The Project ID is Invalid or some required data was not found about that project.") 183 | else: 184 | self._error("Invalid Command!") 185 | 186 | def _split_command(self, text): 187 | """ 188 | Don't use this 189 | """ 190 | splitted_data = text.split(" ") 191 | result = [] 192 | for item in splitted_data: 193 | if item != "": 194 | result.append(item.lower()) 195 | return result 196 | -------------------------------------------------------------------------------- /scratchconnect/CloudConnection.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Cloud Variables File. 3 | Go to https://scratch.mit.edu/projects/578255313/ for the Scratch Encode/Decode Engine! 4 | """ 5 | import json 6 | import requests # Needed to change the URL of the request while using the online IDE 7 | from ssl import SSLEOFError, SSLError 8 | import websocket 9 | import time 10 | from threading import Thread 11 | 12 | from scratchconnect.scOnlineIDE import _change_request_url 13 | from scratchconnect import Exceptions 14 | from scratchconnect.scEncoder import Encoder 15 | from scratchconnect.scCloudEvents import CloudEvents 16 | 17 | _website = "scratch.mit.edu" 18 | _login = f"https://{_website}/login/" 19 | _api = f"api.{_website}" 20 | 21 | 22 | class CloudConnection: 23 | def __init__(self, project_id, client_username, csrf_token, session_id, token, session, online_ide): 24 | """ 25 | Main class to connect cloud variables 26 | """ 27 | self.project_id = str(project_id) 28 | self.client_username = client_username 29 | self.csrf_token = csrf_token 30 | self.session_id = session_id 31 | self.token = token 32 | self.session = session 33 | self.headers = { 34 | "x-csrftoken": self.csrf_token, 35 | "X-Token": self.token, 36 | "x-requested-with": "XMLHttpRequest", 37 | "Cookie": f"scratchcsrftoken={self.csrf_token};scratchlanguage=en;scratchsessionsid={self.session_id};", 38 | "referer": "https://scratch.mit.edu/projects/" + self.project_id + "/", 39 | } 40 | 41 | self.json_headers = { 42 | "x-csrftoken": self.csrf_token, 43 | "X-Token": self.token, 44 | "x-requested-with": "XMLHttpRequest", 45 | "Cookie": f"scratchcsrftoken={self.csrf_token};scratchlanguage=en;scratchsessionsid={self.session_id};", 46 | "referer": "https://scratch.mit.edu/projects/" + str(self.project_id) + "/", 47 | "accept": "application/json", 48 | "Content-Type": "application/json", 49 | "origin": f"https://{_website}" 50 | } 51 | if online_ide: 52 | _change_request_url() 53 | self._event = CloudEvents("Scratch", self) 54 | self.encoder = Encoder() 55 | self._make_connection() 56 | self._start_ping_thread() 57 | 58 | def get_variable_data(self, limit: int = 100, offset: int = 0) -> list[dict]: 59 | """ 60 | Returns the cloud variable data 61 | :param limit: The limit 62 | :param offset: The offset or the number of values you want to skip from the beginning 63 | """ 64 | # Session is not required in the request below: 65 | # If the session is used, the Scratch API sometimes returns cached results 66 | response = requests.get( 67 | f"https://clouddata.scratch.mit.edu/logs?projectid={self.project_id}&limit={limit}&offset={offset}").json() 68 | data = [] 69 | for i in range(0, len(response)): 70 | data.append({'User': response[i]['user'], 71 | 'Action': response[i]['verb'], 72 | 'Name': response[i]['name'], 73 | 'Value': response[i]['value'], 74 | 'Timestamp': response[i]['timestamp'] 75 | }) 76 | return data 77 | 78 | def get_cloud_variable_value(self, variable_name: str, limit: int = 100) -> list: 79 | """ 80 | Returns the cloud variable value as a list. The first of the list is the latest value 81 | :param variable_name: The name of the variable 82 | :param limit: The limit 83 | """ 84 | if str(variable_name.strip())[0] != "☁": 85 | n = f"☁ {variable_name.strip()}" 86 | else: 87 | n = f"{variable_name.strip()}" 88 | data = [] 89 | d = self.get_variable_data(limit=limit) 90 | i = 0 91 | while i < len(d): 92 | if d[i]['Name'] == n: 93 | data.append(d[i]['Value']) 94 | i = i + 1 95 | return data 96 | 97 | def _send_packet(self, packet): 98 | """ 99 | Don't use this 100 | """ 101 | self._ws.send(json.dumps(packet) + "\n") 102 | 103 | def _ping_task(self): 104 | """ 105 | Don't use this 106 | """ 107 | while True: 108 | self._ws.ping() 109 | time.sleep(1) 110 | self._ws.pong() 111 | time.sleep(4) 112 | 113 | def _start_ping_thread(self): 114 | """ 115 | Don't use this 116 | """ 117 | self.ping_task = Thread(target=self._ping_task, daemon=True) 118 | self.ping_task.start() 119 | 120 | def _make_connection(self): 121 | """ 122 | Don't use this 123 | """ 124 | self._ws = websocket.WebSocket(enable_multithread=True) 125 | self._ws.connect( 126 | "wss://clouddata.scratch.mit.edu", 127 | cookie=f"scratchsessionsid={self.session_id};", 128 | origin="https://scratch.mit.edu", 129 | enable_multithread=True, 130 | subprotocols=["binary", "base64"] 131 | ) 132 | self._send_packet( 133 | { 134 | "method": "handshake", 135 | "user": self.client_username, 136 | "project_id": str(self.project_id), 137 | } 138 | ) 139 | self._event.emit('connect') 140 | 141 | def set_cloud_variable(self, variable_name: str, value: int | str) -> bool: 142 | """ 143 | Set a cloud variable 144 | :param variable_name: Variable name 145 | :param value: Variable value 146 | """ 147 | if str(value).isdigit() and value == '': 148 | raise Exceptions.InvalidCloudValue(f"The Cloud Value should be a set of digits and not '{value}'!") 149 | 150 | try: 151 | if len(str(value)) > 256: 152 | raise ValueError( 153 | "Scratch has Cloud Variable Limit of 256 Characters per variable. Try making the value shorter!") 154 | if str(variable_name.strip())[0] != "☁": 155 | n = f"☁ {variable_name.strip()}" 156 | else: 157 | n = f"{variable_name.strip()}" 158 | packet = { 159 | "method": "set", 160 | "name": n, 161 | "value": str(value), 162 | "user": self.client_username, 163 | "project_id": str(self.project_id), 164 | } 165 | self._send_packet(packet) 166 | return True 167 | except (ConnectionAbortedError, BrokenPipeError, SSLEOFError, SSLError): 168 | self._event.emit('disconnect') 169 | self._make_connection() 170 | return False 171 | 172 | def encode(self, text: str, default: str = " ") -> str: 173 | """ 174 | Encode a text. For example: A -> 1 175 | Go to https://scratch.mit.edu/projects/578255313/ for the Scratch Engine! 176 | :param text: The text to encode 177 | :param default: The default value to encode when the character found is not accepted by the encoder 178 | """ 179 | return self.encoder.encode(text, default=default) 180 | 181 | def decode(self, encoded_text: str | int | list) -> str: 182 | """ 183 | Decode a text. For example: 1 -> A 184 | Go to https://scratch.mit.edu/projects/578255313/ for the Scratch Engine! 185 | :param encoded_text: The text to decode 186 | """ 187 | if type(encoded_text) == list: 188 | return self.encoder.decode(encoded_text[0]) 189 | else: 190 | return self.encoder.decode(encoded_text) 191 | 192 | def encode_list(self, data: list, default: str = " ") -> str: 193 | """ 194 | Encode a Python List 195 | :param data: The list to encode 196 | :param default: The default value to encode when the character found is not accepted by the encoder 197 | """ 198 | return self.encoder.encode_list(data, default=default) 199 | 200 | def decode_list(self, encoded_data: str | int | list) -> list: 201 | """ 202 | Decode a Python List 203 | :param encoded_data: The data to be decoded 204 | """ 205 | if type(encoded_data) == list: 206 | return self.encoder.decode_list(encoded_data[0]) 207 | else: 208 | return self.encoder.decode_list(encoded_data) 209 | 210 | def create_cloud_event(self) -> CloudEvents: 211 | """ 212 | Create a Cloud Event 213 | """ 214 | return self._event 215 | -------------------------------------------------------------------------------- /scratchconnect/scCloudStorage.py: -------------------------------------------------------------------------------- 1 | # This feature is deprecated since the v5.0 of the library. Please use the new alternative Cloud Requests feature instead. 2 | 3 | """ 4 | The Cloud Storage File. 5 | 6 | import time 7 | import json 8 | import string 9 | from threading import Thread 10 | 11 | from scratchconnect.CloudConnection import CloudConnection 12 | from scratchconnect.scEncoder import Encoder 13 | 14 | SUPPORTED_CHARS = list(string.ascii_uppercase + string.ascii_lowercase + string.digits + string.punctuation + ' ') 15 | _VARIABLE_LENGTH = 256 16 | _VARIABLES = [f'Response{i}' for i in range(1, 9)] 17 | 18 | _FAIL = 0 19 | _SUCCESS = 1 20 | _ACCESS_DENIED = 2 21 | _ALREADY_EXIST = 3 22 | _DOESNT_EXIST = 4 23 | 24 | 25 | class CloudStorage: 26 | def __init__(self, file_name, rewrite_file, project_id, client_username, csrf_token, session_id, token, 27 | edit_access, all_access): 28 | print("ScratchConnect Note: The Cloud Storage feature is going to be removed in ScratchConnect v5.0! Please use the new alternative feature - Cloud Requests") 29 | self.project_id = project_id 30 | self.client_username = client_username 31 | self.csrf_token = csrf_token 32 | self.session_id = session_id 33 | self.token = token 34 | self.file_name = f"{file_name}.json" 35 | self.rewrite_file = rewrite_file 36 | self._make_file() 37 | self.edit_access = edit_access 38 | self.edit_access.append(client_username) 39 | self.all_access = all_access 40 | self._connect_cloud() 41 | self.encoder = Encoder() 42 | self.loop = True 43 | 44 | def _connect_cloud(self): 45 | self.cloud = CloudConnection(self.project_id, self.client_username, self.csrf_token, self.session_id, 46 | self.token) 47 | 48 | def _get_request(self): 49 | return self._get_cloud_variable_data('Request')[0] 50 | 51 | def _reset_request_var(self): 52 | try: 53 | self.cloud.set_cloud_variable(variable_name='Request', value="") 54 | except: # lgtm [py/catch-base-exception] 55 | time.sleep(1) 56 | self._connect_cloud() 57 | 58 | def _set_response_info(self, status_code, pr, request_type, request_name): 59 | try: 60 | self.cloud.set_cloud_variable(variable_name='Response Info', 61 | value=self.encoder.encode_list([status_code])) 62 | if pr: 63 | if status_code == _SUCCESS: 64 | print(f"scCloudStorage {request_type} '{request_name}' Success!") 65 | elif status_code in [_FAIL, _ALREADY_EXIST, _DOESNT_EXIST]: 66 | print(f"scCloudStorage {request_type} '{request_name}' Failed!") 67 | elif status_code == _ACCESS_DENIED: 68 | print(f"scCloudStorage {request_type} '{request_name}' Access Denied!") 69 | except: # lgtm [py/catch-base-exception] 70 | time.sleep(1) 71 | self._connect_cloud() 72 | 73 | def _set_cloud_var(self, name, value): 74 | try: 75 | return self.cloud.set_cloud_variable(variable_name=name, value=value) 76 | except: # lgtm [py/catch-base-exception] 77 | time.sleep(1) 78 | self._connect_cloud() 79 | 80 | def start_cloud_loop(self, update_time=5, print_requests=False): 81 | t = Thread(target=self._start_loop, args=(update_time, print_requests,)) 82 | t.start() 83 | 84 | def _start_loop(self, update_time, pr): 85 | while self.loop: 86 | try: 87 | if pr: 88 | print("scCloudStorage Checking for new request...") 89 | r = self._get_request() 90 | request = self.encoder.decode_list(str(r[0])) 91 | if len(request) > 1: 92 | request_type = request[0] 93 | request_name = request[1] 94 | if request_name == "": 95 | request_name = None 96 | user = r[1] 97 | if pr: 98 | print(f"scCloudStorage New Request {request_type}") 99 | if request_type == "CREATE": 100 | if user in self.edit_access or self.all_access: 101 | file = self._open_file(mode='r+') 102 | data = json.loads(file.read()) 103 | file.close() 104 | if request_name in data: 105 | self._set_response_info(status_code=_ALREADY_EXIST, pr=pr, request_name=request_name, 106 | request_type=request_type) 107 | self._reset_request_var() 108 | continue 109 | else: 110 | data[request_name] = 0 111 | file = self._open_file(mode='w') 112 | file.write(json.dumps(data)) 113 | file.close() 114 | self._set_response_info(status_code=_SUCCESS, pr=pr, request_name=request_name, 115 | request_type=request_type) 116 | self._reset_request_var() 117 | else: 118 | self._set_response_info(status_code=_ACCESS_DENIED, pr=pr, request_name=request_name, 119 | request_type=request_type) 120 | self._reset_request_var() 121 | 122 | if request_type == "DELETE": 123 | if user in self.edit_access or self.all_access: 124 | file = self._open_file(mode='r+') 125 | data = json.loads(file.read()) 126 | file.close() 127 | if request_name not in data: 128 | self._set_response_info(status_code=_DOESNT_EXIST, pr=pr, request_name=request_name, 129 | request_type=request_type) 130 | self._reset_request_var() 131 | continue 132 | else: 133 | del data[request_name] 134 | file = self._open_file(mode='w') 135 | file.write(json.dumps(data)) 136 | file.close() 137 | self._set_response_info(status_code=_SUCCESS, pr=pr, request_name=request_name, 138 | request_type=request_type) 139 | self._reset_request_var() 140 | else: 141 | self._set_response_info(status_code=_ACCESS_DENIED, pr=pr, request_name=request_name, 142 | request_type=request_type) 143 | self._reset_request_var() 144 | 145 | if request_type == "DELETE_ALL": 146 | if user in self.edit_access or self.all_access: 147 | file = self._open_file(mode='w') 148 | file.write(json.dumps({})) 149 | file.close() 150 | self._set_response_info(status_code=_SUCCESS, pr=pr, request_name=request_name, 151 | request_type=request_type) 152 | self._reset_request_var() 153 | else: 154 | self._set_response_info(status_code=_ACCESS_DENIED, pr=pr, request_name=request_name, 155 | request_type=request_type) 156 | self._reset_request_var() 157 | 158 | if request_type == "GET": 159 | d = self._get_data(request_name) 160 | if d is None: 161 | self._set_response_info(status_code=_DOESNT_EXIST, pr=pr, request_name=request_name, 162 | request_type=request_type) 163 | self._reset_request_var() 164 | continue 165 | else: 166 | data = self.encoder.encode(str(d)) 167 | divided_data = self._divide_code(data, _VARIABLE_LENGTH) 168 | data_index = 0 169 | for variable in _VARIABLES: 170 | if self._set_cloud_var(name=variable, value=divided_data[data_index]) is False: 171 | continue 172 | data_index += 1 173 | if data_index == len(divided_data): 174 | break 175 | self._set_response_info(status_code=_SUCCESS, pr=pr, request_name=request_name, 176 | request_type=request_type) 177 | self._reset_request_var() 178 | 179 | if request_type == "SET": 180 | v = "" 181 | for variable in _VARIABLES: 182 | d = self.cloud.get_cloud_variable_value(variable, limit=3) 183 | if len(d) > 0: 184 | v += d[0] 185 | value = self.encoder.decode(v) 186 | file = self._open_file(mode='r+') 187 | data = json.loads(file.read()) 188 | file.close() 189 | if request_name in data: 190 | data[request_name] = value 191 | else: 192 | self._set_response_info(status_code=_DOESNT_EXIST, pr=pr, request_name=request_name, 193 | request_type=request_type) 194 | self._reset_request_var() 195 | continue 196 | file = self._open_file(mode='w') 197 | file.write(json.dumps(data)) 198 | file.close() 199 | self._set_response_info(status_code=_SUCCESS, pr=pr, request_name=request_name, 200 | request_type=request_type) 201 | self._reset_request_var() 202 | time.sleep(update_time) 203 | except KeyboardInterrupt: 204 | pass 205 | 206 | def _make_file(self): 207 | if self.rewrite_file: 208 | file = open(self.file_name, 'w+') 209 | else: 210 | file = open(self.file_name, 'a+') 211 | t_file = open(self.file_name, 'r') 212 | if len(t_file.read()) == 0: 213 | file.write(json.dumps({})) 214 | t_file.close() 215 | file.close() 216 | 217 | def _open_file(self, mode='r'): 218 | file = open(self.file_name, mode) 219 | return file 220 | 221 | def _get_data(self, key): 222 | try: 223 | file = json.loads(self._open_file(mode='r').read()) 224 | return file[key] 225 | except KeyError: 226 | return None 227 | 228 | def _get_cloud_variable_data(self, variable_name, limit=100): 229 | if str(variable_name.strip())[0] != "☁": 230 | n = f"☁ {variable_name.strip()}" 231 | else: 232 | n = f"{variable_name.strip()}" 233 | data = [] 234 | d = self.cloud.get_variable_data(limit=limit) 235 | i = 0 236 | while i < len(d): 237 | if d[i]['Name'] == n: 238 | data.append([d[i]['Value'], d[i]['User']]) 239 | i = i + 1 240 | return data 241 | 242 | def _divide_code(self, data, letters_length): 243 | i = 0 244 | divide = [] 245 | text = "" 246 | while i < len(data): 247 | text += data[i] 248 | if len(text) >= letters_length: 249 | divide.append(text) 250 | text = "" 251 | i += 1 252 | if len(text) > 0: 253 | divide.append(text) 254 | return divide 255 | """ 256 | -------------------------------------------------------------------------------- /scratchconnect/UserCommon.py: -------------------------------------------------------------------------------- 1 | """ 2 | This File contains the functions used commonly by the ScratchConnect.py/ScratchConnect and User.py/User class 3 | """ 4 | 5 | import json 6 | import requests 7 | 8 | from scratchconnect.scOnlineIDE import _change_request_url 9 | from scratchconnect import Exceptions 10 | 11 | _website = "scratch.mit.edu" 12 | _api = f"https://api.{_website}" 13 | 14 | 15 | class UserCommon: 16 | def __init__(self, username, session, online_ide): 17 | """ 18 | The User Class to connect a Scratch user. 19 | :param username: The username 20 | """ 21 | self.username = username 22 | self.session = session 23 | self._user_link = f"{_api}/users/{self.username}" 24 | if online_ide: 25 | _change_request_url() 26 | self.update_data() 27 | 28 | def update_data(self) -> None: 29 | """ 30 | Update the stored data 31 | """ 32 | self.user_id = None 33 | self.user_messages_count = None 34 | self.user_messages = None 35 | self.user_work = None 36 | self.user_status = None 37 | self.user_joined_date = None 38 | self.user_country = None 39 | self.user_featured_data = None 40 | self.user_projects = None 41 | self.user_followers_count = None 42 | self.user_following_count = None 43 | self.user_total_views = None 44 | self.user_total_loves = None 45 | self.user_total_faves = None 46 | self.user_following = None 47 | self.user_followers = None 48 | self.user_favourites = None 49 | self.user_projects_count = None 50 | self.user_projects_count = None 51 | 52 | data = self.session.get(f"{self._user_link}").json() 53 | try: 54 | self.user_id = data["id"] 55 | except KeyError: 56 | raise Exceptions.InvalidUser(f"Username '{self.username}' doesn't exist!") 57 | self.user_work = data["profile"]["status"] 58 | self.user_bio = data["profile"]["bio"] 59 | self.user_joined_date = data["history"]["joined"] 60 | self.user_country = data["profile"]["country"] 61 | self.user_thumbnail_url = data["profile"]["images"] 62 | 63 | def _update_db_data(self): 64 | """ 65 | Update the stored Data (DON'T USE) 66 | """ 67 | try: 68 | data = requests.get(f"https://scratchdb.lefty.one/v3/user/info/{self.username}").json() 69 | self.user_status = data["status"] 70 | self.user_followers_count = data["statistics"]["followers"] 71 | self.user_following_count = data["statistics"]["following"] 72 | self.user_total_views = data["statistics"]["views"] 73 | self.user_total_loves = data["statistics"]["loves"] 74 | self.user_total_faves = data["statistics"]["favorites"] 75 | except json.decoder.JSONDecodeError: 76 | pass 77 | 78 | def id(self) -> int: 79 | """ 80 | Get the ID of a user's profile 81 | """ 82 | if self.id is None: 83 | self.update_data() 84 | return self.user_id 85 | 86 | def thumbnail_url(self) -> dict: 87 | """Return the thumbnail URL of a user""" 88 | if self.user_thumbnail_url is None: 89 | self.update_data() 90 | return self.user_thumbnail_url 91 | 92 | def messages_count(self) -> int: 93 | """ 94 | Get the messages count of the logged in user 95 | """ 96 | headers = { 97 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36"} 98 | if self.user_messages_count is None: 99 | self.user_messages_count = \ 100 | self.session.get(f"https://api.scratch.mit.edu/users/{self.username}/messages/count", 101 | headers=headers).json()[ 102 | "count"] 103 | return self.user_messages_count 104 | 105 | def work(self) -> str: 106 | """ 107 | Returns the 'What I am working on' of a Scratch profile 108 | """ 109 | if self.user_work is None: 110 | self.update_data() 111 | return self.user_work 112 | 113 | def bio(self) -> str: 114 | """ 115 | Returns the 'About me' of a Scratch profile 116 | """ 117 | if self.user_bio is None: 118 | self.update_data() 119 | return self.user_bio 120 | 121 | def status(self) -> str: 122 | """ 123 | Returns the status(Scratcher or New Scratcher) of a Scratch profile 124 | """ 125 | if self.user_status is None: 126 | self._update_db_data() 127 | return self.user_status 128 | 129 | def joined_date(self) -> str: 130 | """ 131 | Returns the joined date of a Scratch profile 132 | """ 133 | if self.user_joined_date is None: 134 | self.update_data() 135 | return self.user_joined_date 136 | 137 | def country(self) -> str: 138 | """ 139 | Returns the country of a Scratch profile 140 | """ 141 | if self.user_country is None: 142 | self.update_data() 143 | return self.user_country 144 | 145 | def followers_count(self) -> int: 146 | """ 147 | Returns the follower count of a user 148 | """ 149 | if self.user_followers_count is None: 150 | self._update_db_data() 151 | return self.user_followers_count 152 | 153 | def following_count(self) -> int: 154 | """ 155 | Returns the following count of a user 156 | """ 157 | if self.user_following_count is None: 158 | self._update_db_data() 159 | return self.user_following_count 160 | 161 | def total_views(self) -> int: 162 | """ 163 | Returns the total views count of all the shared projects of a user 164 | """ 165 | if self.user_total_views is None: 166 | self._update_db_data() 167 | return self.user_total_views 168 | 169 | def total_loves_count(self) -> int: 170 | """ 171 | Returns the total loves count of all the shared projects of a user 172 | """ 173 | if self.user_total_loves is None: 174 | self._update_db_data() 175 | return self.user_total_loves 176 | 177 | def total_favourites_count(self) -> int: 178 | """ 179 | Returns the total favourites count of all the shared projects of a user 180 | """ 181 | if self.user_total_faves is None: 182 | self._update_db_data() 183 | return self.user_total_faves 184 | 185 | def featured_data(self) -> dict: 186 | """ 187 | Returns the featured project data of the Scratch profile 188 | """ 189 | if self.user_featured_data is None: 190 | self.user_featured_data = self.session.get( 191 | f"https://scratch.mit.edu/site-api/users/all/{self.username}").json() 192 | return self.user_featured_data 193 | 194 | def projects(self, all=False, limit=20, offset=0) -> list: 195 | """ 196 | Returns the list of shared projects of a user 197 | :param all: If you want all then set it to True 198 | :param limit: The limit of the projects 199 | :param offset: The number of projects to be skipped from the beginning 200 | """ 201 | if self.user_projects is None: 202 | projects = [] 203 | if all: 204 | offset = 0 205 | while True: 206 | request = self.session.get( 207 | f"{_api}/users/{self.username}/projects/?limit=40&offset={offset}").json() 208 | projects.append(request) 209 | if len(request) != 40: 210 | break 211 | offset += 40 212 | if not all: 213 | for i in range(1, limit + 1): 214 | request = self.session.get( 215 | f"{_api}/users/{self.username}/projects/?limit={limit}&offset={offset}").json() 216 | projects.append(request) 217 | self.user_projects = projects 218 | return self.user_projects 219 | 220 | def projects_count(self) -> int: 221 | if self.user_projects_count is None: 222 | all_projects = self.projects(all=True) 223 | count = 0 224 | for i in all_projects: 225 | count += len(i) 226 | self.user_projects_count = count 227 | return self.user_projects_count 228 | 229 | def following(self, all=False, limit=20, offset=0) -> list: 230 | """ 231 | Returns the list of the user following 232 | :param all: If you want all then set it to True 233 | :param limit: The limit of the users 234 | :param offset: The number of users to be skipped from the beginning 235 | """ 236 | if self.user_following is None: 237 | following = [] 238 | if all: 239 | offset = 0 240 | while True: 241 | response = self.session.get( 242 | f"{_api}/users/{self.username}/following/?limit=40&offset={offset}").json() 243 | offset += 40 244 | following.append(response) 245 | if len(response) != 40: 246 | break 247 | if not all: 248 | response = self.session.get( 249 | f"{_api}/users/{self.username}/following/?limit={limit}&offset={offset}").json() 250 | following.append(response) 251 | self.user_following = following 252 | return self.user_following 253 | 254 | def followers(self, all=False, limit=20, offset=0) -> list: 255 | """ 256 | Returns the list of the user followers 257 | :param all: If you want all then set it to True 258 | :param limit: The limit of the users 259 | :param offset: The number of users to be skipped from the beginning 260 | """ 261 | if self.user_followers is None: 262 | followers = [] 263 | if all: 264 | offset = 0 265 | while True: 266 | response = self.session.get( 267 | f"{_api}/users/{self.username}/followers/?limit=40&offset={offset}").json() 268 | offset += 40 269 | followers.append(response) 270 | if len(response) != 40: 271 | break 272 | if not all: 273 | response = self.session.get( 274 | f"{_api}/users/{self.username}/followers/?limit={limit}&offset={offset}").json() 275 | followers.append(response) 276 | self.user_followers = followers 277 | return self.user_followers 278 | 279 | def favourites(self, all=False, limit=20, offset=0) -> list: 280 | """ 281 | Returns the list of the user favourites 282 | :param all: If you want all then set it to True 283 | :param limit: The limit of the projects 284 | :param offset: The number of projects to be skipped from the beginning 285 | """ 286 | if self.user_favourites is None: 287 | favourites = [] 288 | if all: 289 | offset = 0 290 | while True: 291 | response = self.session.get( 292 | f"{_api}/users/{self.username}/favorites/?limit=40&offset={offset}").json() 293 | offset += 40 294 | favourites.append(response) 295 | if len(response) != 40: 296 | break 297 | if not all: 298 | response = self.session.get( 299 | f"{_api}/users/{self.username}/favorites/?limit={limit}&offset={offset}").json() 300 | favourites.append(response) 301 | self.user_favourites = favourites 302 | return self.user_favourites 303 | 304 | def user_follower_history(self, segment="", range=30) -> list: 305 | """ 306 | Return the follower history of the user 307 | :param segment: The length of time between each segment, defaults to 1 day. 308 | :param range: Of how far back to get history, defaults to 30 days 309 | """ 310 | return requests.get( 311 | f"https://scratchdb.lefty.one/v3/user/graph/{self.username}/followers?segment={segment}&range={range}").json() 312 | 313 | def all_data(self) -> dict: 314 | """ 315 | Returns all the data of the user 316 | """ 317 | data = { 318 | 'UserName': self.username, 319 | 'UserId': self.id(), 320 | 'Messages Count': self.messages_count(), 321 | 'Join Date': self.joined_date(), 322 | 'Status': self.status(), 323 | 'Work': self.work(), 324 | 'Bio': self.bio(), 325 | 'Country': self.country(), 326 | 'Follower Count': self.followers_count(), 327 | 'Following Count': self.following_count(), 328 | 'Total Views': self.total_views(), 329 | 'Total Loves': self.total_loves_count(), 330 | 'Total Favourites': self.total_favourites_count(), 331 | 'Total Projects Count': self.projects_count() 332 | } 333 | return data 334 | 335 | def ocular_data(self) -> dict: 336 | """ 337 | Get ocular data of the user 338 | """ 339 | return requests.get(f"https://my-ocular.jeffalo.net/api/user/{self.username}").json() 340 | 341 | def aviate_data(self, code=False) -> dict: 342 | """ 343 | Get Aviate Status of the user 344 | :param code: True to get the status code 345 | """ 346 | return requests.get(f"https://aviateapp.eu.org/api/{self.username}?code={str(code).lower()}").json()['status'] 347 | 348 | def comments(self, limit=5, page=1) -> dict: 349 | """ 350 | Get comments of the profile of the user 351 | :param limit: The limit 352 | :param page: The page 353 | """ 354 | return requests.get( 355 | f"https://apis.scratchconnect.eu.org/comments/user/?username={self.username}&limit={limit}&page={page}").json() 356 | -------------------------------------------------------------------------------- /scratchconnect/scCloudRequests.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Cloud Requests File 3 | Inspired by @TimMcCool's scratchattach 4 | """ 5 | 6 | import time 7 | import traceback 8 | from threading import Thread 9 | 10 | from scratchconnect import Warnings 11 | from scratchconnect.CloudConnection import CloudConnection 12 | from scratchconnect.scImage import Image 13 | 14 | VERSION = "2.5 (stable)" 15 | RESPONSE_VARIABLES = [f"Response_{i}" for i in range(1, 9)] 16 | cloud_variable_length_limit = 256 17 | FAIL = 0 18 | SUCCESS = 1 19 | 20 | 21 | class CloudRequests: 22 | def __init__(self, project_id, client_username, csrf_token, session_id, token, handle_all_errors, 23 | print_logs, online_ide, session, default=" "): 24 | print(f"ScratchConnect CloudRequests - v{VERSION}") 25 | self.t = None 26 | self.run_thread = False 27 | self.handle_all_errors = handle_all_errors 28 | self._request_functions = {} 29 | self._event_functions = {} 30 | self._request_value = "" 31 | self.print_logs = print_logs 32 | self.default = default 33 | self.max_tries = 3 34 | self.session = session 35 | self.cloud = CloudConnection(project_id=project_id, client_username=client_username, csrf_token=csrf_token, 36 | session_id=session_id, token=token, online_ide=online_ide, session=self.session) 37 | self._REQUESTS = [] 38 | self._request = {} 39 | 40 | def request(self, req_name: str): 41 | """ 42 | Decorator 43 | """ 44 | 45 | def wrapper(func): 46 | self._request_functions[req_name] = func 47 | 48 | return wrapper 49 | 50 | def event(self, n: str): 51 | """ 52 | Decorator 53 | """ 54 | 55 | def wrapper(func): 56 | self._event_functions[n] = func 57 | 58 | return wrapper 59 | 60 | def emit(self, f_type, arguments="", t="request"): 61 | """ 62 | Don't use this! 63 | """ 64 | if t == "request": 65 | func = self._request_functions[f_type] 66 | if len(arguments) > 0: 67 | if len(arguments) == 1: 68 | return func(arguments[0]) 69 | else: 70 | return func(*tuple(arguments)) 71 | else: 72 | return func() 73 | else: 74 | if f_type in self._event_functions.keys(): 75 | func = self._event_functions[f_type] 76 | func() 77 | 78 | def _done_request(self): 79 | """ 80 | Don't use this! 81 | """ 82 | try: 83 | self.get_request() 84 | self._REQUESTS = self.cloud.decode_list(self._request_value) 85 | self._REQUESTS.pop(0) 86 | self._set_cloud_variable("Request", self.cloud.encode_list(self._REQUESTS)) 87 | time.sleep(1) 88 | except (KeyError, IndexError, ValueError, TypeError): 89 | pass 90 | 91 | def _set_cloud_variable(self, n, v): 92 | """ 93 | Don't use this! 94 | """ 95 | self.cloud.set_cloud_variable(variable_name=n, value=v) 96 | 97 | def _set_response_info(self, id, status_code, rv="", length=0, i=None): 98 | """ 99 | Don't use this! 100 | """ 101 | if i is None: 102 | i = [] 103 | self._set_cloud_variable("Response_Info", self.cloud.encode_list([id, status_code, length, rv, *i])) 104 | 105 | def _get_response_info(self): 106 | """ 107 | Don't use this! 108 | """ 109 | return self.cloud.get_cloud_variable_value("Response_Info", 10)[0] 110 | 111 | def get_request(self): 112 | """ 113 | Don't use this! 114 | """ 115 | try: 116 | self._request_value = self._get_cloud_variable_value(variable_name="Request")[0] 117 | except IndexError: 118 | pass 119 | 120 | def _get_cloud_variable_value(self, variable_name, limit=100): 121 | """ 122 | Don't use this! 123 | """ 124 | if str(variable_name.strip())[0] != "☁": 125 | n = f"☁ {variable_name.strip()}" 126 | else: 127 | n = f"{variable_name.strip()}" 128 | data = [] 129 | d = self.cloud.get_variable_data(limit=limit) 130 | i = 0 131 | while i < len(d): 132 | if d[i]['Name'] == n: 133 | data.append(d[i]['Value']) 134 | self._request["User"] = d[i]["User"] 135 | self._request["Timestamp"] = d[i]["Timestamp"] 136 | i = i + 1 137 | return data 138 | 139 | def _get_args(self, data_length): 140 | value = "" 141 | data = self.cloud.get_variable_data(limit=100) 142 | variables = [f"Response_{i}" for i in range(1, 9)] 143 | for var in variables: 144 | for i in data: 145 | if i["Name"].replace("☁ ", "") == var: 146 | value += i["Value"] 147 | break 148 | if len(value) >= data_length: 149 | break 150 | return value 151 | 152 | def _event(self, up): 153 | """ 154 | Don't use this! 155 | """ 156 | while self.run_thread: 157 | try: 158 | self.get_request() 159 | if self._request_value != "": 160 | args = "" 161 | n = 1 162 | time.sleep(0.1) 163 | success = True 164 | while True: 165 | try: 166 | self._REQUESTS = self.cloud.decode_list(self._request_value) 167 | request = self._REQUESTS[0] 168 | req_id, req_name = self.cloud.decode_list(request) 169 | self._request["Request ID"] = req_id 170 | self._request["Request Name"] = req_name 171 | _raw_ri = self._get_response_info() 172 | ri = self.cloud.decode_list(_raw_ri) 173 | self._log(message= 174 | f"ScratchConnect CloudRequests: New Request: ID - {req_id} Name - {req_name}") 175 | if ri[0] == req_id: 176 | _raw_d = "" 177 | data_length = int(ri[1]) 178 | self._set_cloud_variable("Response_Info", 1) 179 | tries = 1 180 | while True: 181 | try: 182 | if self.cloud.decode(self.cloud.get_cloud_variable_value("Response_Info", 10)[ 183 | 0]) == "Success" or tries > self.max_tries + 2: 184 | break 185 | except (KeyError, IndexError, ValueError): 186 | pass 187 | tries += 1 188 | time.sleep(0.1) 189 | if tries == self.max_tries + 2: 190 | Warnings.warn( 191 | f"ScratchConnect CloudRequests: Request ID - {req_id}: Closing the request as the server didn't received a response form the Project! Maybe the project was stopped or closed!") 192 | self.emit("error", t="event") 193 | break 194 | _raw_d = self._get_args(data_length=data_length) if data_length > 0 else "" 195 | if len(_raw_d) != data_length: 196 | Warnings.warn( 197 | f"ScratchConnect CloudRequests: Request ID - {req_id}: Error getting the request argument(s). Maybe the argument(s) were too long! Request will be closed!") 198 | self.emit("error", t="event") 199 | self._set_response_info(req_id, FAIL) 200 | self._done_request() 201 | success = False 202 | break 203 | args = self.cloud.decode_list(_raw_d) 204 | self._request["Arguments"] = args 205 | time.sleep(0.1) 206 | self._set_cloud_variable("Response_Info", 1) 207 | self._log(message= 208 | f"ScratchConnect CloudRequests: ID - {req_id} Arguments - {args}") 209 | break 210 | else: 211 | break 212 | except (ValueError, IndexError): 213 | self.get_request() 214 | n += 1 215 | if n == self.max_tries: 216 | self._done_request() 217 | break 218 | time.sleep(0.1) 219 | if not success: 220 | continue 221 | if n == self.max_tries: 222 | continue 223 | self.emit("new_request", t="event") 224 | if req_name in self._request_functions.keys(): 225 | return_value = self.emit(req_name, args) 226 | encoded_value = "" 227 | rv = "" 228 | if type(return_value) is list: 229 | data = return_value 230 | rv = "List" 231 | encoded_value = self.cloud.encode_list(data, default=self.default) 232 | elif type(return_value) is dict: 233 | data = list(return_value.values()) 234 | rv = "List" 235 | encoded_value = self.cloud.encode_list(data, default=self.default) 236 | elif (type(return_value) is str) or (return_value is None): 237 | data = str(return_value) 238 | rv = "String" 239 | encoded_value = self.cloud.encode(data, default=self.default) 240 | elif type(return_value) is int: 241 | data = return_value 242 | rv = "Int" 243 | encoded_value = str(data) 244 | elif type(return_value) == Image: 245 | if return_value.encode_image(): 246 | data = return_value.get_image_data() 247 | rv = "Image" 248 | encoded_value = data 249 | else: 250 | Warnings.warn( 251 | f"ScratchConnect CloudRequests: Request ID - {req_id}: Closing the request as the image data was invalid or cannot be found!") 252 | self.emit("error", t="event") 253 | self._set_response_info(req_id, FAIL) 254 | self._done_request() 255 | continue 256 | splitted_data = self._split_encoded_data(encoded_value) 257 | index = 0 258 | variable_index = 0 259 | l = len(encoded_value) 260 | time.sleep(0.1) 261 | if rv == "Image": 262 | self._set_response_info(req_id, SUCCESS, rv, l, return_value.get_size()) 263 | else: 264 | self._set_response_info(req_id, SUCCESS, rv, l) 265 | success = True 266 | while index < len(splitted_data): 267 | self._set_cloud_variable(RESPONSE_VARIABLES[variable_index], splitted_data[index]) 268 | index += 1 269 | variable_index += 1 270 | wait = 0 271 | if variable_index >= len(RESPONSE_VARIABLES): 272 | variable_index = 0 273 | self._set_response_info(req_id, SUCCESS) 274 | while True: 275 | wait += 1 276 | if int(self._get_response_info()) == 1: 277 | break 278 | if wait == self.max_tries: 279 | success = False 280 | Warnings.warn( 281 | f"ScratchConnect CloudRequests: Request ID - {req_id}: Closing the request as the server didn't received a response form the Project! Maybe the project was stopped or closed!") 282 | self.emit("error", t="event") 283 | break 284 | time.sleep(0.1) 285 | if wait == self.max_tries: 286 | break 287 | time.sleep(0.1) 288 | if success: 289 | self._set_response_info(req_id, SUCCESS) 290 | self._log( 291 | message=f"ScratchConnect CloudRequests: Success: ID - {req_id}") 292 | else: 293 | self._log( 294 | message=f"ScratchConnect CloudRequests: ID - {req_id}: Closing the request as the return data was not found or the request name is invalid!") 295 | self.emit("error", t="event") 296 | self._done_request() 297 | time.sleep(up) 298 | except Exception as E: 299 | if E == "KeyBoardInterrupt": 300 | self.run_thread = False 301 | Warnings.warn(f"ScratchConnect: Error in Cloud Requests: {E}:") 302 | self.emit("error", t="event") 303 | self._done_request() 304 | if self.handle_all_errors: 305 | print("\t" + traceback.format_exc().replace("\n", "\n\t") + "") 306 | else: 307 | raise Exception(E) 308 | 309 | def _log(self, t="success", message=""): 310 | """ 311 | Don't use this! 312 | """ 313 | if self.print_logs: 314 | if t == "success": 315 | print(message) 316 | else: 317 | print(f"ScratchConnect: Error in Cloud Requests: {message}") 318 | 319 | def _split_encoded_data(self, data): 320 | """ 321 | Don't use this! 322 | """ 323 | result = [] 324 | i = 0 325 | temp_str = "" 326 | while i < len(data): 327 | temp_str += data[i] 328 | if len(temp_str) >= cloud_variable_length_limit: 329 | result.append(temp_str) 330 | temp_str = "" 331 | i += 1 332 | if len(temp_str) > 0: 333 | result.append(temp_str) 334 | while (len(result) % len(RESPONSE_VARIABLES)) != 0: 335 | result.append("") 336 | return result 337 | 338 | def get_request_info(self) -> dict: 339 | """ 340 | Get the request info 341 | """ 342 | return self._request 343 | 344 | def start(self, update_time: int = 1) -> None: 345 | """ 346 | Start the events loop 347 | :param update_time: The update time 348 | """ 349 | self.run_thread = True 350 | self.t = Thread(target=self._event, args=(update_time,)) 351 | self.t.start() 352 | self.emit("connect", t="event") 353 | 354 | def stop(self) -> None: 355 | """ 356 | Stop the events loop 357 | """ 358 | self.run = False 359 | -------------------------------------------------------------------------------- /docs/the_scratchconnect_class.md: -------------------------------------------------------------------------------- 1 | # The ScratchConnect class 2 | 3 | The `ScratchConnect` class is the main class of the library. **It is essential in every program using the library.** 4 | 5 | Once you finished the login part, you can now use the library! 6 | 7 | Example to get the data of the logged in user: 8 | 9 | ```python title="user_data.py" 10 | import scratchconnect 11 | 12 | session = scratchconnect.ScratchConnect("Username", "Password") 13 | 14 | all_data = session.all_data() # Get the profile data of the logged in user in 'dict' format 15 | 16 | print(all_data) 17 | ``` 18 | 19 | ???+ question "But what if I haven't logged in using ScratchConnect? How do I get the user data?" 20 | No worries :) 21 | 22 | Try the following code: 23 | 24 | ```python title="nologin_user_data.py" 25 | import scratchconnect 26 | 27 | session = scratchconnect.ScratchConnect() # Leave the arguments blank 28 | 29 | user = session.connect_user("griffpatch") # Connect a user. For eg., "griffpatch" 30 | 31 | all_data = user.all_data() # Get the profile data of the user in 'dict' format 32 | 33 | print(all_data) 34 | ``` 35 | ## Properties/Parameters 36 | 37 | ### `#!python username: str | None` 38 | 39 | The username to login 40 | 41 | ### `#!python password: str | None` 42 | 43 | The password of that username to login 44 | 45 | ### `#!python cookie: dict | None` 46 | 47 | The cookie if you are [logging in with cookie](/getting_started/#cookie-login) 48 | 49 | ### `#!python auto_cookie_login: bool` 50 | 51 | Set it to `#!python True` if you are using [advanced cookie login](/getting_started/#advanced-cookie-login) 52 | 53 | ### `#!python online_ide_cookie: dict | None` 54 | 55 | The cookie if you are [logging in using an online IDE](/login_in_replit) 56 | 57 | ## Methods 58 | 59 | !!! note "Important Note" 60 | **Some of the methods below use the Scratch DB API to fetch the information which may not be always up to date and may raise errors if the Scratch DB API is down!** 61 | 62 | Some of the methods below require login either using `username and password` or `cookie` and will raise an error if you use those methods/functions without login! 63 | 64 | ### `update_data()` 65 | 66 | The function to update the data of the logged in user 67 | 68 | ??? info "Example" 69 | ```python 70 | import scratchconnect 71 | 72 | session = scratchconnect.ScratchConnect("Username", "Password") 73 | 74 | session.update_data() 75 | ``` 76 | 77 | ### `id()` 78 | 79 | Returns the ID of the logged in user in `#!python int` format 80 | 81 | ??? info "Example" 82 | ```python 83 | import scratchconnect 84 | 85 | session = scratchconnect.ScratchConnect("Username", "Password") 86 | 87 | print(session.id()) 88 | ``` 89 | 90 | ### `thumbnail_url()` 91 | 92 | Returns the thumbnail URL data of the logged in user in `#!python dict` format 93 | 94 | ??? info "Example" 95 | ```python 96 | import scratchconnect 97 | 98 | session = scratchconnect.ScratchConnect("Username", "Password") 99 | 100 | print(session.thumbnail_url()) 101 | ``` 102 | 103 | ### `messages_count()` 104 | 105 | Returns the messages count of the logged in user in `#!python int` format 106 | 107 | ??? info "Example" 108 | ```python 109 | import scratchconnect 110 | 111 | session = scratchconnect.ScratchConnect("Username", "Password") 112 | 113 | print(session.messages_count()) 114 | ``` 115 | 116 | ### `work()` 117 | 118 | Returns the 'What I am working on' section of the profile in `#!python str` format 119 | 120 | ??? info "Example" 121 | ```python 122 | import scratchconnect 123 | 124 | session = scratchconnect.ScratchConnect("Username", "Password") 125 | 126 | print(session.work()) 127 | ``` 128 | 129 | ### `bio()` 130 | 131 | Returns the 'About me' section of the profile in `#!python str` format 132 | 133 | ??? info "Example" 134 | ```python 135 | import scratchconnect 136 | 137 | session = scratchconnect.ScratchConnect("Username", "Password") 138 | 139 | print(session.bio()) 140 | ``` 141 | 142 | ### `status()` 143 | 144 | Returns whether the logged in user has a `Scratcher` or a `Non Scratcher` status in `#!python str` format 145 | 146 | ??? info "Example" 147 | ```python 148 | import scratchconnect 149 | 150 | session = scratchconnect.ScratchConnect("Username", "Password") 151 | 152 | print(session.status()) 153 | ``` 154 | 155 | ### `joined_date()` 156 | 157 | Returns the joined date of a Scratch profile in `#!python str` format 158 | 159 | ??? info "Example" 160 | ```python 161 | import scratchconnect 162 | 163 | session = scratchconnect.ScratchConnect("Username", "Password") 164 | 165 | print(session.joined_date()) 166 | ``` 167 | 168 | ### `country()` 169 | 170 | Returns the country of a Scratch profile in `#!python str` format 171 | 172 | ??? info "Example" 173 | ```python 174 | import scratchconnect 175 | 176 | session = scratchconnect.ScratchConnect("Username", "Password") 177 | 178 | print(session.country()) 179 | ``` 180 | 181 | ### `followers_count()` 182 | 183 | Returns the follower count of a user in `#!python int` format 184 | 185 | ??? info "Example" 186 | ```python 187 | import scratchconnect 188 | 189 | session = scratchconnect.ScratchConnect("Username", "Password") 190 | 191 | print(session.followers_count()) 192 | ``` 193 | 194 | ### `following_count()` 195 | 196 | Returns the following count of a user in `#!python int` format 197 | 198 | ??? info "Example" 199 | ```python 200 | import scratchconnect 201 | 202 | session = scratchconnect.ScratchConnect("Username", "Password") 203 | 204 | print(session.following_count()) 205 | ``` 206 | 207 | ### `total_views()` 208 | 209 | Returns the total views count of all the shared projects of a user in `#!python int` format 210 | 211 | ??? info "Example" 212 | ```python 213 | import scratchconnect 214 | 215 | session = scratchconnect.ScratchConnect("Username", "Password") 216 | 217 | print(session.total_views()) 218 | ``` 219 | 220 | ### `total_loves_count()` 221 | 222 | Returns the total loves count of all the shared projects of a user in `#!python int` format 223 | 224 | ??? info "Example" 225 | ```python 226 | import scratchconnect 227 | 228 | session = scratchconnect.ScratchConnect("Username", "Password") 229 | 230 | print(session.total_loves_count()) 231 | ``` 232 | 233 | ### `total_favourites_count()` 234 | 235 | Returns the total favourites count of all the shared projects of a user in `#!python int` format 236 | 237 | ??? info "Example" 238 | ```python 239 | import scratchconnect 240 | 241 | session = scratchconnect.ScratchConnect("Username", "Password") 242 | 243 | print(session.total_favourites_count()) 244 | ``` 245 | 246 | ### `projects_count()` 247 | 248 | Returns the total shared projects of the user in `#!python int` format 249 | 250 | ??? info "Example" 251 | ```python 252 | import scratchconnect 253 | 254 | session = scratchconnect.ScratchConnect("Username", "Password") 255 | 256 | print(session.projects_count()) 257 | ``` 258 | 259 | ### `featured_data()` 260 | 261 | Returns the featured project data of the Scratch profile in `#!python dict` format 262 | 263 | ??? info "Example" 264 | ```python 265 | import scratchconnect 266 | 267 | session = scratchconnect.ScratchConnect("Username", "Password") 268 | 269 | print(session.featured_data()) 270 | ``` 271 | 272 | ### `projects(all, limit, offset)` 273 | 274 | Returns the list of shared projects of a user in `#!python list` format 275 | 276 | **Parameters** 277 | 278 | | Name | Description | Required | Default Value | 279 | |--------|-------------------------------------------------------------------|----------|---------------| 280 | | all | Set it to True if you want to fetch all the projects of that user | No | `False` | 281 | | limit | The number of projects you want to fetch | No | 20 | 282 | | offset | The number of projects to be skipped from the beginning | No | 0 | 283 | 284 | ??? info "Example" 285 | ```python 286 | import scratchconnect 287 | 288 | session = scratchconnect.ScratchConnect("Username", "Password") 289 | 290 | print(session.projects(all=False, limit=20, offset=0)) 291 | ``` 292 | 293 | ### `following(all, limit, offset)` 294 | 295 | Returns the list of Scratchers the user is following in `#!python list` format 296 | 297 | **Parameters** 298 | 299 | | Name | Description | Required | Default Value | 300 | |--------|-------------------------------------------------------------------|----------|---------------| 301 | | all | Set it to True if you want to fetch all the following of that user| No | `False` | 302 | | limit | The number of users you want to fetch | No | 20 | 303 | | offset | The number of users to be skipped from the beginning | No | 0 | 304 | 305 | ??? info "Example" 306 | ```python 307 | import scratchconnect 308 | 309 | session = scratchconnect.ScratchConnect("Username", "Password") 310 | 311 | print(session.following(all=False, limit=20, offset=0)) 312 | ``` 313 | 314 | ### `followers(all, limit, offset)` 315 | 316 | Returns the list of Scratchers the user is followed by in `#!python list` format 317 | 318 | **Parameters** 319 | 320 | | Name | Description | Required | Default Value | 321 | |--------|-------------------------------------------------------------------|----------|---------------| 322 | | all | Set it to True if you want to fetch all the followers of that user| No | `False` | 323 | | limit | The number of users you want to fetch | No | 20 | 324 | | offset | The number of users to be skipped from the beginning | No | 0 | 325 | 326 | ??? info "Example" 327 | ```python 328 | import scratchconnect 329 | 330 | session = scratchconnect.ScratchConnect("Username", "Password") 331 | 332 | print(session.followers(all=False, limit=20, offset=0)) 333 | ``` 334 | 335 | ### `favourites(all, limit, offset)` 336 | 337 | Returns the list of projects the user has favourited in `#!python list` format 338 | 339 | **Parameters** 340 | 341 | | Name | Description | Required | Default Value | 342 | |--------|------------------------------------------------------------------------------|----------|---------------| 343 | | all | Set it to True if you want to fetch all the favourite projects of that user | No | `False` | 344 | | limit | The number of projects you want to fetch | No | 20 | 345 | | offset | The number of projects to be skipped from the beginning | No | 0 | 346 | 347 | ??? info "Example" 348 | ```python 349 | import scratchconnect 350 | 351 | session = scratchconnect.ScratchConnect("Username", "Password") 352 | 353 | print(session.favourites(all=False, limit=20, offset=0)) 354 | ``` 355 | 356 | ### `user_follower_history(all, limit, offset)` 357 | 358 | Returns the follower history of the user in `#!python list` format 359 | 360 | **Parameters** 361 | 362 | | Name | Description | Required | Default Value | 363 | |---------|-----------------------------------------|----------|---------------| 364 | | segment | The length of time between each segment | No | "" | 365 | | range | Of how far back to get history | No | 30 | 366 | 367 | ??? info "Example" 368 | ```python 369 | import scratchconnect 370 | 371 | session = scratchconnect.ScratchConnect("Username", "Password") 372 | 373 | print(session.user_follower_history(segment="", range=30)) 374 | ``` 375 | 376 | ### `all_data()` 377 | 378 | Returns all the data of the user in `#!python dict` format 379 | 380 | ??? info "Example" 381 | ```python 382 | import scratchconnect 383 | 384 | session = scratchconnect.ScratchConnect("Username", "Password") 385 | 386 | print(session.all_data()) 387 | ``` 388 | 389 | 390 | !!! note 391 | Remember to call the `update_data()` function when you need to update the data! 392 | 393 | ### `ocular_data()` 394 | 395 | Returns the [ocular](https://ocular.jeffalo.net) data of the user in `#!python dict` format 396 | 397 | ??? info "Example" 398 | ```python 399 | import scratchconnect 400 | 401 | session = scratchconnect.ScratchConnect("Username", "Password") 402 | 403 | print(session.ocular_data()) 404 | ``` 405 | 406 | ### `aviate_data()` 407 | 408 | Returns the [aviate](https://aviateapp.eu.org) data of the user in `#!python dict` format 409 | 410 | **Parameters** 411 | 412 | | Name | Description | Required | Default Value | 413 | |------|------------------------------------|----------|---------------| 414 | | code | True to get the code of the status | No | `False` | 415 | 416 | ??? info "Example" 417 | ```python 418 | import scratchconnect 419 | 420 | session = scratchconnect.ScratchConnect("Username", "Password") 421 | 422 | print(session.aviate_data(code=False)) 423 | ``` 424 | 425 | ### `comments()` 426 | 427 | Returns the comments on the user's profile 428 | 429 | **Parameters** 430 | 431 | | Name | Description | Required | Default Value | 432 | |-------|-------------|----------|-----------------| 433 | | limit | The limit | No | 5 (recommended) | 434 | | page | The page | No | 1 | 435 | 436 | ??? info "Example" 437 | ```python 438 | import scratchconnect 439 | 440 | session = scratchconnect.ScratchConnect("Username", "Password") 441 | 442 | print(session.comments(limit=5, page=1)) 443 | ``` 444 | 445 | ### `messages()` 446 | 447 | Returns the messages of the logged in user in `#!python dict` format 448 | 449 | **Parameters** 450 | 451 | | Name | Description | Required | Default Value | 452 | |--------|-------------------------------------------------------------------------------|----------|---------------| 453 | | all | Set it to `True` if you want to fetch all the messages | No | `False` | 454 | | limit | The limit of messages you want to get if you're not using the `all` parameter | No | 20 | 455 | | offset | The number of messages to skip from the beginning | No | 0 | 456 | | filter | Filter the messages | No | `""` | 457 | 458 | ??? info "Example" 459 | ```python 460 | import scratchconnect 461 | 462 | session = scratchconnect.ScratchConnect("Username", "Password") 463 | 464 | print(session.messages(all=True)) # Get all the messages 465 | 466 | print(session.messages(limit=10, offset=0)) # Get the first 10 messages 467 | 468 | print(session.messages(limit=10, offset=5)) # Get 10 messages skipping 5 from the beginning 469 | ``` 470 | 471 | ### `clear_messages()` 472 | 473 | Clears the messages (count) of the logged in user and returns the response in `#!python str` format 474 | 475 | ??? info "Example" 476 | ```python 477 | import scratchconnect 478 | 479 | session = scratchconnect.ScratchConnect("Username", "Password") 480 | 481 | print(session.clear_messages()) 482 | ``` 483 | 484 | ### `my_stuff_projects()` 485 | 486 | Get the projects in the MyStuff section of the logged in user 487 | 488 | **Parameters** 489 | 490 | | Name | Description | Required | Default Value | 491 | |---------|-------------------------|----------|---------------| 492 | | order | The order | No | all | 493 | | page | The page | No | 1 | 494 | | sort_by | Sort by a specific case | No | `""` | 495 | 496 | ??? info "Example" 497 | ```python 498 | import scratchconnect 499 | 500 | session = scratchconnect.ScratchConnect("Username", "Password") 501 | 502 | print(session.my_stuff_projects()) 503 | ``` 504 | 505 | ### `toggle_commenting()` 506 | 507 | Toggle the commenting of the profile 508 | 509 | ??? info "Example" 510 | ```python 511 | import scratchconnect 512 | 513 | session = scratchconnect.ScratchConnect("Username", "Password") 514 | 515 | print(session.toggle_commenting()) 516 | ``` 517 | 518 | ### `follow_user(username)` 519 | 520 | Follow a user 521 | 522 | **Parameter** 523 | 524 | | Name | Description | Required | Default Value | 525 | |----------|-------------------------|----------|---------------| 526 | | username | The username to follow | Yes | - | 527 | 528 | ??? info "Example" 529 | ```python 530 | import scratchconnect 531 | 532 | session = scratchconnect.ScratchConnect("Username", "Password") 533 | 534 | print(session.follow_user(username="griffpatch")) 535 | ``` 536 | 537 | ### `unfollow_user(username)` 538 | 539 | UnFollow a user 540 | 541 | **Parameter** 542 | 543 | | Name | Description | Required | Default Value | 544 | |----------|-------------------------|----------|---------------| 545 | | username | The username to unfollow| Yes | - | 546 | 547 | ??? info "Example" 548 | ```python 549 | import scratchconnect 550 | 551 | session = scratchconnect.ScratchConnect("Username", "Password") 552 | 553 | print(session.unfollow_user(username="griffpatch")) 554 | ``` 555 | -------------------------------------------------------------------------------- /scratchconnect/ScratchConnect.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main File to Connect all the Scratch API and the Scratch DB 3 | """ 4 | 5 | import requests 6 | from requests.models import Response 7 | import json 8 | import re 9 | 10 | from scratchconnect.UserCommon import UserCommon 11 | from scratchconnect import Exceptions 12 | from scratchconnect import Warnings 13 | from scratchconnect import Project 14 | from scratchconnect import Studio 15 | from scratchconnect import User 16 | from scratchconnect import Forum 17 | from scratchconnect.scOnlineIDE import _change_request_url 18 | from scratchconnect.scScratchTerminal import _terminal 19 | from scratchconnect.scImage import Image 20 | 21 | _website = "scratch.mit.edu" 22 | _login = f"https://{_website}/login/" 23 | _api = f"api.{_website}" 24 | 25 | 26 | class ScratchConnect(UserCommon): 27 | def __init__(self, username: str = None, password: str = None, cookie: dict = None, auto_cookie_login: bool = False, 28 | online_ide_cookie: dict = None): 29 | """ 30 | Class to make a connection to Scratch 31 | :param username: The username of a Scratch Profile 32 | :param password: The password of a Scratch Profile 33 | """ 34 | self.session = requests.Session() # The main Session object 35 | self.username = username 36 | self.password = password 37 | self.cookie = cookie 38 | self.scratch_session = None 39 | self._online_ide = False 40 | self.auto_cookie_login = auto_cookie_login 41 | self.online_ide_cookie = online_ide_cookie 42 | 43 | if self.username is not None and self.password is not None: 44 | self._login(cookie=False, auto_cookie_login=self.auto_cookie_login) 45 | self._logged_in = True 46 | elif self.cookie is not None: 47 | self._login(cookie=True, auto_cookie_login=self.auto_cookie_login) 48 | self._logged_in = True 49 | elif self.online_ide_cookie is not None: 50 | _change_request_url() 51 | self._online_ide = True 52 | self._login(online_ide=True) 53 | self._logged_in = True 54 | else: 55 | self._logged_in = False 56 | self.session_id = "" 57 | self.headers = { 58 | "x-csrftoken": "", 59 | "X-Token": "", 60 | "x-requested-with": "XMLHttpRequest", 61 | "Cookie": "scratchcsrftoken=" + "" + ";scratchlanguage=en;scratchsessionsid=" + "" + ";", 62 | "referer": "https://scratch.mit.edu", 63 | } 64 | Warnings.warn( 65 | "ScratchConnect Warning: Login with Username/Password and Cookie Failed! Continuing without login...") 66 | self.session.headers.update(self.headers) # Update the session headers 67 | super().__init__(self.username, self.session, 68 | self._online_ide) # Get other properties and methods from the parent(UserCommon) class 69 | self.update_data() 70 | 71 | def _login(self, cookie=False, auto_cookie_login=False, online_ide=False): 72 | """ 73 | Function to login(don't use this) 74 | """ 75 | global _user_link 76 | if cookie is False and online_ide is False: 77 | headers = { 78 | "x-csrftoken": "a", 79 | "x-requested-with": "XMLHttpRequest", 80 | "Cookie": "scratchcsrftoken=a;scratchlanguage=en;", 81 | "referer": "https://scratch.mit.edu", 82 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36" 83 | } 84 | data = json.dumps({"username": self.username, "password": self.password}) 85 | request = requests.post(f"{_login}", data=data, headers=headers) 86 | try: 87 | request.json() 88 | except Exception: 89 | if request.status_code == 403: # * 90 | if auto_cookie_login is True: 91 | self._cookie_login() 92 | else: 93 | Warnings.warn( 94 | """ScratchConnect: Scratch is not letting you login from this device.\nTry to do the following to fix this issue:\n- Try again later (10-15 minutes)\n- Use Cookie login - https://github.com/Sid72020123/scratchconnect#Cookie-Login\n- Try from another device (Scratch sometimes blocks login from Replit)""") 95 | try: 96 | self.session_id = re.search('"(.*)"', request.headers["Set-Cookie"]).group() 97 | self._get_token() 98 | except AttributeError: 99 | if auto_cookie_login is True: 100 | self._cookie_login() 101 | else: 102 | raise Exceptions.InvalidInfo('Invalid Username or Password!') 103 | self._get_csrf_token() 104 | _user_link = f"https://{_api}/users/{self.username}/" 105 | elif cookie is not False: 106 | self._cookie_login() 107 | elif online_ide is not False: 108 | self._online_ide_login() 109 | 110 | self.headers = { 111 | "x-csrftoken": self.csrf_token, 112 | "X-Token": self.token, 113 | "x-requested-with": "XMLHttpRequest", 114 | "Cookie": f"scratchcsrftoken={self.csrf_token};scratchlanguage=en;scratchsessionsid={self.session_id};", 115 | "referer": "https://scratch.mit.edu", 116 | } 117 | 118 | def _get_csrf_token(self): 119 | headers = { 120 | "x-requested-with": "XMLHttpRequest", 121 | "Cookie": "scratchlanguage=en;permissions=%7B%7D;", 122 | "referer": "https://scratch.mit.edu", 123 | } 124 | request = requests.get("https://scratch.mit.edu/csrf_token/", headers=headers) 125 | self.csrf_token = re.search("scratchcsrftoken=(.*?);", request.headers["Set-Cookie"]).group(1) 126 | 127 | def _get_token(self): 128 | response = requests.post("https://scratch.mit.edu/session", headers={ 129 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36', 130 | "x-csrftoken": "a", 131 | "x-requested-with": "XMLHttpRequest", 132 | "referer": "https://scratch.mit.edu", 133 | }, cookies={"scratchsessionsid": self.session_id, "scratchcsrftoken": "a", "scratchlanguage": "en"}).json() 134 | self.token = response['user']['token'] 135 | self.scratch_session = response 136 | if self.scratch_session["user"]["banned"]: 137 | raise Exceptions.UnauthorizedAction( 138 | "You are banned on Scratch! You cannot login from ScratchConnect unless you are unbanned! This error is raised because ScratchConnect won't allow the banned users to login and do something inappropriate!") 139 | 140 | def _cookie_login(self): 141 | if self.cookie is None: 142 | raise Exceptions.InvalidInfo("Cookie Not Provided!") 143 | try: 144 | self.username = self.cookie["Username"] 145 | self.session_id = self.cookie["SessionID"] 146 | except KeyError: 147 | raise Exceptions.InvalidInfo("Required Cookie Headers are missing!") 148 | try: 149 | self._get_token() 150 | self._get_csrf_token() 151 | Warnings.warn( 152 | "ScratchConnect Warning: You are logging in with cookie. Some features might not work if the cookie values are wrong!") 153 | except KeyError: 154 | Warnings.warn( 155 | "ScratchConnect Warning: Cookie Login Failed because the cookie values may be wrong!") 156 | self.csrf_token = "" 157 | self.token = "" 158 | 159 | def _online_ide_login(self): 160 | if self.online_ide_cookie is None: 161 | raise Exceptions.InvalidInfo("Cookie Info Not Provided!") 162 | try: 163 | self.username = self.online_ide_cookie["Username"] 164 | self.session_id = self.online_ide_cookie["SessionID"] 165 | except KeyError: 166 | raise Exceptions.InvalidInfo("Required Cookie Headers are missing!") 167 | try: 168 | self._get_token() 169 | self._get_csrf_token() 170 | Warnings.warn( 171 | "ScratchConnect Warning: You are logging in on Replit or some other online IDE. Some features might not work if the cookie values are wrong or it may be slow! Also, you can't do any social interactions!") 172 | except (KeyError, TypeError, ValueError): 173 | Warnings.warn( 174 | "ScratchConnect Warning: Fetching token or csrf_token failed! You can still continue but social interactions won't work!") 175 | self.csrf_token = "" 176 | self.token = "" 177 | 178 | def check(self, username) -> None: 179 | try: 180 | self.session.get(f"https://{_api}/users/{username}").json()["id"] 181 | except KeyError: 182 | raise Exceptions.InvalidUser(f"Username '{username}' doesn't exist!") 183 | 184 | def messages(self, all: bool = False, limit: int = 20, offset: int = 0, filter: str = "all") -> dict: 185 | """ 186 | Get the list of messages 187 | :param all: True if you want all the messages 188 | :param limit: The limit of the messages 189 | :param offset: The number of messages to be skipped from the beginning 190 | :param filter: Filter the messages 191 | :return: The list of the messages 192 | """ 193 | if self._logged_in is False: 194 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 195 | 196 | if self.user_messages is None: 197 | messages = [] 198 | if all: 199 | offset = 0 200 | while True: 201 | response = self.session.get( 202 | f"https://api.scratch.mit.edu/users/{self.username}/messages/?limit=40&offset={offset}&filter={filter}").json() 203 | messages.append(response) 204 | if len(response) != 40: 205 | break 206 | offset += 40 207 | if not all: 208 | for i in range(1, limit + 1): 209 | response = self.session.get( 210 | f"https://api.scratch.mit.edu/users/{self.username}/messages/?limit={limit}&offset={offset}&filter={filter}").json() 211 | messages.append(response) 212 | self.user_messages = messages 213 | return self.user_messages 214 | 215 | def clear_messages(self) -> str: 216 | """ 217 | Clear the messages 218 | """ 219 | if self._logged_in is False: 220 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 221 | return self.session.post(f"https://scratch.mit.edu/site-api/messages/messages-clear/").text 222 | 223 | def my_stuff_projects(self, order: str = "all", page: int = 1, sort_by: str = "") -> dict: 224 | """ 225 | Get the projects in the MyStuff section of the logged in user 226 | :param order: the order 227 | :param page: the page 228 | :param sort_by: sort 229 | """ 230 | if self._logged_in is False: 231 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 232 | return self.session.get( 233 | f"https://scratch.mit.edu/site-api/projects/{order}/?page={page}&ascsort=&descsort={sort_by}").json() 234 | 235 | def toggle_commenting(self) -> Response: 236 | """ 237 | Toggle the commenting of the profile 238 | """ 239 | return self.session.post(f"https://scratch.mit.edu/site-api/comments/user/{self.username}/toggle-comments/") 240 | 241 | def follow_user(self, username: str) -> Response: 242 | """ 243 | Follow a user 244 | :param username: The username 245 | """ 246 | self.check(username) 247 | if username == self.username: 248 | raise Exceptions.UnauthorizedAction(f"You can't follow yourself!") 249 | return self.session.put( 250 | f"https://scratch.mit.edu/site-api/users/followers/{username}/add/?usernames={self.username}") 251 | 252 | def unfollow_user(self, username: str) -> Response: 253 | """ 254 | UnFollow a user 255 | :param username: The username 256 | """ 257 | self.check(username) 258 | if username == self.username: 259 | raise Exceptions.UnauthorizedAction(f"You can't unfollow yourself!") 260 | return self.session.put( 261 | f"https://scratch.mit.edu/site-api/users/followers/{username}/remove/?usernames={self.username}") 262 | 263 | def set_bio(self, content: str) -> Response: 264 | """ 265 | Set the bio or 'About Me' of the profile 266 | :param content: The bio or the content. 267 | Thanks to QuantumCodes for helping me in the error! 268 | """ 269 | data = json.dumps({"bio": content}) 270 | return self.session.put(f"https://scratch.mit.edu/site-api/users/all/{self.username}/", data=data) 271 | 272 | def set_work(self, content: str) -> Response: 273 | """ 274 | Set the status or 'What I am Working On' of the profile 275 | :param content: The work or the content. 276 | Thanks to QuantumCodes for helping me in the error! 277 | """ 278 | data = json.dumps({"status": content}) 279 | return self.session.put(f"https://scratch.mit.edu/site-api/users/all/{self.username}/", data=data) 280 | 281 | def _check_project(self, project_id: int) -> None: 282 | """ 283 | Don't use this function 284 | """ 285 | try: 286 | self.session.get(f"https://api.scratch.mit.edu/projects/{project_id}/").json()["id"] 287 | except KeyError: 288 | raise Exceptions.InvalidProject(f"The project with ID - '{project_id}' doesn't exist!") 289 | 290 | def feed(self, limit: int = 40, offset: int = 0) -> dict: 291 | """ 292 | Returns the "What's Happening" section of the front page 293 | :param limit: the limit; max: 40 294 | :param offset: the offset 295 | """ 296 | if self._logged_in is False: 297 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 298 | return self.session.get( 299 | f"https://api.scratch.mit.edu/users/{self.username}/following/users/activity?limit={limit}&offset={offset}").json() 300 | 301 | def site_health(self) -> dict: 302 | """ 303 | Returns the health of the Scratch Website. 304 | """ 305 | return self.session.get("https://api.scratch.mit.edu/health").json() 306 | 307 | def site_news(self) -> dict: 308 | """ 309 | Returns the news of the Scratch Website. 310 | """ 311 | return self.session.get("https://api.scratch.mit.edu/news").json() 312 | 313 | def site_front_page_projects(self) -> dict: 314 | """ 315 | Returns the front page projects of the Scratch Website. 316 | """ 317 | return self.session.get("https://api.scratch.mit.edu/proxy/featured").json() 318 | 319 | def explore_projects(self, mode: str = "trending", query: str = "*") -> dict: 320 | """ 321 | Explore the projects 322 | :param mode: The mode such as 'popular' or 'trending' 323 | :param query: The query 324 | """ 325 | return self.session.get(f"https://api.scratch.mit.edu/explore/projects/?mode={mode}&q={query}").json() 326 | 327 | def explore_studios(self, mode: str = "trending", query: str = "*") -> dict: 328 | """ 329 | Explore the studios 330 | :param mode: The mode such as 'popular' or 'trending' 331 | :param query: The query 332 | """ 333 | return self.session.get(f"https://api.scratch.mit.edu/explore/studios/?mode={mode}&q={query}").json() 334 | 335 | def search_projects(self, mode: str = "trending", search: str = "*") -> dict: 336 | """ 337 | Search the projects 338 | :param mode: The mode such as 'popular' or 'trending' 339 | :param query: The query 340 | """ 341 | return self.session.get(f"https://api.scratch.mit.edu/search/projects/?mode={mode}&q={search}").json() 342 | 343 | def search_studios(self, mode: str = "trending", search: str = "*") -> dict: 344 | """ 345 | Search the studios 346 | :param mode: The mode such as 'popular' or 'trending' 347 | :param query: The query 348 | """ 349 | return self.session.get(f"https://api.scratch.mit.edu/search/studios/?mode={mode}&q={search}").json() 350 | 351 | def set_featured_project(self, project_id: int, label: str = "featured_project") -> dict: 352 | """ 353 | Set the 'Featured Project' of a Scratch Profile 354 | :param project_id: The project id 355 | :param label: The Label, options: 356 | "featured_project": "", 357 | "featured_tutorial": 0, 358 | "work_in_progress": 1, 359 | "remix_this": 2, 360 | "my_favorite_things": 3, 361 | "why_i_scratch": 4, 362 | """ 363 | if self._logged_in is False: 364 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 365 | self._check_project(project_id) 366 | if not self.session.get(f"https://api.scratch.mit.edu/projects/{project_id}/").json()["author"][ 367 | "username"] == self.username: 368 | raise Exceptions.UnauthorizedAction( 369 | f"The project with ID - '{project_id}' cannot be set because the owner of that project is not '{self.username}'!") 370 | _label = ( 371 | { 372 | "featured_project": "", 373 | "featured_tutorial": 0, 374 | "work_in_progress": 1, 375 | "remix_this": 2, 376 | "my_favorite_things": 3, 377 | "why_i_scratch": 4, 378 | } 379 | )[label] 380 | data = {"featured_project": project_id, "featured_project_label": _label} 381 | return self.session.put(f"https://scratch.mit.edu/site-api/users/all/{self.username}/", 382 | data=json.dumps(data)).json() 383 | 384 | def search_forum(self, q: str, order: str = "relevance", page: int = 0) -> dict: 385 | """ 386 | Search the forum 387 | :param q: query 388 | :param order: The order. Use values like "relevance", "newest", "oldest" 389 | :param page: page 390 | """ 391 | return requests.get(f"https://scratchdb.lefty.one/v3/forum/search?q={q}&o={order}&page={page}").json() 392 | 393 | def connect_user(self, username: str) -> User.User: 394 | """ 395 | Connect a Scratch User 396 | :param username: A valid Username 397 | """ 398 | return User.User(username=username, client_username=self.username, session=self.session, 399 | logged_in=self._logged_in, online_ide=self._online_ide) 400 | 401 | def connect_studio(self, studio_id: int) -> Studio.Studio: 402 | """ 403 | Connect a Scratch Studio 404 | :param studio_id: A valid studio ID 405 | """ 406 | return Studio.Studio(id=studio_id, client_username=self.username, session=self.session, 407 | logged_in=self._logged_in, online_ide=self._online_ide) 408 | 409 | def connect_project(self, project_id: int, access_unshared: bool = False) -> Project.Project: 410 | """ 411 | Connect a Scratch Project 412 | :param project_id: A valid project ID 413 | :param access_unshared: Set to True if you want to connect an unshared project 414 | """ 415 | return Project.Project(id=project_id, client_username=self.username, session=self.session, 416 | logged_in=self._logged_in, unshared=access_unshared, session_id=self.session_id, 417 | online_ide=self._online_ide) 418 | 419 | def connect_forum_topic(self, forum_id: int) -> Forum.Forum: 420 | """ 421 | Connect a Scratch Forum Topic 422 | :param forum_id: A valid forum topic ID 423 | """ 424 | return Forum.Forum(id=forum_id, client_username=self.username, headers=self.headers, logged_in=self._logged_in, 425 | online_ide=self._online_ide, session=self.session) 426 | 427 | def create_new_terminal(self) -> _terminal: 428 | """ 429 | Create a new Terminal object 430 | """ 431 | return _terminal(sc=self) 432 | 433 | def create_new_image(self) -> Image: 434 | """ 435 | Create a new scImage object 436 | """ 437 | return Image(online_ide=self._online_ide) 438 | -------------------------------------------------------------------------------- /scratchconnect/Studio.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Studio File 3 | """ 4 | import requests 5 | from requests.models import Response 6 | import json 7 | 8 | import scratchconnect.ScratchConnect 9 | from scratchconnect.scOnlineIDE import _change_request_url 10 | from scratchconnect import Exceptions 11 | 12 | _website = "scratch.mit.edu" 13 | _login = f"https://{_website}/login/" 14 | _api = f"https://api.{_website}" 15 | 16 | 17 | class Studio: 18 | def __init__(self, id, client_username, session, logged_in, online_ide): 19 | """ 20 | The Studio Class 21 | :param id: The ID of the studio 22 | """ 23 | self.client_username = client_username 24 | self._logged_in = logged_in 25 | self.studio_id = str(id) 26 | self.session = session 27 | self.session.headers["referer"] = f"https://scratch.mit.edu/studios/{self.studio_id}" 28 | if online_ide: 29 | _change_request_url() 30 | self.update_data() 31 | 32 | def update_data(self) -> None: 33 | """ 34 | Update the studio data 35 | """ 36 | self.studio_title = None 37 | self.studio_owner = None 38 | self.studio_description = None 39 | self.studio_visibility = None 40 | self.studio_are_comments_allowed = None 41 | self.studio_history = None 42 | self.studio_stats = None 43 | self.studio_thumbnail_url = None 44 | self.studio_projects = None 45 | self.studio_comments = None 46 | self.studio_curators = None 47 | self.studio_managers = None 48 | self.studio_activity = None 49 | 50 | data = self.session.get(f"{_api}/studios/{self.studio_id}").json() 51 | try: 52 | self.studio_id = data["id"] 53 | except KeyError: 54 | raise Exceptions.InvalidStudio(f"Studio with ID - '{self.studio_id}' doesn't exist!") 55 | self.studio_title = data["title"] 56 | self.studio_owner = data["host"] 57 | self.studio_description = data["description"] 58 | self.studio_visibility = data["visibility"] 59 | self.studio_is_public = data["public"] == True 60 | self.studio_is_open_to_all = data["open_to_all"] == True 61 | self.studio_are_comments_allowed = data["comments_allowed"] == True 62 | self.studio_history = data["history"] 63 | self.studio_stats = data["stats"] 64 | self.studio_thumbnail_url = data["image"] 65 | 66 | def _check_project(self, project_id: int) -> None: 67 | """ 68 | Don't use this function 69 | """ 70 | try: 71 | requests.get(f"https://api.scratch.mit.edu/projects/{project_id}/").json()["id"] 72 | except KeyError: 73 | raise Exceptions.InvalidProject(f"The project with ID - '{project_id}' doesn't exist!") 74 | 75 | def _check_username(self, username) -> None: 76 | """ 77 | Don't use this function 78 | """ 79 | try: 80 | requests.get(f"{_api}/users/{username}").json()["id"] 81 | except KeyError: 82 | raise scratchconnect.Exceptions.InvalidUser(f"Username '{username}' doesn't exist!") 83 | 84 | def user_id(self, username) -> int: 85 | """ 86 | Returns the user ID 87 | :param username: Username 88 | """ 89 | return self.session.get(f"{_api}/users/{username}").json()["id"] 90 | 91 | def id(self) -> int: 92 | """ 93 | Returns the studio ID 94 | """ 95 | if self.studio_id is None: 96 | self.update_data() 97 | return self.studio_id 98 | 99 | def title(self) -> str: 100 | """ 101 | Returns the studio title 102 | """ 103 | if self.studio_title is None: 104 | self.update_data() 105 | return self.studio_title 106 | 107 | def host_id(self) -> int: 108 | """ 109 | Returns the studio owner/host ID 110 | """ 111 | if self.studio_owner is None: 112 | self.update_data() 113 | return self.studio_owner 114 | 115 | def description(self) -> str: 116 | """ 117 | Returns the studio description 118 | """ 119 | if self.studio_description is None: 120 | self.update_data() 121 | return self.studio_description 122 | 123 | def visibility(self) -> str: 124 | """ 125 | Returns the studio visibility 126 | """ 127 | if self.studio_visibility is None: 128 | self.update_data() 129 | return self.studio_visibility 130 | 131 | def is_public(self) -> bool: 132 | """ 133 | Returns whether a studio is public 134 | """ 135 | if self.studio_is_public is None: 136 | self.update_data() 137 | return self.studio_is_public 138 | 139 | def is_open_to_all(self) -> bool: 140 | """ 141 | Returns whether a studio is open to all 142 | """ 143 | if self.studio_is_open_to_all is None: 144 | self.update_data() 145 | return self.studio_is_open_to_all 146 | 147 | def are_comments_allowed(self) -> bool: 148 | """ 149 | Returns whether a studio has comments allowed 150 | """ 151 | if self.studio_are_comments_allowed is None: 152 | self.update_data() 153 | return self.studio_are_comments_allowed 154 | 155 | def history(self) -> dict: 156 | """ 157 | Returns the history of the studio 158 | """ 159 | if self.studio_history is None: 160 | self.update_data() 161 | return self.studio_history 162 | 163 | def stats(self) -> dict: 164 | """ 165 | Returns the stats of the studio 166 | """ 167 | if self.studio_stats is None: 168 | self.update_data() 169 | return self.studio_stats 170 | 171 | def thumbnail_url(self) -> str: 172 | """ 173 | Returns the thumbnail URL of the studio 174 | """ 175 | if self.studio_thumbnail_url is None: 176 | self.update_data() 177 | return self.studio_thumbnail_url 178 | 179 | def add_project(self, project_id) -> dict: 180 | """ 181 | Add a project to a studio 182 | :param project_id: The project ID 183 | """ 184 | if self._logged_in is False: 185 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 186 | self._check_project(project_id) 187 | headers = {"referer": f"https://scratch.mit.edu/projects/{project_id}/"} 188 | return self.session.post(f"https://api.scratch.mit.edu/studios/{self.studio_id}/project/{project_id}/", 189 | headers=headers).json() 190 | 191 | def remove_project(self, project_id) -> dict: 192 | """ 193 | Remove a project from a studio 194 | :param project_id: The project ID 195 | """ 196 | if self._logged_in is False: 197 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 198 | self._check_project(project_id) 199 | headers = {"referer": f"https://scratch.mit.edu/projects/{project_id}/"} 200 | return self.session.post(f"https://api.scratch.mit.edu/studios/{self.studio_id}/project/{project_id}/", 201 | headers=headers).json() 202 | 203 | def open_to_public(self) -> dict: 204 | """ 205 | Open the studio to public 206 | """ 207 | if self._logged_in is False: 208 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 209 | return self.session.put( 210 | f"https://scratch.mit.edu/site-api/galleries/{self.studio_id}/mark/open/").json() 211 | 212 | def close_to_public(self) -> dict: 213 | """ 214 | Close the studio to public 215 | """ 216 | if self._logged_in is False: 217 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 218 | return requests.put( 219 | f"https://scratch.mit.edu/site-api/galleries/{self.studio_id}/mark/closed/").json() 220 | 221 | def follow_studio(self) -> dict: 222 | """ 223 | Follow the studio 224 | """ 225 | if self._logged_in is False: 226 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 227 | return self.session.put( 228 | f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.studio_id}/add/?usernames={self.client_username}").json() 229 | 230 | def unfollow_studio(self) -> dict: 231 | """ 232 | UnFollow the studio 233 | """ 234 | if self._logged_in is False: 235 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 236 | return self.session.put( 237 | f"https://scratch.mit.edu/site-api/users/bookmarkers/{self.studio_id}/remove/?usernames={self.client_username}").json() 238 | 239 | def toggle_commenting(self) -> str: 240 | """ 241 | Toggle the commenting of the studio 242 | """ 243 | if self._logged_in is False: 244 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 245 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/comments/"} 246 | return self.session.post(f"https://scratch.mit.edu/site-api/comments/gallery/{self.studio_id}/toggle-comments/", 247 | headers=headers).text 248 | 249 | def post_comment(self, content: str, parent_id: int = "", commentee_id: int = "") -> Response: 250 | """ 251 | Post comment in the studio 252 | :param content: The comment 253 | """ 254 | if self._logged_in is False: 255 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 256 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/comments/"} 257 | data = { 258 | "commentee_id": commentee_id, 259 | "content": content, 260 | "parent_id": parent_id, 261 | } 262 | return self.session.post(f"https://scratch.mit.edu/site-api/comments/gallery/{self.studio_id}/add/", 263 | headers=headers, 264 | data=json.dumps(data) 265 | ) 266 | 267 | def reply_comment(self, content: str, comment_id: int) -> Response: 268 | """ 269 | Reply a comment 270 | :param content: The content 271 | :param comment_id: The comment ID 272 | """ 273 | if self._logged_in is False: 274 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 275 | return self.post_comment(content=content, parent_id=comment_id) 276 | 277 | def delete_comment(self, comment_id: int) -> Response: 278 | """ 279 | Delete comment in the studio 280 | :param comment_id: The comment ID 281 | """ 282 | if self._logged_in is False: 283 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 284 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/comments/"} 285 | data = {"id": comment_id} 286 | return self.session.post(f"https://scratch.mit.edu/site-api/comments/user/{self.client_username}/del/", 287 | headers=headers, data=json.dumps(data)) 288 | 289 | def report_comment(self, comment_id: int) -> Response: 290 | """ 291 | Report comment in the studio 292 | :param comment_id: The comment ID 293 | """ 294 | if self._logged_in is False: 295 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 296 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/comments/"} 297 | data = {"id": comment_id} 298 | return self.session.post(f"https://scratch.mit.edu/site-api/comments/user/{self.client_username}/rep/", 299 | headers=headers, data=json.dumps(data)) 300 | 301 | def invite_curator(self, username: str) -> Response: 302 | """ 303 | Invite a user to the studio 304 | :param username: The Username 305 | """ 306 | if self._logged_in is False: 307 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 308 | self._check_username(username) 309 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/curators/"} 310 | return self.session.put( 311 | f"https://scratch.mit.edu/site-api/users/curators-in/{self.studio_id}/invite_curator/?usernames={username}", 312 | headers=headers) 313 | 314 | def remove_curator(self, username: str) -> Response: 315 | """ 316 | Remove a user from the studio 317 | :param username: The Username 318 | """ 319 | if self._logged_in is False: 320 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 321 | self._check_username(username) 322 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/curators/"} 323 | return self.session.put( 324 | f"https://scratch.mit.edu/site-api/users/curators-in/{self.studio_id}/remove/?usernames={username}", 325 | headers=headers) 326 | 327 | def accept_curator(self) -> Response: 328 | """ 329 | Accept the curator invitation in a studio 330 | """ 331 | if self._logged_in is False: 332 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 333 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/curators/"} 334 | return self.session.put( 335 | f"https://scratch.mit.edu/site-api/users/curators-in/{self.studio_id}/add/?usernames={self.client_username}", 336 | headers=headers) 337 | 338 | def promote_curator(self, username: str) -> Response: 339 | """ 340 | Promote a user in the studio 341 | :param username: The Username 342 | """ 343 | if self._logged_in is False: 344 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 345 | self._check_username(username) 346 | headers = {"referer": f"https://scratch.mit.edu/studios/{self.studio_id}/curators/"} 347 | return self.session.put( 348 | f"https://scratch.mit.edu/site-api/users/curators-in/{self.studio_id}/promote/?usernames={username}", 349 | headers=headers) 350 | 351 | def set_description(self, content: str) -> dict: 352 | """ 353 | Set the description of a Studio 354 | :param content: The description or content 355 | """ 356 | if self._logged_in is False: 357 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 358 | data = json.dumps({"description": content}) 359 | return self.session.put(f"https://scratch.mit.edu/site-api/galleries/all/{self.studio_id}/", 360 | data=data).json() 361 | 362 | def set_title(self, content: str) -> dict: 363 | """ 364 | Set the title of a Studio 365 | :param content: The title or content 366 | """ 367 | if self._logged_in is False: 368 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 369 | data = json.dumps({"title": content}) 370 | return self.session.put(f"https://scratch.mit.edu/site-api/galleries/all/{self.studio_id}/", 371 | data=data).json() 372 | 373 | def projects(self, all: bool = False, limit: int = 20, offset: int = 0) -> list: 374 | """ 375 | Get the projects of the studio 376 | :param all: If you want all the projects then set it to True 377 | :param limit: The limit 378 | :param offset: The offset or the number of data you want from the beginning 379 | """ 380 | if self.studio_projects is None: 381 | projects = [] 382 | if all: 383 | limit = 40 384 | offset = 0 385 | while True: 386 | response = self.session.get( 387 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/projects/?limit={limit}&offset={offset}").json() 388 | projects.append(response) 389 | offset += 40 390 | if len(response) != 40: 391 | break 392 | if not all: 393 | projects.append(self.session.get( 394 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/projects/?limit={limit}&offset={offset}").json()) 395 | self.studio_projects = projects 396 | return self.studio_projects 397 | 398 | def comments(self, all: bool = False, limit: int = 20, offset: int = 0) -> list: 399 | """ 400 | Get the comments of the studio 401 | :param all: If you want all the comments then set it to True 402 | :param limit: The limit 403 | :param offset: The offset or the number of data you want from the beginning 404 | """ 405 | if self.studio_comments is None: 406 | comments = [] 407 | if all: 408 | limit = 40 409 | offset = 0 410 | while True: 411 | response = self.session.get( 412 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/comments/?limit={limit}&offset={offset}").json() 413 | comments.append(response) 414 | offset += 40 415 | if len(response) != 40: 416 | break 417 | if not all: 418 | comments.append(self.session.get( 419 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/comments/?limit={limit}&offset={offset}").json()) 420 | self.studio_comments = comments 421 | return self.studio_comments 422 | 423 | def comment_replies(self, comment_id: int, limit: int = 20, offset: int = 0) -> list: 424 | """ 425 | Get the comment replies from the comment ID 426 | :param comment_id: The comment ID 427 | :param limit: The limit (max: 40) 428 | :param offset: The offset or the number of replies to skip from the beginning 429 | """ 430 | return self.session.get( 431 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/comments/{comment_id}/replies?limit={limit}&offset={offset}").json() 432 | 433 | # Doesn't work :/ 434 | # def set_thumbnail(self, image_path: str) -> Response: 435 | # """ 436 | # Set the thumbnail of the Studio 437 | # :param image_path: The path of the image 438 | # """ 439 | # if self._logged_in is False: 440 | # raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 441 | # with open(image_path, "rb") as f: 442 | # image = f.read() 443 | # return self.session.post(f"https://scratch.mit.edu/site-api/galleries/all/{self.studio_id}", 444 | # data=image) 445 | 446 | def curators(self, all: bool = False, limit: int = 20, offset: int = 0) -> list: 447 | """ 448 | Get the curators of the studio 449 | :param all: If you want all the curators then set it to True 450 | :param limit: The limit 451 | :param offset: The offset or the number of data you want from the beginning 452 | """ 453 | if self.studio_curators is None: 454 | curators = [] 455 | if all: 456 | limit = 40 457 | offset = 0 458 | while True: 459 | response = self.session.get( 460 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/curators/?limit={limit}&offset={offset}").json() 461 | curators.append(response) 462 | offset += 40 463 | if len(response) != 40: 464 | break 465 | if not all: 466 | curators.append(self.session.get( 467 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/curators/?limit={limit}&offset={offset}").json()) 468 | self.studio_curators = curators 469 | return self.studio_curators 470 | 471 | def managers(self, all: bool = False, limit: int = 20, offset: int = 0) -> list: 472 | """ 473 | Get the managers of the studio 474 | :param all: If you want all the managers then set it to True 475 | :param limit: The limit 476 | :param offset: The offset or the number of data you want from the beginning 477 | """ 478 | if self.studio_managers is None: 479 | managers = [] 480 | if all: 481 | limit = 40 482 | offset = 0 483 | while True: 484 | response = self.session.get( 485 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/managers/?limit={limit}&offset={offset}").json() 486 | managers.append(response) 487 | offset += 40 488 | if len(response) != 40: 489 | break 490 | if not all: 491 | managers.append(self.session.get( 492 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/managers/?limit={limit}&offset={offset}").json()) 493 | self.studio_managers = managers 494 | return self.studio_managers 495 | 496 | def activity(self, all: bool = False, limit: int = 20, offset: int = 0) -> list: 497 | """ 498 | Get the activity of the studio 499 | :param all: If you want all the activity then set it to True 500 | :param limit: The limit 501 | :param offset: The offset or the number of data you want from the beginning 502 | """ 503 | if self.studio_activity is None: 504 | activity = [] 505 | if all: 506 | limit = 40 507 | offset = 0 508 | while True: 509 | response = self.session.get( 510 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/activity/?limit={limit}&offset={offset}").json() 511 | activity.append(response) 512 | offset += 40 513 | if len(response) != 40: 514 | break 515 | if not all: 516 | activity.append(self.session.get( 517 | f"https://api.scratch.mit.edu/studios/{self.studio_id}/activity/?limit={limit}&offset={offset}").json()) 518 | self.studio_activity = activity 519 | return self.studio_activity 520 | 521 | def all_data(self) -> dict: 522 | """ 523 | Returns all the data of a Scratch Studio 524 | """ 525 | data = { 526 | 'Studio ID': self.id(), 527 | 'Title': self.title(), 528 | 'Host ID': self.host_id(), 529 | 'Description': self.description(), 530 | 'Comments Count': self.stats()['comments'], 531 | 'Followers Count': self.stats()['followers'], 532 | 'Managers Count': self.stats()['managers'], 533 | 'Projects Count': self.stats()['projects'], 534 | 'Visibility': self.visibility(), 535 | 'Is Public?': self.is_public(), 536 | 'Is Open To All?': self.is_open_to_all(), 537 | 'Are Comments Allowed?': self.are_comments_allowed(), 538 | } 539 | return data 540 | -------------------------------------------------------------------------------- /scratchconnect/Project.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Project File 3 | """ 4 | import json 5 | 6 | import requests 7 | from requests.models import Response 8 | 9 | from scratchconnect.scOnlineIDE import _change_request_url 10 | from scratchconnect import CloudConnection 11 | from scratchconnect import TurbowarpCloudConnection 12 | from scratchconnect import scCloudRequests 13 | from scratchconnect import Exceptions 14 | from scratchconnect import Warnings 15 | 16 | _website = "scratch.mit.edu" 17 | _login = f"https://{_website}/login/" 18 | _api = f"api.{_website}" 19 | _project = f"https://{_api}/projects/" 20 | 21 | 22 | class Project: 23 | def __init__(self, id, client_username, session, unshared, logged_in, session_id, online_ide): 24 | """ 25 | The Project Class 26 | :param id: The project ID 27 | """ 28 | self.access_unshared = unshared 29 | self.project_id = str(id) 30 | self.client_username = client_username 31 | self.session = session 32 | self.csrf_token = self.session.headers["x-csrftoken"] 33 | self.session_id = session_id 34 | self.token = self.session.headers["X-Token"] 35 | self._logged_in = logged_in 36 | self.json_headers = { 37 | "x-csrftoken": self.csrf_token, 38 | "X-Token": self.token, 39 | "x-requested-with": "XMLHttpRequest", 40 | "Cookie": f"scratchcsrftoken={self.csrf_token};scratchlanguage=en;scratchsessionsid={self.session_id};", 41 | "referer": f"https://scratch.mit.edu/projects/{self.project_id}/", 42 | "accept": "application/json", 43 | "Content-Type": "application/json", 44 | "origin": f"https://{_website}" 45 | } 46 | if online_ide: 47 | _change_request_url() 48 | self.online_ide = online_ide 49 | self.update_data() 50 | 51 | def update_data(self) -> None: 52 | self.project_author = None 53 | self.project_title = None 54 | self.project_notes = None 55 | self.project_instructions = None 56 | self.project_are_comments_allowed = None 57 | self.project_stats = None 58 | self.project_history = None 59 | self.project_remix_data = None 60 | self.project_visibility = None 61 | self.project_is_public = None 62 | self.project_is_published = None 63 | self.project_thubmnail_url = None 64 | self.project_token = None 65 | 66 | data = self.session.get(f"{_project}{self.project_id}").json() 67 | try: 68 | self.project_id = data["id"] 69 | except KeyError: 70 | if self.access_unshared: 71 | pass 72 | else: 73 | raise Exceptions.InvalidProject( 74 | f"The project with ID - '{self.project_id}' doesn't exist or is unshared! To connect an unshared project using ScratchConnect, use the access_unshared parameter of the Project class.") 75 | if not self.access_unshared: 76 | self.project_author = data["author"] 77 | self.project_title = data["title"] 78 | self.project_notes = data["description"] 79 | self.project_instructions = data["instructions"] 80 | self.project_are_comments_allowed = data["comments_allowed"] == True 81 | self.project_stats = data["stats"] 82 | self.project_history = data["history"] 83 | self.project_remix_data = data["remix"] 84 | self.project_visibility = data["visibility"] 85 | self.project_is_public = data["public"] == True 86 | self.project_is_published = data["is_published"] == True 87 | self.project_thubmnail_url = data["images"] 88 | self.project_token = data["project_token"] 89 | 90 | def id(self) -> int: 91 | """ 92 | Returns the project ID 93 | """ 94 | return self.project_id 95 | 96 | def author(self) -> dict: 97 | """ 98 | Returns the author of the project 99 | """ 100 | if self.project_author is None: 101 | self.update_data() 102 | return self.project_author 103 | 104 | def title(self) -> str: 105 | """ 106 | Returns the title of the project 107 | """ 108 | if self.title is None: 109 | self.update_data() 110 | return self.project_title 111 | 112 | def notes(self) -> str: 113 | """ 114 | Returns the notes(Notes or Credits) of the project 115 | """ 116 | if self.project_notes is None: 117 | self.update_data() 118 | return self.project_notes 119 | 120 | def instructions(self) -> str: 121 | """ 122 | Returns the instructions of the project 123 | """ 124 | if self.project_instructions is None: 125 | self.update_data() 126 | return self.project_instructions 127 | 128 | def are_comments_allowed(self) -> bool: 129 | """ 130 | Returns whether the comments are allowed in a project 131 | """ 132 | if self.project_are_comments_allowed is None: 133 | self.update_data() 134 | return self.project_are_comments_allowed 135 | 136 | def stats(self) -> dict: 137 | """ 138 | Returns the stats of a project 139 | """ 140 | if self.project_stats is None: 141 | self.update_data() 142 | return self.project_stats 143 | 144 | def history(self) -> dict: 145 | """ 146 | Returns the history of a project 147 | """ 148 | if self.project_history is None: 149 | self.update_data() 150 | return self.project_history 151 | 152 | def remix_data(self) -> dict: 153 | """ 154 | Returns the remix data of a project 155 | """ 156 | if self.project_remix_data is None: 157 | self.update_data() 158 | return self.project_remix_data 159 | 160 | def visibility(self) -> str: 161 | """ 162 | Returns whether the project is visible 163 | """ 164 | if self.project_visibility is None: 165 | self.update_data() 166 | return self.project_visibility 167 | 168 | def is_public(self) -> bool: 169 | """ 170 | Returns whether the project is public 171 | """ 172 | if self.project_is_public is None: 173 | self.update_data() 174 | return self.project_is_public 175 | 176 | def is_published(self) -> bool: 177 | """ 178 | Returns whether the project is published 179 | """ 180 | if self.project_is_published is None: 181 | self.update_data() 182 | return self.project_is_public 183 | 184 | def thumbnail_url(self) -> dict: 185 | """ 186 | Returns the thumbnail url of a project 187 | """ 188 | if self.project_thubmnail_url is None: 189 | self.update_data() 190 | return self.project_thubmnail_url 191 | 192 | def assets_info(self) -> dict: 193 | """ 194 | Returns the Assets info of a project 195 | """ 196 | return requests.get(f"https://scratchdb.lefty.one/v3/project/info/{self.project_id}").json()[ 197 | "metadata"] 198 | 199 | def scripts(self) -> dict: 200 | """ 201 | Returns the scripts of a project 202 | """ 203 | return self.session.get(f"https://projects.scratch.mit.edu/{self.project_id}?token={self.project_token}").json() 204 | 205 | def love(self) -> dict: 206 | """ 207 | Love a project 208 | """ 209 | if self._logged_in is False: 210 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 211 | return self.session.post( 212 | f"https://api.scratch.mit.edu/proxy/projects/{self.project_id}/loves/user/{self.client_username}").json() 213 | 214 | def unlove(self) -> dict: 215 | """ 216 | UnLove a project 217 | """ 218 | if self._logged_in is False: 219 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 220 | return self.session.delete( 221 | f"https://api.scratch.mit.edu/proxy/projects/{self.project_id}/loves/user/{self.client_username}").json() 222 | 223 | def favourite(self) -> dict: 224 | """ 225 | Favourite a project 226 | """ 227 | if self._logged_in is False: 228 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 229 | return self.session.post( 230 | f"https://api.scratch.mit.edu/proxy/projects/{self.project_id}/favorites/user/{self.client_username}").json() 231 | 232 | def unfavourite(self): 233 | """ 234 | UnFavourite a project 235 | """ 236 | if self._logged_in is False: 237 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 238 | return self.session.delete( 239 | f"https://api.scratch.mit.edu/proxy/projects/{self.project_id}/favorites/user/{self.client_username}").json() 240 | 241 | def comments(self, all: bool = False, limit: int = 20, offset: int = 0, comment_id: int = None) -> list: 242 | """ 243 | Returns the list of comments of a project 244 | :param all: True if you want all 245 | :param limit: The limit 246 | :param offset: The offset or the data which you want after the beginning 247 | :param comment_id: If you want a comment from its ID then use this 248 | """ 249 | if self.project_author is None: 250 | self.update_data() 251 | comments = [] 252 | if all: 253 | offset = 40 254 | limit = 40 255 | while True: 256 | response = self.session.get( 257 | f"https://api.scratch.mit.edu/users/{self.project_author['username']}/projects/{self.project_id}/comments/?limit={limit}&offset={offset}").json() 258 | if len(response) != 40: 259 | break 260 | offset += 40 261 | comments.append(response) 262 | if not all: 263 | comments.append(self.session.get( 264 | f"https://api.scratch.mit.edu/users/{self.project_author['username']}/projects/{self.project_id}/comments/?limit={limit}&offset={offset}" 265 | ).json()) 266 | if comment_id is not None: 267 | comments = [] 268 | comments.append(self.session.get( 269 | f"https://api.scratch.mit.edu/users/{self.project_author['username']}/projects/{self.project_id}/comments/{comment_id}" 270 | ).json()) 271 | return comments 272 | 273 | def comment_replies(self, comment_id: int, limit: int = 20, offset: int = 0) -> list: 274 | """ 275 | Get the replies of the comment 276 | :param comment_id: ID of the comment 277 | :param limit: The limit (max: 40) 278 | :param offset: The number of replies to skip from the beginning 279 | """ 280 | return self.session.get( 281 | f"https://api.scratch.mit.edu/users/{self.project_author['username']}/projects/{self.project_id}/comments/{comment_id}/replies?limit={limit}&offset={offset}").json() 282 | 283 | def remixes(self, all: bool = False, limit: int = 20, offset: int = 0) -> list: 284 | """ 285 | Returns the list of remixes of a project 286 | :param all: True if you want all 287 | :param limit: The limit 288 | :param offset: The offset or the data which you want after the beginning 289 | """ 290 | projects = [] 291 | if all: 292 | offset = 0 293 | while True: 294 | response = self.session.get( 295 | f"https://api.scratch.mit.edu/projects/{self.project_id}/remixes/?limit=40&offset={offset}").json() 296 | projects += response 297 | if len(response) != 40: 298 | break 299 | offset += 40 300 | else: 301 | projects.append(self.session.get( 302 | f"https://api.scratch.mit.edu/projects/{self.project_id}/remixes/?limit={limit}&offset={offset}").json()) 303 | return projects 304 | 305 | def post_comment(self, content: str, parent_id: int = "", commentee_id: int = "") -> Response: 306 | """ 307 | Post a comment 308 | :param content: The comment or the content 309 | """ 310 | if self._logged_in is False: 311 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 312 | data = { 313 | "commentee_id": commentee_id, 314 | "content": content, 315 | "parent_id": parent_id, 316 | } 317 | return self.session.post(f"https://api.scratch.mit.edu/proxy/comments/project/{self.project_id}/", 318 | data=json.dumps(data), headers=self.json_headers) 319 | 320 | def reply_comment(self, content: str, comment_id: int) -> Response: 321 | """ 322 | Reply a comment 323 | :param content: The content 324 | :param comment_id: The comment ID 325 | """ 326 | if self._logged_in is False: 327 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 328 | return self.post_comment(content=content, parent_id=comment_id) 329 | 330 | def toggle_commenting(self) -> dict: 331 | """ 332 | Toggle the commenting of a project 333 | """ 334 | if self._logged_in is False: 335 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 336 | if self.author()['username'] != self.client_username: 337 | raise Exceptions.UnauthorizedAction( 338 | f"You are not allowed to do that because you are not the owner of the project with ID - '{self.project_id}'!") 339 | data = {"comments_allowed": not self.are_comments_allowed()} 340 | return self.session.put(f"https://api.scratch.mit.edu/projects/{self.project_id}/", data=json.dumps(data), 341 | headers=self.json_headers).json() 342 | 343 | def turn_on_commenting(self) -> dict: 344 | """ 345 | Turn On the commenting of a project 346 | """ 347 | if self._logged_in is False: 348 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 349 | if self.author()['username'] != self.client_username: 350 | raise Exceptions.UnauthorizedAction( 351 | f"You are not allowed to do that because you are not the owner of the project with ID - '{self.project_id}'!") 352 | data = {"comments_allowed": True} 353 | return self.session.put(f"https://api.scratch.mit.edu/projects/{self.project_id}/", data=json.dumps(data), 354 | headers=self.json_headers).json() 355 | 356 | def turn_off_commenting(self) -> dict: 357 | """ 358 | Turn Off the commenting of a project 359 | """ 360 | if self._logged_in is False: 361 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 362 | if self.author()['username'] != self.client_username: 363 | raise Exceptions.UnauthorizedAction( 364 | f"You are not allowed to do that because you are not the owner of the project with ID - '{self.project_id}'!") 365 | data = {"comments_allowed": False} 366 | return self.session.put(f"https://api.scratch.mit.edu/projects/{self.project_id}/", data=json.dumps(data), 367 | headers=self.json_headers, ).json() 368 | 369 | def report(self, category: str, reason: str, image: str = None) -> str: 370 | """ 371 | Report a project 372 | :param category: The category 373 | :param reason: The reason 374 | """ 375 | if self._logged_in is False: 376 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 377 | if self.author()['username'] == self.client_username: 378 | raise Exceptions.UnauthorizedAction("You can't report your own project!") 379 | if not image: 380 | image = self.thumbnail_url() 381 | data = {"notes": reason, "report_category": category, "thumbnail": image} 382 | return self.session.post(f"https://api.scratch.mit.edu/proxy/comments/project/{self.project_id}/", 383 | data=json.dumps(data), headers=self.json_headers).text 384 | 385 | def unshare(self) -> Response: 386 | """ 387 | Unshare a project 388 | """ 389 | if self._logged_in is False: 390 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 391 | if self.author()['username'] != self.client_username: 392 | raise Exceptions.UnauthorizedAction( 393 | f"You are not allowed to do that because you are not the owner of the project with ID - '{self.project_id}'!") 394 | return self.session.put(f"https://api.scratch.mit.edu/proxy/projects/{self.project_id}/unshare/", 395 | headers=self.json_headers) 396 | 397 | def share(self) -> Response: 398 | """ 399 | Share a project 400 | """ 401 | Warnings.warn( 402 | "ScratchConnect Warning: The 'share()' function doesn't work sometimes because the Scratch server blocks the request returning the status code 503.") 403 | if self._logged_in is False: 404 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 405 | return self.session.put(f"https://api.scratch.mit.edu/proxy/projects/{self.project_id}/share/", 406 | headers=self.json_headers) 407 | 408 | def view(self) -> Response: 409 | """ 410 | Just view a project 411 | """ 412 | if self._logged_in is False: 413 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 414 | return self.session.post( 415 | f"https://api.scratch.mit.edu/users/{self.client_username}/projects/{self.project_id}/views/") 416 | 417 | def set_thumbnail(self, file: str) -> Response: 418 | """ 419 | Set the thumbnail of a project 420 | :param file: The location of the file 421 | """ 422 | if self._logged_in is False: 423 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 424 | if self.author()['username'] != self.client_username: 425 | raise Exceptions.UnauthorizedAction( 426 | f"You are not allowed to do that because you are not the owner of the project with ID - '{self.project_id}'!") 427 | with open(file, "rb") as f: 428 | image = f.read() 429 | return self.session.post(f"https://scratch.mit.edu/internalapi/project/thumbnail/{self.project_id}/set/", 430 | data=image) 431 | 432 | def delete_comment(self, comment_id: int) -> Response: 433 | """ 434 | Delete a comment 435 | :param comment_id: Comment ID 436 | """ 437 | if self._logged_in is False: 438 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 439 | return self.session.delete( 440 | f"https://api.scratch.mit.edu/proxy/comments/project/{self.project_id}/comment/{comment_id}") 441 | 442 | def report_comment(self, comment_id: int) -> Response: 443 | """ 444 | Report a comment 445 | :param comment_id: Comment ID 446 | """ 447 | if self._logged_in is False: 448 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 449 | return self.session.delete( 450 | f"https://api.scratch.mit.edu/proxy/comments/project/{self.project_id}/comment/{comment_id}/report") 451 | 452 | def set_title(self, title: str) -> dict: 453 | """ 454 | Set the title of the project 455 | :param title: The title 456 | """ 457 | if self._logged_in is False: 458 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 459 | data = {'title': title} 460 | return self.session.put(f"{_project}{self.project_id}", data=json.dumps(data), headers=self.json_headers).json() 461 | 462 | def set_description(self, description: str) -> dict: 463 | """ 464 | Set the description of the project 465 | :param description: The description 466 | """ 467 | if self._logged_in is False: 468 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 469 | data = {'description': description} 470 | return self.session.put(f"{_project}{self.project_id}", data=json.dumps(data), headers=self.json_headers).json() 471 | 472 | def set_instruction(self, instruction: str) -> dict: 473 | """ 474 | Set the instruction of the project 475 | :param instruction: The instruction 476 | """ 477 | if self._logged_in is False: 478 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 479 | data = {'instructions': instruction} 480 | return self.session.put(f"{_project}{self.project_id}", data=json.dumps(data), headers=self.json_headers).json() 481 | 482 | def remix_project(self, title: str) -> dict: 483 | """ 484 | Remix the project 485 | :param title: The title of the remixed project 486 | """ 487 | Warnings.warn( 488 | "ScratchConnect Warning: The 'remix_project()' function doesn't work sometimes because the Scratch server blocks the request returning the status code 503.") 489 | if self._logged_in is False: 490 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 491 | if self.access_unshared: 492 | raise Exceptions.UnauthorizedAction( 493 | "Cannot perform the action because the project as is accessed as an unshared project.") 494 | return self.session.post( 495 | f"https://projects.scratch.mit.edu/?is_remix=1&original_id={self.id()}&title={title}", 496 | data=json.dumps(self.scripts()), headers={"origin": f"https://{_website}"}) 497 | 498 | def connect_cloud_variables(self) -> CloudConnection.CloudConnection: 499 | """ 500 | Connect the cloud variables of the project 501 | """ 502 | if self._logged_in is False: 503 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 504 | return CloudConnection.CloudConnection(project_id=self.project_id, client_username=self.client_username, 505 | csrf_token=self.csrf_token, 506 | session_id=self.session_id, token=self.token, session=self.session, 507 | online_ide=self.online_ide) 508 | 509 | def connect_turbowarp_cloud(self, username: str = None) -> TurbowarpCloudConnection.TurbowarpCloudConnection: 510 | """ 511 | Connect the cloud variables of the project 512 | """ 513 | if username is None: 514 | username = self.client_username 515 | return TurbowarpCloudConnection.TurbowarpCloudConnection(project_id=self.project_id, 516 | username=username) 517 | 518 | def create_cloud_storage(self): 519 | """ 520 | Create a Cloud Database/Storage in a project 521 | """ 522 | Warnings.warn( 523 | "ScratchConnect Warning: The Cloud Storage feature is deprecated since the v5.0 of the library. Please use the new alternative Cloud Requests feature instead!") 524 | 525 | def create_cloud_requests(self, handle_all_errors: bool = True, 526 | print_logs: bool = True) -> scCloudRequests.CloudRequests: 527 | """ 528 | Create a Cloud Database/Storage in a project 529 | """ 530 | if self._logged_in is False: 531 | raise Exceptions.UnauthorizedAction("Cannot perform the action because the user is not logged in!") 532 | return scCloudRequests.CloudRequests(project_id=self.project_id, 533 | client_username=self.client_username, 534 | csrf_token=self.csrf_token, 535 | session_id=self.session_id, token=self.token, 536 | handle_all_errors=handle_all_errors, print_logs=print_logs, 537 | session=self.session, online_ide=self.online_ide) 538 | 539 | def all_data(self) -> dict: 540 | """ 541 | Returns all the data of a Scratch Project 542 | """ 543 | data = { 544 | 'Project ID': self.id(), 545 | 'Project Name': self.title(), 546 | 'Author': self.author()['username'], 547 | 'Are Comments Allowed?': self.are_comments_allowed(), 548 | 'Views': self.stats()['views'], 549 | 'Loves': self.stats()['loves'], 550 | 'Favourites': self.stats()['favorites'], 551 | 'Remixes': self.stats()['remixes'], 552 | 'Visibility': self.visibility(), 553 | 'Is public?': self.is_public(), 554 | 'Is published?': self.is_published(), 555 | 'Version': self.assets_info()['version'], 556 | 'Costumes': self.assets_info()['costumes'], 557 | 'Blocks': self.assets_info()['blocks'], 558 | 'Variables': self.assets_info()['variables'], 559 | 'Assets': self.assets_info()['assets'] 560 | } 561 | return data 562 | --------------------------------------------------------------------------------