Scratch3技术分析之创作平台API(第1篇)

创作平台

Scratch3.0的创作平台在这儿可以体验。创作平台是用户的创作工具,用户在这里头拖拽积木,编程程序。我们重点关注创作平台的这些功能:

  • 创建新项目
  • 更新项目
  • 分享项目
  • 改编项目

创作平台的源代码托管在GitHub: LLK/scratch-gui

Scratch3技术分析系列文章中中,我们计划从创作平台的后端API开始分析。之后转向分析社区的后端API。

下边正式开始我们的分析, 分析主要依赖于Chrome DevTools.

打开创作平台

在此只讨论已登陆状态。你如果想复现以下分析,需要先注册一个用户。我注册的测试用户是api_test

假设你目前已经处于登陆状态。

创建新项目

每当你打开创作平台: https://scratch.mit.edu/projects/editor, 你会发现scratch后端自动为你创建了一个新项目,前端redirect到https://scratch.mit.edu/projects/[projects_id]/editor,形如https://scratch.mit.edu/projects/278159553/editor

这块是前端(client)策略,和后端API关系不大。

之所以提及,是因为它可能令人困惑,因为许多平台,是在第一次提交数据时,才创建项目。而Scratch的创作工具,则在你打开创作工具时,就默认为你创建了项目。

由于第一次打开就创建(POST)了新项目,于是之后的任何操作都是更新它(PUT)

创建项目实际是: POST https://projects.scratch.mit.edu/, 细节如下:

可以发现,即便用户啥也没做,也会有数据被提交(在request payload中可以看到). 不过这样一来,POST(创建)和之后的PUT(保存用户作品)几乎一致。

后端返回:{"status":"ok","content-name":"278159553","content-title":"VW50aXRsZWQtMzY=","autosave-interval":"120"}

保存数据

搭建两块积木,点击立即保存

保存期间,与后端发生了3次api交互:

OPTIONS https://projects.scratch.mit.edu/278159553
PUT https://projects.scratch.mit.edu/278159553
POST https://scratch.mit.edu/internalapi/project/thumbnail/278159553/set/

OPTIONS https://projects.scratch.mit.edu/278159553可以忽视,OPTIONS是Request Method的一种,用于获取目的资源所支持的通信选项。


保存项目数据(json)

PUT https://projects.scratch.mit.edu/278159553 的请求细节为:

如果你对官方的后端技术栈感兴趣(希望不是为了做坏事),多关注Response Headers,其中server: restify暴露了scratch的后端框架用了restify

我们来看看提交的数据.(不必去理解它,为了构建后端API,无需注意提交的json数据的内部细节)

Request Headers中我们得知content-type: application/json, 数据以json格式发往后端。

保存的数据为:

{"targets":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["我的变量",0]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"assetId":"cd21514d0531fdffb22204e0ec5ed84a","name":"背景1","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"啵","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"角色1","variables":{},"lists":{},"broadcasts":{},"blocks":{"qI)a2qkxK=]VtK^%eH@~":{"opcode":"motion_movesteps","next":null,"parent":"FYZnCl:MFx`sQhhiEwkj","inputs":{"STEPS":[1,[4,"10"]]},"fields":{},"shadow":false,"topLevel":false},"FYZnCl:MFx`sQhhiEwkj":{"opcode":"event_whenflagclicked","next":"qI)a2qkxK=]VtK^%eH@~","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":150,"y":32}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"b7853f557e4426412e64bb3da6531a99","name":"造型1","bitmapResolution":1,"md5ext":"b7853f557e4426412e64bb3da6531a99.svg","dataFormat":"svg","rotationCenterX":48,"rotationCenterY":50},{"assetId":"e6ddc55a6ddd9cc9d84fe0b4c21e016f","name":"造型2","bitmapResolution":1,"md5ext":"e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg","dataFormat":"svg","rotationCenterX":46,"rotationCenterY":53}],"sounds":[{"assetId":"83c36d806dc92327b9e7049a565c6bff","name":"喵","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"0.2.0-prerelease.20190107184530","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}}

之后我们单独写一篇文章讨论Scratch的项目数据结构,它是被精心设计过的,非常优美。

后端的返回json作为响应{"status":"ok","content-length":32744,"content-name":"278159553","autosave-internal":"120","result-code":0}Status Code为200。 非常REST。


更新项目缩略图

POST https://scratch.mit.edu/internalapi/project/thumbnail/278159553/set/用于更新项目的缩略图。

舞台区将被截图发网后端(之后在社区和项目展示页作为项目的封面)

获取项目数据(GET)

在新页面打开我们刚才保存的项目:https://scratch.mit.edu/projects/278159553/editor

关注以下API交互:

GET https://projects.scratch.mit.edu/278159553
GET https://api.scratch.mit.edu/projects/278159553

GET https://projects.scratch.mit.edu/278159553后台返回的数据,正是我们刚才提交的:

