scratchattach 2.1.15b0__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. cli/__about__.py +1 -0
  2. cli/__init__.py +26 -0
  3. cli/cmd/__init__.py +4 -0
  4. cli/cmd/group.py +127 -0
  5. cli/cmd/login.py +60 -0
  6. cli/cmd/profile.py +7 -0
  7. cli/cmd/sessions.py +5 -0
  8. cli/context.py +142 -0
  9. cli/db.py +66 -0
  10. cli/namespace.py +14 -0
  11. {scratchattach/cloud → cloud}/_base.py +112 -87
  12. {scratchattach/cloud → cloud}/cloud.py +16 -16
  13. {scratchattach/editor → editor}/__init__.py +2 -1
  14. {scratchattach/editor → editor}/asset.py +26 -14
  15. {scratchattach/editor → editor}/backpack_json.py +3 -5
  16. {scratchattach/editor → editor}/base.py +2 -4
  17. {scratchattach/editor → editor}/block.py +27 -22
  18. {scratchattach/editor → editor}/blockshape.py +1 -1
  19. {scratchattach/editor → editor}/build_defaulting.py +2 -2
  20. editor/commons.py +145 -0
  21. {scratchattach/editor → editor}/field.py +1 -1
  22. {scratchattach/editor → editor}/inputs.py +6 -3
  23. {scratchattach/editor → editor}/meta.py +10 -7
  24. {scratchattach/editor → editor}/monitor.py +10 -8
  25. {scratchattach/editor → editor}/mutation.py +68 -11
  26. {scratchattach/editor → editor}/pallete.py +1 -3
  27. {scratchattach/editor → editor}/prim.py +4 -0
  28. {scratchattach/editor → editor}/project.py +118 -16
  29. {scratchattach/editor → editor}/sprite.py +25 -15
  30. {scratchattach/editor → editor}/vlb.py +2 -2
  31. {scratchattach/eventhandlers → eventhandlers}/_base.py +1 -0
  32. {scratchattach/eventhandlers → eventhandlers}/cloud_events.py +26 -6
  33. {scratchattach/eventhandlers → eventhandlers}/cloud_recorder.py +4 -4
  34. {scratchattach/eventhandlers → eventhandlers}/cloud_requests.py +139 -54
  35. {scratchattach/eventhandlers → eventhandlers}/cloud_server.py +6 -3
  36. {scratchattach/eventhandlers → eventhandlers}/cloud_storage.py +1 -2
  37. eventhandlers/filterbot.py +163 -0
  38. other/other_apis.py +598 -0
  39. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/METADATA +7 -11
  40. scratchattach-3.0.0b1.dist-info/RECORD +79 -0
  41. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/WHEEL +1 -1
  42. scratchattach-3.0.0b1.dist-info/entry_points.txt +2 -0
  43. scratchattach-3.0.0b1.dist-info/top_level.txt +7 -0
  44. {scratchattach/site → site}/_base.py +32 -5
  45. site/activity.py +426 -0
  46. {scratchattach/site → site}/alert.py +4 -5
  47. {scratchattach/site → site}/backpack_asset.py +2 -1
  48. {scratchattach/site → site}/classroom.py +80 -73
  49. {scratchattach/site → site}/cloud_activity.py +43 -29
  50. {scratchattach/site → site}/comment.py +86 -100
  51. {scratchattach/site → site}/forum.py +8 -4
  52. site/placeholder.py +132 -0
  53. {scratchattach/site → site}/project.py +228 -122
  54. {scratchattach/site → site}/session.py +156 -71
  55. {scratchattach/site → site}/studio.py +139 -46
  56. site/typed_dicts.py +151 -0
  57. {scratchattach/site → site}/user.py +511 -215
  58. {scratchattach/utils → utils}/commons.py +12 -4
  59. {scratchattach/utils → utils}/encoder.py +7 -4
  60. {scratchattach/utils → utils}/enums.py +1 -0
  61. {scratchattach/utils → utils}/exceptions.py +36 -2
  62. utils/optional_async.py +154 -0
  63. utils/requests.py +306 -0
  64. scratchattach/__init__.py +0 -29
  65. scratchattach/editor/commons.py +0 -273
  66. scratchattach/eventhandlers/filterbot.py +0 -161
  67. scratchattach/other/other_apis.py +0 -284
  68. scratchattach/site/activity.py +0 -382
  69. scratchattach/utils/requests.py +0 -93
  70. scratchattach-2.1.15b0.dist-info/RECORD +0 -66
  71. scratchattach-2.1.15b0.dist-info/top_level.txt +0 -1
  72. {scratchattach/cloud → cloud}/__init__.py +0 -0
  73. {scratchattach/editor → editor}/code_translation/__init__.py +0 -0
  74. {scratchattach/editor → editor}/code_translation/parse.py +0 -0
  75. {scratchattach/editor → editor}/comment.py +0 -0
  76. {scratchattach/editor → editor}/extension.py +0 -0
  77. {scratchattach/editor → editor}/twconfig.py +0 -0
  78. {scratchattach/eventhandlers → eventhandlers}/__init__.py +0 -0
  79. {scratchattach/eventhandlers → eventhandlers}/combine.py +0 -0
  80. {scratchattach/eventhandlers → eventhandlers}/message_events.py +0 -0
  81. {scratchattach/other → other}/__init__.py +0 -0
  82. {scratchattach/other → other}/project_json_capabilities.py +0 -0
  83. {scratchattach-2.1.15b0.dist-info → scratchattach-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
  84. {scratchattach/site → site}/__init__.py +0 -0
  85. {scratchattach/site → site}/browser_cookie3_stub.py +0 -0
  86. {scratchattach/site → site}/browser_cookies.py +0 -0
  87. {scratchattach/utils → utils}/__init__.py +0 -0
@@ -1,273 +0,0 @@
1
- """
2
- Shared functions used by the editor module
3
- """
4
- from __future__ import annotations
5
-
6
- import json
7
- import random
8
- import string
9
- from typing import Optional, Final, Any, TYPE_CHECKING, Union
10
- from enum import Enum, EnumMeta
11
-
12
- if TYPE_CHECKING:
13
- from . import sprite, build_defaulting
14
-
15
- SpriteInput = Union[sprite.Sprite, build_defaulting._SetSprite]
16
- else:
17
- SpriteInput = Any
18
-
19
- from scratchattach.utils import exceptions
20
-
21
- DIGITS: Final[tuple[str, ...]] = tuple("0123456789")
22
-
23
- ID_CHARS: Final[str] = string.ascii_letters + string.digits # + string.punctuation
24
-
25
-
26
- # Strangely enough, it seems like something in string.punctuation causes issues. Not sure why
27
-
28
-
29
- def _read_json_number(_str: str) -> float | int:
30
- ret = ''
31
-
32
- minus = _str[0] == '-'
33
- if minus:
34
- ret += '-'
35
- _str = _str[1:]
36
-
37
- def read_fraction(sub: str):
38
- sub_ret = ''
39
- if sub[0] == '.':
40
- sub_ret += '.'
41
- sub = sub[1:]
42
- while sub[0] in DIGITS:
43
- sub_ret += sub[0]
44
- sub = sub[1:]
45
-
46
- return sub_ret, sub
47
-
48
- def read_exponent(sub: str):
49
- sub_ret = ''
50
- if sub[0].lower() == 'e':
51
- sub_ret += sub[0]
52
- sub = sub[1:]
53
-
54
- if sub[0] in "-+":
55
- sub_ret += sub[0]
56
- sub = sub[1:]
57
-
58
- if sub[0] not in DIGITS:
59
- raise exceptions.UnclosedJSONError(f"Invalid exponent {sub}")
60
-
61
- while sub[0] in DIGITS:
62
- sub_ret += sub[0]
63
- sub = sub[1:]
64
-
65
- return sub_ret
66
-
67
- if _str[0] == '0':
68
- ret += '0'
69
- _str = _str[1:]
70
-
71
- elif _str[0] in DIGITS[1:9]:
72
- while _str[0] in DIGITS:
73
- ret += _str[0]
74
- _str = _str[1:]
75
-
76
- frac, _str = read_fraction(_str)
77
- ret += frac
78
-
79
- ret += read_exponent(_str)
80
-
81
- return json.loads(ret)
82
-
83
- # todo: consider if this should be moved to util.commons instead of editor.commons
84
- # note: this is currently unused code
85
- def consume_json(_str: str, i: int = 0) -> str | float | int | dict | list | bool | None:
86
- """
87
- *'gobble up some JSON until we hit something not quite so tasty'*
88
-
89
- Reads a JSON string and stops at the natural end (i.e. when brackets close, or when quotes end, etc.)
90
- """
91
- # Named by ChatGPT
92
- section = ''.join(_str[i:])
93
- if section.startswith("true"):
94
- return True
95
- elif section.startswith("false"):
96
- return False
97
- elif section.startswith("null"):
98
- return None
99
- elif section[0] in "0123456789.-":
100
- return _read_json_number(section)
101
-
102
- depth = 0
103
- json_text = ''
104
- out_string = True
105
-
106
- for char in section:
107
- json_text += char
108
-
109
- if char == '"':
110
- if len(json_text) > 1:
111
- unescaped = json_text[-2] != '\\'
112
- else:
113
- unescaped = True
114
- if unescaped:
115
- out_string ^= True
116
- if out_string:
117
- depth -= 1
118
- else:
119
- depth += 1
120
-
121
- if out_string:
122
- if char in "[{":
123
- depth += 1
124
- elif char in "}]":
125
- depth -= 1
126
-
127
- if depth == 0 and json_text.strip():
128
- return json.loads(json_text.strip())
129
-
130
- raise exceptions.UnclosedJSONError(f"Unclosed JSON string, read {json_text}")
131
-
132
-
133
- def is_partial_json(_str: str, i: int = 0) -> bool:
134
- try:
135
- consume_json(_str, i)
136
- return True
137
-
138
- except exceptions.UnclosedJSONError:
139
- return False
140
-
141
- except ValueError:
142
- return False
143
-
144
-
145
- def is_valid_json(_str: Any) -> bool:
146
- """
147
- Try to load a json string, if it fails, return False, else return true.
148
- """
149
- try:
150
- json.loads(_str)
151
- return True
152
- except (ValueError, TypeError):
153
- return False
154
-
155
-
156
- def noneless_update(obj: dict, update: dict) -> None:
157
- """
158
- equivalent to dict.update, except and values of None are not assigned
159
- """
160
- for key, value in update.items():
161
- if value is not None:
162
- obj[key] = value
163
-
164
-
165
- def remove_nones(obj: dict) -> None:
166
- """
167
- Removes all None values from a dict.
168
- :param obj: Dictionary to remove all None values.
169
- """
170
- nones = []
171
- for key, value in obj.items():
172
- if value is None:
173
- nones.append(key)
174
- for key in nones:
175
- del obj[key]
176
-
177
-
178
- def safe_get(lst: list | tuple, _i: int, default: Optional[Any] = None) -> Any:
179
- """
180
- Like dict.get() but for lists
181
- """
182
- if len(lst) <= _i:
183
- return default
184
- else:
185
- return lst[_i]
186
-
187
-
188
- def trim_final_nones(lst: list) -> list:
189
- """
190
- Removes the last None values from a list until a non-None value is hit.
191
- :param lst: list which will **not** be modified.
192
- """
193
- i = len(lst)
194
- for item in lst[::-1]:
195
- if item is not None:
196
- break
197
- i -= 1
198
- return lst[:i]
199
-
200
-
201
- def dumps_ifnn(obj: Any) -> Optional[str]:
202
- """
203
- Return json.dumps(obj) if the object is not None
204
- """
205
- if obj is None:
206
- return None
207
- else:
208
- return json.dumps(obj)
209
-
210
-
211
- def gen_id() -> str:
212
- """
213
- Generate an id for scratch blocks/variables/lists/broadcasts
214
-
215
- The old 'naïve' method but that chances of a repeat are so miniscule
216
- Have to check if whitespace chars break it
217
- May later add checking within sprites so that we don't need such long ids (we can save space this way)
218
- """
219
- return ''.join(random.choices(ID_CHARS, k=20))
220
-
221
-
222
- def sanitize_fn(filename: str):
223
- """
224
- Removes illegal chars from a filename
225
- :return: Sanitized filename
226
- """
227
- # Maybe could import a slugify module, but it's a bit overkill
228
- ret = ''
229
- for char in filename:
230
- if char in string.ascii_letters + string.digits + "-_":
231
- ret += char
232
- else:
233
- ret += '_'
234
- return ret
235
-
236
-
237
- def get_folder_name(name: str) -> str | None:
238
- """
239
- Get the name of the folder if this is a turbowarp-style costume name
240
- """
241
- if name.startswith('//'):
242
- return None
243
-
244
- if '//' in name:
245
- return name.split('//')[0]
246
- else:
247
- return None
248
-
249
-
250
- def get_name_nofldr(name: str) -> str:
251
- """
252
- Get the sprite/asset name without the folder name
253
- """
254
- fldr = get_folder_name(name)
255
- if fldr is None:
256
- return name
257
- else:
258
- return name[len(fldr) + 2:]
259
-
260
- # Parent enum class
261
- class SingletonMeta(EnumMeta):
262
-
263
- def __call__(self, value=0, *args, **kwds):
264
- if value != 0:
265
- raise ValueError("Value must be 0.")
266
- old_bases = self.__bases__
267
- self.__bases__ = old_bases + (Enum,)
268
- result = super().__call__(value, *args, **kwds)
269
- self.__bases__ = old_bases
270
- return result
271
-
272
- Singleton = Enum
273
-
@@ -1,161 +0,0 @@
1
- """FilterBot class"""
2
- from __future__ import annotations
3
-
4
- from .message_events import MessageEvents
5
- import time
6
-
7
- class HardFilter:
8
-
9
- def __init__(self, filter_name="UntitledFilter", *, equals=None, contains=None, author_name=None, project_id=None, profile=None, case_sensitive=False):
10
- self.equals=equals
11
- self.contains=contains
12
- self.author_name=author_name
13
- self.project_id=project_id
14
- self.profile=profile
15
- self.case_sensitive=case_sensitive
16
- self.filter_name = filter_name
17
-
18
- def apply(self, content, author_name, source_id):
19
- if not self.case_sensitive:
20
- content = content.lower()
21
- if self.equals is not None:
22
- if self.case_sensitive:
23
- if self.equals == content:
24
- return True
25
- else:
26
- if self.equals.lower() == content:
27
- return True
28
- if self.contains is not None:
29
- if self.case_sensitive:
30
- if self.contains.lower() in content:
31
- return True
32
- else:
33
- if self.contains in content:
34
- return True
35
- if self.author_name == author_name:
36
- return True
37
- if self.project_id == source_id or self.profile == source_id:
38
- return True
39
- return False
40
-
41
- class SoftFilter(HardFilter):
42
- def __init__(self, score:float, filter_name="UntitledFilter", *, equals=None, contains=None, author_name=None, project_id=None, profile=None, case_sensitive=False):
43
- self.score = score
44
- super().__init__(filter_name, equals=equals, contains=contains, author_name=author_name, project_id=project_id, profile=profile, case_sensitive=case_sensitive)
45
-
46
- class SpamFilter(HardFilter):
47
- def __init__(self, filter_name="UntitledFilter", *, equals=None, contains=None, author_name=None, project_id=None, profile=None, case_sensitive=False):
48
- self.memory = []
49
- super().__init__(filter_name, equals=equals, contains=contains, author_name=author_name, project_id=project_id, profile=profile, case_sensitive=case_sensitive)
50
-
51
- def apply(self, content, author_name, source_id):
52
- applies = super().apply(content, author_name, source_id)
53
- if not applies:
54
- return False
55
- self.memory.insert(0, {"content":content, "time":time.time()})
56
- print(content, self.memory)
57
- for comment in list(self.memory)[1:]:
58
- if comment["time"] < time.time() -300:
59
- self.memory.remove(comment)
60
- if comment["content"].lower() == content.lower():
61
- return True
62
- return False
63
-
64
- class Filterbot(MessageEvents):
65
-
66
- # The Filterbot class is built upon MessageEvents, similar to how CloudEvents is built upon CloudEvents
67
-
68
- def __init__(self, user, *, log_deletions=True):
69
- super().__init__(user)
70
- self.hard_filters = []
71
- self.soft_filters = []
72
- self.spam_filters = []
73
- self.log_deletions = log_deletions
74
- self.event(self.on_message, thread=False)
75
- self.update_interval = 2
76
-
77
- def add_filter(self, filter_obj):
78
- if isinstance(filter_obj, SoftFilter):
79
- self.soft_filters.append(filter_obj)
80
- elif isinstance(filter_obj, SpamFilter): # careful: SpamFilter is also HardFilter due to inheritence
81
- self.spam_filters.append(filter_obj)
82
- elif isinstance(filter_obj, HardFilter):
83
- self.hard_filters.append(filter_obj)
84
-
85
- def add_f4f_filter(self):
86
- self.add_filter(HardFilter("(f4f_filter) 'f4f'", contains="f4f"))
87
- self.add_filter(HardFilter("(f4f_filter) 'follow me'", contains="follow me"))
88
- self.add_filter(HardFilter("(f4f_filter) 'follow @'", contains="follow @"))
89
- self.add_filter(HardFilter("(f4f_filter) f 4 f'", contains="f 4 f"))
90
- self.add_filter(HardFilter("(f4f_filter) 'follow for'", contains="follow for"))
91
-
92
- def add_ads_filter(self):
93
- self.add_filter(SoftFilter(1, "(ads_filter) links", contains="scratch.mit.edu/projects/"))
94
- self.add_filter(SoftFilter(-1, "(ads_filter) feedback", contains="feedback"))
95
- self.add_filter(HardFilter("(ads_filter) 'check out my'", contains="check out my"))
96
- self.add_filter(HardFilter("(ads_filter) 'play my'", contains="play my"))
97
- self.add_filter(SoftFilter(1, "(ads_filter) 'advertis'", contains="advertis"))
98
-
99
- def add_spam_filter(self):
100
- self.add_filter(SpamFilter("(spam_filter)", contains=""))
101
-
102
- def add_genalpha_nonsense_filter(self):
103
- self.add_filter(HardFilter("(genalpha_nonsene_filter) 'skibidi'", contains="skibidi"))
104
- self.add_filter(HardFilter("[genalpha_nonsene_filter) 'rizzler'", contains="rizzler"))
105
- self.add_filter(HardFilter("(genalpha_nonsene_filter) 'fanum tax'", contains="fanum tax"))
106
-
107
- def on_message(self, message):
108
- if message.type == "addcomment":
109
- delete = False
110
- content = message.comment_fragment
111
-
112
- if message.comment_type == 0: # project comment
113
- source_id = message.comment_obj_id
114
- if self.user._session.connect_project(message.comment_obj_id).author_name != self.user.username:
115
- return # no permission to delete
116
- if message.comment_type == 1: # profile comment
117
- source_id = message.comment_obj_title
118
- if message.comment_obj_title != self.user.username:
119
- return # no permission to delete
120
- if message.comment_type == 2: # studio comment
121
- return # studio comments aren't handled
122
-
123
- # Apply hard filters
124
- for hard_filter in self.hard_filters:
125
- if hard_filter.apply(content, message.actor_username, source_id):
126
- delete=True
127
- if self.log_deletions:
128
- print(f"DETECTED: #{message.comment_id} violates hard filter: {hard_filter.filter_name}")
129
- break
130
-
131
- # Apply spam filters
132
- if delete is False:
133
- for spam_filter in self.spam_filters:
134
- if spam_filter.apply(content, message.actor_username, source_id):
135
- delete=True
136
- if self.log_deletions:
137
- print(f"DETECTED: #{message.comment_id} violates spam filter: {spam_filter.filter_name}")
138
- break
139
-
140
- # Apply soft filters
141
- if delete is False:
142
- score = 0
143
- violated_filers = []
144
- for soft_filter in self.soft_filters:
145
- if soft_filter.apply(content, message.actor_username, source_id):
146
- score += soft_filter.score
147
- violated_filers.append(soft_filter.name)
148
- if score >= 1:
149
- print(f"DETECTED: #{message.comment_id} violates too many soft filters: {violated_filers}")
150
- delete = True
151
-
152
- if delete is True:
153
- try:
154
- message.target().delete()
155
- if self.log_deletions:
156
- print(f"DELETED: #{message.comment_id} by f{message.actor_username}: '{content}'")
157
- except Exception as e:
158
- if self.log_deletions:
159
- print(f"DELETION FAILED: #{message.comment_id} by f{message.actor_username}: '{content}'")
160
-
161
-