{"targets":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["我的变量",0]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"assetId":"cd21514d0531fdffb22204e0ec5ed84a","name":"背景1","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"啵","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"角色1","variables":{},"lists":{},"broadcasts":{},"blocks":{"qI)a2qkxK=]VtK^%eH@~":{"opcode":"motion_movesteps","next":null,"parent":"FYZnCl:MFx`sQhhiEwkj","inputs":{"STEPS":[1,[4,"10"]]},"fields":{},"shadow":false,"topLevel":false},"FYZnCl:MFx`sQhhiEwkj":{"opcode":"event_whenflagclicked","next":"qI)a2qkxK=]VtK^%eH@~","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":150,"y":32}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"b7853f557e4426412e64bb3da6531a99","name":"造型1","bitmapResolution":1,"md5ext":"b7853f557e4426412e64bb3da6531a99.svg","dataFormat":"svg","rotationCenterX":48,"rotationCenterY":50},{"assetId":"e6ddc55a6ddd9cc9d84fe0b4c21e016f","name":"造型2","bitmapResolution":1,"md5ext":"e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg","dataFormat":"svg","rotationCenterX":46,"rotationCenterY":53}],"sounds":[{"assetId":"83c36d806dc92327b9e7049a565c6bff","name":"喵","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"0.2.0-prerelease.20190107184530","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}}

我们将https://projects.scratch.mit.edu/278159553提供的信息称为项目的内部数据。这部分信息是scratch项目的数据表示形式。它的另一个表示形式是scratch程序。我们将项目保存到本地时,这些json整个在一个文件中,所以我们可以把它整体视为静态文件。

看到这里,小伙伴可能有点纳闷了,怎么返回的数据中没有项目meta信息: 项目的名字啦、项目点赞数啦、项目id啦、项目的缩略图地址啦之类的东西。

这些信息由GET https://api.scratch.mit.edu/projects/278159553接口系统

{
  "id": 278159553,
  "title": "Untitled-36",
  "description": "",
  "instructions": "",
  "visibility": "visible",
  "public": true,
  "comments_allowed": true,
  "is_published": true,
  "author": {
    "id": 18286387,
    "username": "wwj718",
    "scratchteam": false,
    "history": { "joined": "1900-01-01T00:00:00.000Z" },
    "profile": {
      "id": null,
      "images": {
        "90x90": "https://cdn2.scratch.mit.edu/get_image/user/18286387_90x90.png?v=",
        "60x60": "https://cdn2.scratch.mit.edu/get_image/user/18286387_60x60.png?v=",
        "55x55": "https://cdn2.scratch.mit.edu/get_image/user/18286387_55x55.png?v=",
        "50x50": "https://cdn2.scratch.mit.edu/get_image/user/18286387_50x50.png?v=",
        "32x32": "https://cdn2.scratch.mit.edu/get_image/user/18286387_32x32.png?v="
      }
    }
  },
  "image": "https://cdn2.scratch.mit.edu/get_image/project/278159553_480x360.png",
  "images": {
    "282x218": "https://cdn2.scratch.mit.edu/get_image/project/278159553_282x218.png?v=1547026704",
    "216x163": "https://cdn2.scratch.mit.edu/get_image/project/278159553_216x163.png?v=1547026704",
    "200x200": "https://cdn2.scratch.mit.edu/get_image/project/278159553_200x200.png?v=1547026704",
    "144x108": "https://cdn2.scratch.mit.edu/get_image/project/278159553_144x108.png?v=1547026704",
    "135x102": "https://cdn2.scratch.mit.edu/get_image/project/278159553_135x102.png?v=1547026704",
    "100x80": "https://cdn2.scratch.mit.edu/get_image/project/278159553_100x80.png?v=1547026704"
  },
  "history": {
    "created": "2019-01-09T09:16:15.000Z",
    "modified": "2019-01-09T09:38:24.000Z",
    "shared": "2019-01-09T11:02:36.000Z" // 未分享是null
  },
  "stats": {
    "views": 1,
    "loves": 0,
    "favorites": 0,
    "comments": 0,
    "remixes": 0
  },
  "remix": { "parent": null, "root": null }
}

我们把这些数据称为项目的外部数据,主要与项目的社交属性有关(分享/点赞/混合)。

至此我们差不多就可以构建Scratch project的后端model了。

顺便也把GET https://api.scratch.mit.edu/projects/278159553的通信细节列出

分享项目

PUT https://api.scratch.mit.edu/proxy/projects/278159553/share

后端返回:{"is_published":"true"}

交互细节

这里值得一提的是。url中包含proxy,从语义猜测,这是个过度阶段得api,原先这部分的api用django写,目前还没有使用restify完全实现,只是在restify中包装了django得api。 其中x-csrftoken很可能就是django需要的header参数。

改编项目

改编项目,概念颇似github中的fork。

我们来改编这个项目: https://scratch.mit.edu/projects/276660763/editor/

点击改变,URL变为https://scratch.mit.edu/projects/278183746/editor

发生的API交互为:

POST https://projects.scratch.mit.edu/?is_remix=1&original_id=276660763&title=Scratch%203.0%20is%20here%21
POST https://scratch.mit.edu/internalapi/project/thumbnail/278183746/set/

可见,改编本质上是基于当前页面的项目数据,创建新项目,在url参数中说明改编自什么父项目!API的一致性非常好!

POST https://projects.scratch.mit.edu/?is_remix=1&original_id=276660763&title=Scratch%203.0%20is%20here%21返回:

{"status":"ok","content-name":"278183746","content-title":"U2NyYXRjaCAzLjAgaXMgaGVyZSEgcmVtaXgtMw==","autosave-interval":"120"}

获取改编项目的数据

GET https://projects.scratch.mit.edu/278183746 (项目的内部数据)

{"targets":[{"isStage":true,"name":"Stage","variables":{},"lists":{},"broadcasts":{},"blocks":{"3cR(o:tw]HBgKsti_Q)S":{"opcode":"event_whenflagclicked","next":"+kvKfEp%EYSJFj%RGky2","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":52,"y":49},"+kvKfEp%EYSJFj%RGky2":{"opcode":"control_forever","next":null,"parent":"3cR(o:tw]HBgKsti_Q)S","inputs":{"SUBSTACK":[2,":T1p=hf!cs59HaF5n8Id"]},"fields":{},"shadow":false,"topLevel":false},":T1p=hf!cs59HaF5n8Id":{"opcode":"sound_playuntildone","next":null,"parent":"+kvKfEp%EYSJFj%RGky2","inputs":{"SOUND_MENU":[1,"x##owp)CLS^@CW|%Mr;a"]},"fields":{},"shadow":false,"topLevel":false},"x##owp)CLS^@CW|%Mr;a":{"opcode":"sound_sounds_menu","next":null,"parent":":T1p=hf!cs59HaF5n8Id","inputs":{},"fields":{"SOUND_MENU":["Celebration Song"]},"shadow":true,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"e42904fd488ec20699e3525b2e768c4e","name":"Background","bitmapResolution":1,"md5ext":"e42904fd488ec20699e3525b2e768c4e.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[{"assetId":"65fb0fd9faf8ecdb07e203998b6067fc","name":"Celebration Song","dataFormat":"wav","rate":22050,"sampleCount":5688585,"md5ext":"65fb0fd9faf8ecdb07e203998b6067fc.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"off","textToSpeechLanguage":null},{"isStage":false,"name":"Scratch Cat","variables":{},"lists":{},"broadcasts":{},"blocks":{"DAgri|f0b0-qvj1dbB(j":{"opcode":"event_whenflagclicked","next":"))L(a^)NJBLu*qnD0zqW","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":63,"y":58},"))L(a^)NJBLu*qnD0zqW":{"opcode":"motion_gotoxy","next":"3i1!GO9qPSqF`B([=qgD","parent":"DAgri|f0b0-qvj1dbB(j","inputs":{"X":[1,[4,0]],"Y":[1,[4,0]]},"fields":{},"shadow":false,"topLevel":false},"3i1!GO9qPSqF`B([=qgD":{"opcode":"motion_setrotationstyle","next":"xEl;UQ[q([{FmimS]%nc","parent":"))L(a^)NJBLu*qnD0zqW","inputs":{},"fields":{"STYLE":["left-right"]},"shadow":false,"topLevel":false},"xEl;UQ[q([{FmimS]%nc":{"opcode":"looks_switchcostumeto","next":"sxxM;2LRrhj2ogrDc4G}","parent":"3i1!GO9qPSqF`B([=qgD","inputs":{"COSTUME":[1,"%+B_`S0Uff?]w.;4B`}P"]},"fields":{},"shadow":false,"topLevel":false},"%+B_`S0Uff?]w.;4B`}P":{"opcode":"looks_costume","next":null,"parent":"xEl;UQ[q([{FmimS]%nc","inputs":{},"fields":{"COSTUME":["Launch Dance 1"]},"shadow":true,"topLevel":false},"sxxM;2LRrhj2ogrDc4G}":{"opcode":"control_forever","next":null,"parent":"xEl;UQ[q([{FmimS]%nc","inputs":{"SUBSTACK":[2,"/{pGoV)XFJUG71~PJGQo"]},"fields":{},"shadow":false,"topLevel":false},"/{pGoV)XFJUG71~PJGQo":{"opcode":"motion_pointindirection","next":"z8yx%IhP{jKU|J3xiQO7","parent":"sxxM;2LRrhj2ogrDc4G}","inputs":{"DIRECTION":[1,[8,90]]},"fields":{},"shadow":false,"topLevel":false},"z8yx%IhP{jKU|J3xiQO7":{"opcode":"control_repeat","next":"[#%1}sI.R5vOsvH28L^/","parent":"/{pGoV)XFJUG71~PJGQo","inputs":{"TIMES":[1,[6,6]],"SUBSTACK":[2,"PZc2Hh||URK}Ra6S3KXz"]},"fields":{},"shadow":false,"topLevel":false},"PZc2Hh||URK}Ra6S3KXz":{"opcode":"looks_nextcostume","next":".FblvK`:%q_p`Z}_qs0M","parent":"z8yx%IhP{jKU|J3xiQO7","inputs":{},"fields":{},"shadow":false,"topLevel":false},".FblvK`:%q_p`Z}_qs0M":{"opcode":"control_wait","next":null,"parent":"PZc2Hh||URK}Ra6S3KXz","inputs":{"DURATION":[1,[5,0.5]]},"fields":{},"shadow":false,"topLevel":false},"[#%1}sI.R5vOsvH28L^/":{"opcode":"motion_pointindirection","next":"uNba4/i7dp)@C^Q[4iRy","parent":"z8yx%IhP{jKU|J3xiQO7","inputs":{"DIRECTION":[1,[8,-90]]},"fields":{},"shadow":false,"topLevel":false},"uNba4/i7dp)@C^Q[4iRy":{"opcode":"control_repeat","next":null,"parent":"[#%1}sI.R5vOsvH28L^/","inputs":{"TIMES":[1,[6,6]],"SUBSTACK":[2,"]q[m:+M5h)*ueKe1F=wN"]},"fields":{},"shadow":false,"topLevel":false},"]q[m:+M5h)*ueKe1F=wN":{"opcode":"looks_nextcostume","next":"tdjsS~#EW5lD=RwVT?8(","parent":"uNba4/i7dp)@C^Q[4iRy","inputs":{},"fields":{},"shadow":false,"topLevel":false},"tdjsS~#EW5lD=RwVT?8(":{"opcode":"control_wait","next":null,"parent":"]q[m:+M5h)*ueKe1F=wN","inputs":{"DURATION":[1,[5,0.5]]},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":1,"costumes":[{"assetId":"7b7b5534b7220d7b3b650fb0ef4c1231","name":"Launch Dance 1","bitmapResolution":1,"md5ext":"7b7b5534b7220d7b3b650fb0ef4c1231.svg","dataFormat":"svg","rotationCenterX":78,"rotationCenterY":135},{"assetId":"6f5cfd00497303a9df804baca5ba46cd","name":"Launch Dance 2","bitmapResolution":1,"md5ext":"6f5cfd00497303a9df804baca5ba46cd.svg","dataFormat":"svg","rotationCenterX":78,"rotationCenterY":145}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"pop","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":2,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"left-right"},{"isStage":false,"name":"confetti","variables":{},"lists":{},"broadcasts":{},"blocks":{"*M6p!(P?e@riXF+:j/Zn":{"opcode":"event_whenflagclicked","next":"EpRWF`|+9^X@p]]njua1","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":42,"y":59},"EpRWF`|+9^X@p]]njua1":{"opcode":"looks_hide","next":"hB}JrE-+#L0gT~6/p*y6","parent":"*M6p!(P?e@riXF+:j/Zn","inputs":{},"fields":{},"shadow":false,"topLevel":false},"hB}JrE-+#L0gT~6/p*y6":{"opcode":"control_forever","next":null,"parent":"EpRWF`|+9^X@p]]njua1","inputs":{"SUBSTACK":[2,"!x*HhPq?~.6BeqTsZ4ro"]},"fields":{},"shadow":false,"topLevel":false},"!x*HhPq?~.6BeqTsZ4ro":{"opcode":"control_create_clone_of","next":null,"parent":"hB}JrE-+#L0gT~6/p*y6","inputs":{"CLONE_OPTION":[1,"{?bm)?kpbHHW{mqNzvqI"]},"fields":{},"shadow":false,"topLevel":false},"{?bm)?kpbHHW{mqNzvqI":{"opcode":"control_create_clone_of_menu","next":null,"parent":"!x*HhPq?~.6BeqTsZ4ro","inputs":{},"fields":{"CLONE_OPTION":["_myself_"]},"shadow":true,"topLevel":false},"FM!NAH@`eu4UT,JPH/]^":{"opcode":"control_start_as_clone","next":"ch1sNDRxQpDV}Fl:]C?a","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":55,"y":490},"ch1sNDRxQpDV}Fl:]C?a":{"opcode":"looks_show","next":"gm-8lOB8OHXcR*,4@D!G","parent":"FM!NAH@`eu4UT,JPH/]^","inputs":{},"fields":{},"shadow":false,"topLevel":false},"gm-8lOB8OHXcR*,4@D!G":{"opcode":"motion_gotoxy","next":"iw/rG]nC]vw,kLCSqSVh","parent":"ch1sNDRxQpDV}Fl:]C?a","inputs":{"X":[3,"09Nmy6@[tVNmU!_[eZB2",[4,10]],"Y":[1,[4,180]]},"fields":{},"shadow":false,"topLevel":false},"09Nmy6@[tVNmU!_[eZB2":{"opcode":"operator_random","next":null,"parent":"gm-8lOB8OHXcR*,4@D!G","inputs":{"FROM":[1,[4,-233]],"TO":[1,[4,233]]},"fields":{},"shadow":false,"topLevel":false},"iw/rG]nC]vw,kLCSqSVh":{"opcode":"looks_switchcostumeto","next":"]-u,r6LbB#*_w0nPF82S","parent":"gm-8lOB8OHXcR*,4@D!G","inputs":{"COSTUME":[3,"odmz7bTTPOm-N@Vdog]5","IDT#NqY,5U6(`G7!ax*}"]},"fields":{},"shadow":false,"topLevel":false},"odmz7bTTPOm-N@Vdog]5":{"opcode":"operator_random","next":null,"parent":"iw/rG]nC]vw,kLCSqSVh","inputs":{"FROM":[1,[4,1]],"TO":[1,[4,4]]},"fields":{},"shadow":false,"topLevel":false},"IDT#NqY,5U6(`G7!ax*}":{"opcode":"looks_costume","next":null,"parent":"iw/rG]nC]vw,kLCSqSVh","inputs":{},"fields":{"COSTUME":[""]},"shadow":true,"topLevel":false},"]-u,r6LbB#*_w0nPF82S":{"opcode":"looks_seteffectto","next":"`Eln_V*EJZG{h=LX3)^x","parent":"iw/rG]nC]vw,kLCSqSVh","inputs":{"VALUE":[3,"V[DD].JkDo647[(M*,d4",[4,10]]},"fields":{"EFFECT":["color"]},"shadow":false,"topLevel":false},"V[DD].JkDo647[(M*,d4":{"opcode":"operator_random","next":null,"parent":"]-u,r6LbB#*_w0nPF82S","inputs":{"FROM":[1,[4,0]],"TO":[1,[4,100]]},"fields":{},"shadow":false,"topLevel":false},"`Eln_V*EJZG{h=LX3)^x":{"opcode":"looks_setsizeto","next":"Dj]~SwUD+Kw{~8[Xu0~`","parent":"]-u,r6LbB#*_w0nPF82S","inputs":{"SIZE":[3,"+/)uc{CnYnmR[N#P41Eu",[4,10]]},"fields":{},"shadow":false,"topLevel":false},"+/)uc{CnYnmR[N#P41Eu":{"opcode":"operator_random","next":null,"parent":"`Eln_V*EJZG{h=LX3)^x","inputs":{"FROM":[1,[4,100]],"TO":[1,[4,125]]},"fields":{},"shadow":false,"topLevel":false},"Dj]~SwUD+Kw{~8[Xu0~`":{"opcode":"motion_glidesecstoxy","next":"RN;6V~MOndS*z3K+X_AK","parent":"`Eln_V*EJZG{h=LX3)^x","inputs":{"SECS":[3,"tXzt,MO(_+Gie9j!MEG+",[4,10]],"X":[3,"o*`O]VAp(2f=cuGYZ%Wg",[4,10]],"Y":[1,[4,-177]]},"fields":{},"shadow":false,"topLevel":false},"tXzt,MO(_+Gie9j!MEG+":{"opcode":"operator_random","next":null,"parent":"Dj]~SwUD+Kw{~8[Xu0~`","inputs":{"FROM":[1,[4,0.5]],"TO":[1,[4,2]]},"fields":{},"shadow":false,"topLevel":false},"o*`O]VAp(2f=cuGYZ%Wg":{"opcode":"motion_xposition","next":null,"parent":"Dj]~SwUD+Kw{~8[Xu0~`","inputs":{},"fields":{},"shadow":false,"topLevel":false},"RN;6V~MOndS*z3K+X_AK":{"opcode":"control_delete_this_clone","next":null,"parent":"Dj]~SwUD+Kw{~8[Xu0~`","inputs":{},"fields":{},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"024e4a12770c91f6843e1f1773a31272","name":"costume1","bitmapResolution":1,"md5ext":"024e4a12770c91f6843e1f1773a31272.svg","dataFormat":"svg","rotationCenterX":4,"rotationCenterY":4},{"assetId":"8e54feb32cdddc3a3dc146f48777479c","name":"costume2","bitmapResolution":1,"md5ext":"8e54feb32cdddc3a3dc146f48777479c.svg","dataFormat":"svg","rotationCenterX":4,"rotationCenterY":3},{"assetId":"c32c7bcb9f5870a824f6f8ce1facebf8","name":"costume3","bitmapResolution":1,"md5ext":"c32c7bcb9f5870a824f6f8ce1facebf8.svg","dataFormat":"svg","rotationCenterX":4,"rotationCenterY":4},{"assetId":"e2b2d97f76d0f3fcc54dc0d192976eec","name":"costume4","bitmapResolution":1,"md5ext":"e2b2d97f76d0f3fcc54dc0d192976eec.svg","dataFormat":"svg","rotationCenterX":3,"rotationCenterY":4}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"pop","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":1,"visible":false,"x":-164,"y":-177,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"},{"isStage":false,"name":"Thumbnail","variables":{},"lists":{},"broadcasts":{},"blocks":{"v|[%SWWcCG_z#=5k8^(m":{"opcode":"event_whenflagclicked","next":"@YebWp8N.]:!u1QFj}p[","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":59,"y":68},"@YebWp8N.]:!u1QFj}p[":{"opcode":"motion_gotoxy","next":"8^cnVM0FqA*GtC:wq0XD","parent":"v|[%SWWcCG_z#=5k8^(m","inputs":{"X":[1,[4,"0"]],"Y":[1,[4,"0"]]},"fields":{},"shadow":false,"topLevel":false},"8^cnVM0FqA*GtC:wq0XD":{"opcode":"looks_seteffectto","next":null,"parent":"@YebWp8N.]:!u1QFj}p[","inputs":{"VALUE":[1,[4,"100"]]},"fields":{"EFFECT":["ghost"]},"shadow":false,"topLevel":false}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"8e5cc5d3958709d6a80bde80f94751f9","name":"Editable","bitmapResolution":1,"md5ext":"8e5cc5d3958709d6a80bde80f94751f9.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180},{"assetId":"9230a0b1c1176bf5ce32b3a79f16405b","name":"Thumbnail","bitmapResolution":2,"md5ext":"9230a0b1c1176bf5ce32b3a79f16405b.png","dataFormat":"png","rotationCenterX":480,"rotationCenterY":360}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"pop","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":3,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"0.2.0-prerelease.20190107184530","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}}

GET https://api.scratch.mit.edu/projects/278183746(项目的外部数据)

{
  "id": 278183746,
  "title": "Scratch 3.0 is here! remix-3",
  "description": "",
  "instructions": "Scratch 3.0 is here! \n\nRemix this project and join us in celebration! We look forward to seeing what you create :D\n\nMeow!\n=^..^=",
  "visibility": "visible",
  "public": false,
  "comments_allowed": true,
  "is_published": false,
  "author": {
    "id": 18286387,
    "username": "wwj718",
    "scratchteam": false,
    "history": { "joined": "1900-01-01T00:00:00.000Z" },
    "profile": {
      "id": null,
      "images": {
        "90x90": "https://cdn2.scratch.mit.edu/get_image/user/18286387_90x90.png?v=",
        "60x60": "https://cdn2.scratch.mit.edu/get_image/user/18286387_60x60.png?v=",
        "55x55": "https://cdn2.scratch.mit.edu/get_image/user/18286387_55x55.png?v=",
        "50x50": "https://cdn2.scratch.mit.edu/get_image/user/18286387_50x50.png?v=",
        "32x32": "https://cdn2.scratch.mit.edu/get_image/user/18286387_32x32.png?v="
      }
    }
  },
  "image": "https://cdn2.scratch.mit.edu/get_image/project/278183746_480x360.png",
  "images": {
    "282x218": "https://cdn2.scratch.mit.edu/get_image/project/278183746_282x218.png?v=1547032372",
    "216x163": "https://cdn2.scratch.mit.edu/get_image/project/278183746_216x163.png?v=1547032372",
    "200x200": "https://cdn2.scratch.mit.edu/get_image/project/278183746_200x200.png?v=1547032372",
    "144x108": "https://cdn2.scratch.mit.edu/get_image/project/278183746_144x108.png?v=1547032372",
    "135x102": "https://cdn2.scratch.mit.edu/get_image/project/278183746_135x102.png?v=1547032372",
    "100x80": "https://cdn2.scratch.mit.edu/get_image/project/278183746_100x80.png?v=1547032372"
  },
  "history": {
    "created": "2019-01-09T11:12:51.000Z",
    "modified": "2019-01-09T11:12:52.000Z",
    "shared": null
  },
  "stats": {
    "views": 1,
    "loves": 0,
    "favorites": 0,
    "comments": 0,
    "remixes": 0
  },
  "remix": { "parent": 276660763, "root": 276660763 }
}

2019.01.21更新


以上人有些数据有空缺,我给出一份更完整的:

{
  "id": 281071071,
  "title": "Scratch Translate 2.0 remix",
  "description": "",
  "instructions": "Make sure your sound is on! This translator features ambient music and some nifty sound effects :)\n\nWith the recent addition of text to speech and translation functions, this is currently the most advanced translator on Scratch possible! Featuring...\n\n-12 unique languages\n-Mobile support\n-Text to speech in respective languages\n-Prompts based on chosen language\n-Swapping two languages\n-Translating without retyping phrases\n\nInstructions:\nSelect the respective language by clicking the drop down button. The switch button in the center allows you to swap between two languages. When trying to translate a phrase into different languages, you can change the second language and your translation will automatically update! If you want to write a new phrase, press space or use the translation button. Finally, you can listen to text-to-speech using the sound buttons.",
  "visibility": "visible",
  "public": true,
  "comments_allowed": true,
  "is_published": true,
  "author": {
    "id": 18286387,
    "username": "wwj718",
    "scratchteam": false,
    "history": { "joined": "1900-01-01T00:00:00.000Z" },
    "profile": {
      "id": null,
      "images": {
        "90x90": "https://cdn2.scratch.mit.edu/get_image/user/18286387_90x90.png?v=",
        "60x60": "https://cdn2.scratch.mit.edu/get_image/user/18286387_60x60.png?v=",
        "55x55": "https://cdn2.scratch.mit.edu/get_image/user/18286387_55x55.png?v=",
        "50x50": "https://cdn2.scratch.mit.edu/get_image/user/18286387_50x50.png?v=",
        "32x32": "https://cdn2.scratch.mit.edu/get_image/user/18286387_32x32.png?v="
      }
    }
  },
  "image": "https://cdn2.scratch.mit.edu/get_image/project/281071071_480x360.png",
  "images": {
    "282x218": "https://cdn2.scratch.mit.edu/get_image/project/281071071_282x218.png?v=1548057585",
    "216x163": "https://cdn2.scratch.mit.edu/get_image/project/281071071_216x163.png?v=1548057585",
    "200x200": "https://cdn2.scratch.mit.edu/get_image/project/281071071_200x200.png?v=1548057585",
    "144x108": "https://cdn2.scratch.mit.edu/get_image/project/281071071_144x108.png?v=1548057585",
    "135x102": "https://cdn2.scratch.mit.edu/get_image/project/281071071_135x102.png?v=1548057585",
    "100x80": "https://cdn2.scratch.mit.edu/get_image/project/281071071_100x80.png?v=1548057585"
  },
  "history": {
    "created": "2019-01-21T07:54:14.000Z",
    "modified": "2019-01-21T07:59:45.000Z",
    "shared": "2019-01-21T07:59:45.000Z"
  },
  "stats": {
    "views": 1,
    "loves": 0,
    "favorites": 0,
    "comments": 0,
    "remixes": 0
  },
  "remix": { "parent": 279503424, "root": 279207380 }
}

删除项目

目前需要到https://scratch.mit.edu/mystuff/里边才能删除项目。首先你要将项目改为非分享模式,然后才能删除。

删除项目的web请求为:PUT https://scratch.mit.edu/site-api/projects/all/281079681/

请求数据为:

{
  "view_count": 1,
  "favorite_count": 0,
  "remixers_count": 0,
  "creator": "wwj718",
  "title": "Untitled-40",
  "isPublished": false,
  "datetime_created": "2019-01-21T08:52:31",
  "thumbnail_url": "//uploads.scratch.mit.edu/projects/thumbnails/281079681.png",
  "visibility": "trshbyusr",
  "love_count": 0,
  "datetime_modified": "2019-01-21T09:22:56.784",
  "uncached_thumbnail_url": "//cdn2.scratch.mit.edu/get_image/project/281079681_100x80.png",
  "thumbnail": "281079681.png",
  "datetime_shared": "2019-01-21T08:54:17",
  "commenters_count": 0,
  "id": 281079681
}

需要注意:"visibility": "trshbyusr",,trshbyusr意思因该是用户将项目移到垃圾桶。应该是伪删除。

ps:这个接口目前正在迁移中。

你仍然可以访问到该项目,只是它在垃圾桶里:https://scratch.mit.edu/projects/281079681/

可以看到,当前项目外部数据为:"visibility": "notvisible",

非法请求

为了让我们自定义的后端兼容scratch的前端,我们需要把非法请求的后端响应也做个分析. 以便后端对错误的响应和官方完全一致,这样我们就不必对scratch前端做任何修改。

我们重点分析,这两种典型的请求错误。

  • 未登录
  • 提交错误数据

未登录

未登录状态下与需要用户身份的API交互,产生的错误,一般是403错误。

一试之下,果不其然(使用httpie测试)

http PUT https://projects.scratch.mit.edu/278159553

提交错误数据

接下来,我们来模拟提交错误数据。(已登陆)

http PUT https://projects.scratch.mit.edu/278159553  'Cookie:_ga=GA1.3.971025469.1526828393; _ga=GA1.2.1108813793.1527058082; ajs_user_id=null; ajs_group_id=null; ajs_anonymous_id=%22006109e0-522e-443a-89fc-0d5ce2d618ce%22; __unam=70b497d-16492cf2b97-22ea0fd0-2; __utmc=133675020; _gcl_au=1.1.1178771398.1541398301; scratchcsrftoken=6yewIduDcyNoLwsdlzFu9U8ruIGR6d82; _gid=GA1.3.1921625291.1547005822; __utmz=133675020.1547009438.121.16.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided); __utma=133675020.971025469.1526828393.1547009438.1547028631.122; __utmb=133675020.0.10.1547028631; scratchsessionsid=".eJxVj0tPxCAUhf8La6cWKNDObiYZ3agLjRrdNBQuLX3ApI80qfG_C6ab2d2c75ybc37QMsHo5ADoiNa1FThHd6iUy9yUkZRWB4BzknOai4BmmGblfWf_A37sQN8GKqk6cDEVNXCzVXK23iU7mJJXuPa7eN7N4a8PRwgpkxJdcG0w5RkWpCgMEQpLATkTBSbH7_ppaR634bn94Kf2gZ2bt-398Fmm2yW86X1t3cFeY2lME84TLGiSp7FjL129yDoW35qDckHTbdB8OdsBNu8iOQ0whnL3L7CWX2He7bhGTk0waWwAdGqqQgJlnBFlKFSp4lRWQEhmqGIZ4Qz9_gGKgHFO:1ghAol:ZmjO6wosjoQAYN0n5dCpVvrrkA4"' < /tmp/project.json

Cookie数据从Chrome DevTools中获得。

第一次我使用有效的项目数据(json)去更新它(将角色名字改为test1).

cat /tmp/project.json:

{"targets":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["我的变量",0]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"assetId":"cd21514d0531fdffb22204e0ec5ed84a","name":"背景1","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"啵","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"test1","variables":{},"lists":{},"broadcasts":{},"blocks":{"qI)a2qkxK=]VtK^%eH@~":{"opcode":"motion_movesteps","next":null,"parent":"FYZnCl:MFx`sQhhiEwkj","inputs":{"STEPS":[1,[4,"10"]]},"fields":{},"shadow":false,"topLevel":false},"FYZnCl:MFx`sQhhiEwkj":{"opcode":"event_whenflagclicked","next":"qI)a2qkxK=]VtK^%eH@~","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":150,"y":32}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"b7853f557e4426412e64bb3da6531a99","name":"造型1","bitmapResolution":1,"md5ext":"b7853f557e4426412e64bb3da6531a99.svg","dataFormat":"svg","rotationCenterX":48,"rotationCenterY":50},{"assetId":"e6ddc55a6ddd9cc9d84fe0b4c21e016f","name":"造型2","bitmapResolution":1,"md5ext":"e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg","dataFormat":"svg","rotationCenterX":46,"rotationCenterY":53}],"sounds":[{"assetId":"83c36d806dc92327b9e7049a565c6bff","name":"喵","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"0.2.0-prerelease.20190107184530","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}}

修改成功:

接下来我使用无效的项目数据("targets"->"targets1"),

cat /tmp/project.json:

{"targets1":[{"isStage":true,"name":"Stage","variables":{"`jEk@4|i[#Fk?(8x)AV.-my variable":["我的变量",0]},"lists":{},"broadcasts":{},"blocks":{},"comments":{},"currentCostume":0,"costumes":[{"assetId":"cd21514d0531fdffb22204e0ec5ed84a","name":"背景1","md5ext":"cd21514d0531fdffb22204e0ec5ed84a.svg","dataFormat":"svg","rotationCenterX":240,"rotationCenterY":180}],"sounds":[{"assetId":"83a9787d4cb6f3b7632b4ddfebf74367","name":"啵","dataFormat":"wav","format":"","rate":44100,"sampleCount":1032,"md5ext":"83a9787d4cb6f3b7632b4ddfebf74367.wav"}],"volume":100,"layerOrder":0,"tempo":60,"videoTransparency":50,"videoState":"on","textToSpeechLanguage":null},{"isStage":false,"name":"test1","variables":{},"lists":{},"broadcasts":{},"blocks":{"qI)a2qkxK=]VtK^%eH@~":{"opcode":"motion_movesteps","next":null,"parent":"FYZnCl:MFx`sQhhiEwkj","inputs":{"STEPS":[1,[4,"10"]]},"fields":{},"shadow":false,"topLevel":false},"FYZnCl:MFx`sQhhiEwkj":{"opcode":"event_whenflagclicked","next":"qI)a2qkxK=]VtK^%eH@~","parent":null,"inputs":{},"fields":{},"shadow":false,"topLevel":true,"x":150,"y":32}},"comments":{},"currentCostume":0,"costumes":[{"assetId":"b7853f557e4426412e64bb3da6531a99","name":"造型1","bitmapResolution":1,"md5ext":"b7853f557e4426412e64bb3da6531a99.svg","dataFormat":"svg","rotationCenterX":48,"rotationCenterY":50},{"assetId":"e6ddc55a6ddd9cc9d84fe0b4c21e016f","name":"造型2","bitmapResolution":1,"md5ext":"e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg","dataFormat":"svg","rotationCenterX":46,"rotationCenterY":53}],"sounds":[{"assetId":"83c36d806dc92327b9e7049a565c6bff","name":"喵","dataFormat":"wav","format":"","rate":44100,"sampleCount":37376,"md5ext":"83c36d806dc92327b9e7049a565c6bff.wav"}],"volume":100,"layerOrder":1,"visible":true,"x":0,"y":0,"size":100,"direction":90,"draggable":false,"rotationStyle":"all around"}],"monitors":[],"extensions":[],"meta":{"semver":"3.0.0","vm":"0.2.0-prerelease.20190107184530","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}}

有点尴尬,这个错误信息没有被官方处理(可能会引起灾难,大家不要试了).原来想测试学习官方的错误处理机制了,结果测出了官方后端的bug。我稍后给他们反馈一下。

以上的演示有助于大家了解,如何与后端交互,并且测试它,所以我都保留下来。

相关链接




Fork me on GitHub