Skip to content

cocorum.utils

This module provides various functions that stand by themselves and are used by a lot of different classes in the package. While most of these functions aren't usually meant to be used directly, you might find some of them handy. At least one function is not currently used by the rest of the package at all, but I figured it might be useful in the future.

Rumble API utilities

This submodule provides some utilities for working with the APIs S.D.G.

MD5Ex

MD5 extended hashing utilities

Source code in cocorum/utils.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class MD5Ex:
    """MD5 extended hashing utilities"""

    @staticmethod
    def hash(message: str) -> str:
        """Hash a string.

    Args:
        message (str): Message to hash.

    Returns:
        Hash (str): The hex digest hash result.
        """

        #Actually, we can except bytes, but if we got string, encode the string
        if isinstance(message, str):
            message = message.encode(static.Misc.text_encoding)

        return hashlib.md5(message).hexdigest()

    @staticmethod
    def hash_stretch(password: str, salt: str, iterations: int = 1024) -> str:
        """Stretch-hash a password with a salt.

    Args:
        password (str): The password to hash.
        salt (str): The salt to add to the password.
        iterations (int): Number of times to stretch the hashing.

    Returns:
        Hash (str): The completed stretched hash.
        """

        #Start with the salt and password together
        message = (salt + password).encode(static.Misc.text_encoding)

        #Make one hash of it
        current = MD5Ex.hash(message)

        #Then keep re-adding the password and re-hashing
        for _ in range(iterations):
            current = MD5Ex.hash(current + password)

        return current

hash(message) staticmethod

Hash a string.

Parameters:

Name Type Description Default
message str

Message to hash.

required

Returns:

Name Type Description
Hash str

The hex digest hash result.

Source code in cocorum/utils.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@staticmethod
def hash(message: str) -> str:
    """Hash a string.

Args:
    message (str): Message to hash.

Returns:
    Hash (str): The hex digest hash result.
    """

    #Actually, we can except bytes, but if we got string, encode the string
    if isinstance(message, str):
        message = message.encode(static.Misc.text_encoding)

    return hashlib.md5(message).hexdigest()

hash_stretch(password, salt, iterations=1024) staticmethod

Stretch-hash a password with a salt.

Parameters:

Name Type Description Default
password str

The password to hash.

required
salt str

The salt to add to the password.

required
iterations int

Number of times to stretch the hashing.

1024

Returns:

Name Type Description
Hash str

The completed stretched hash.

Source code in cocorum/utils.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@staticmethod
def hash_stretch(password: str, salt: str, iterations: int = 1024) -> str:
    """Stretch-hash a password with a salt.

Args:
    password (str): The password to hash.
    salt (str): The salt to add to the password.
    iterations (int): Number of times to stretch the hashing.

Returns:
    Hash (str): The completed stretched hash.
    """

    #Start with the salt and password together
    message = (salt + password).encode(static.Misc.text_encoding)

    #Make one hash of it
    current = MD5Ex.hash(message)

    #Then keep re-adding the password and re-hashing
    for _ in range(iterations):
        current = MD5Ex.hash(current + password)

    return current

badges_to_glyph_string(badges)

Convert a list of badges into a string of glyphs.

Parameters:

Name Type Description Default
badges list

A list of str or objects with str method that are valid badge slugs.

required

Returns:

Name Type Description
Glyphs str

The badge list as a UTF-8 glyph string, uses ? for unknown badges.

Source code in cocorum/utils.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def badges_to_glyph_string(badges) -> str:
    """Convert a list of badges into a string of glyphs.

    Args:
        badges (list): A list of str or objects with __str__ method that are valid badge slugs.

    Returns:
        Glyphs (str): The badge list as a UTF-8 glyph string, uses ? for unknown badges.
        """

    out = ""
    for badge in badges:
        badge = str(badge)
        if badge in static.Misc.badges_as_glyphs:
            out += static.Misc.badges_as_glyphs[badge]
        else:
            out += "?"
    return out

base_10_to_36(b10)

Convert a base 10 number to base 36.

Parameters:

Name Type Description Default
b10 int

The base 10 number.

required

Returns:

Name Type Description
B36 str

The same number in base 36.

Source code in cocorum/utils.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def base_10_to_36(b10) -> str:
    """Convert a base 10 number to base 36.

    Args:
        b10 (int): The base 10 number.

    Returns:
        B36 (str): The same number in base 36.
        """

    b10 = int(b10)
    b36 = ""
    base_len = len(static.Misc.base36)
    while b10:
        b36 = static.Misc.base36[b10 % base_len] + b36
        b10 //= base_len

    return b36

base_36_and_10(num, assume_10=False)

Take a base 36 or base 10 number, and return both base 36 and 10.

Parameters:

Name Type Description Default
num (int, str)

The number in either base 10 or 36.

required
assume_10 bool

If the number is a string but looks like base 10, should we assume it is? Defaults to False.

False

Returns:

Name Type Description
B36 str

The number in base 36.

B10 int

The number in base 10.

Source code in cocorum/utils.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def base_36_and_10(num, assume_10 = False):
    """Take a base 36 or base 10 number, and return both base 36 and 10.

    Args:
        num (int, str): The number in either base 10 or 36.
        assume_10 (bool): If the number is a string but looks like base 10, should we assume it is?
            Defaults to False.

    Returns:
        B36 (str): The number in base 36.
        B10 (int): The number in base 10.
        """

    return ensure_b36(num, assume_10), ensure_b10(num, assume_10)

base_36_to_10(b36)

Convert a base 36 number to base 10.

Parameters:

Name Type Description Default
b36 str

The base 36 number.

required

Returns:

Name Type Description
B10 int

The same number in base 10.

Source code in cocorum/utils.py
104
105
106
107
108
109
110
111
112
113
114
def base_36_to_10(b36) -> int:
    """Convert a base 36 number to base 10.

    Args:
        b36 (str): The base 36 number.

    Returns:
        B10 (int): The same number in base 10.
        """

    return int(str(b36), 36)

calc_password_hashes(password, salts)

Hash a password for Rumble authentication.

Parameters:

Name Type Description Default
password str

The password to hash.

required
salts iter

The three salts to use for hashing.

required

Returns:

Name Type Description
Hashes iter

The three results of hashing.

Source code in cocorum/utils.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def calc_password_hashes(password: str, salts):
    """Hash a password for Rumble authentication.

    Args:
        password (str): The password to hash.
        salts (iter): The three salts to use for hashing.

    Returns:
        Hashes (iter): The three results of hashing.
        """

    #Stretch-hash the password with the first salt
    stretched1 = MD5Ex.hash_stretch(password, salts[0], 128)

    #Add the second salt to that result, and hash one more time
    final_hash1 = MD5Ex.hash(stretched1 + salts[1])

    #Stretch-hash the password with the third salt
    stretched2 = MD5Ex.hash_stretch(password, salts[2], 128)

    return final_hash1, stretched2, salts[1]

ensure_b10(num, assume_10=False)

No matter wether a number is base 36 or 10, return 10.

Parameters:

Name Type Description Default
num (int, str)

The number in either base 10 or 36.

required
assume_10 bool

If the number is a string but looks like base 10, should we assume it is? Defaults to False.

False

Returns:

Name Type Description
Number int

The number in base 10.

Source code in cocorum/utils.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def ensure_b10(num, assume_10 = False) -> int:
    """No matter wether a number is base 36 or 10, return 10.

    Args:
        num (int, str): The number in either base 10 or 36.
        assume_10 (bool): If the number is a string but looks like base 10, should we assume it is?
            Defaults to False.

    Returns:
        Number (int): The number in base 10.
        """

    #It is base 10 or has an integer conversion method
    if isinstance(num, int) or hasattr(num, "__int__"):
        return int(num)

    #It is a string or has a string conversion attribute
    if isinstance(num, str) or hasattr(num, "__str__"):
        num = str(num)

        #The string number is in base 10
        if num.isnumeric() and assume_10:
            return base_10_to_36(num), int(num)

    #It is base 36:
    return base_36_to_10(num)

ensure_b36(num, assume_10=False)

No matter wether a number is base 36 or 10, return 36.

Parameters:

Name Type Description Default
num (int, str)

The number in either base 10 or 36.

required
assume_10 bool

If the number is a string but looks like base 10, should we assume it is? Defaults to False.

False

Returns:

Name Type Description
Number str

The number in base 36.

Source code in cocorum/utils.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def ensure_b36(num, assume_10 = False) -> str:
    """No matter wether a number is base 36 or 10, return 36.

    Args:
        num (int, str): The number in either base 10 or 36.
        assume_10 (bool): If the number is a string but looks like base 10, should we assume it is?
            Defaults to False.

    Returns:
        Number (str): The number in base 36.
        """

    #It is base 10
    if isinstance(num, int) or hasattr(num, "__int__"):
        return base_10_to_36(int(num))

    #It is a string or has a string conversion attribute
    if isinstance(num, str) or hasattr(num, "__str__"):
        num = str(num)

        #The string number is in base 10
        if num.isnumeric() and assume_10:
            return base_10_to_36(num)

    #It is base 36:
    return num

form_timestamp(seconds, suffix='+00:00')

Form a Rumble timestamp.

Parameters:

Name Type Description Default
seconds float

Timestamp in seconds since Epoch, UTC.

required

Returns:

Name Type Description
Timestamp str

The same timestamp value, in Rumble's API format.

Source code in cocorum/utils.py
73
74
75
76
77
78
79
80
81
82
83
def form_timestamp(seconds: float, suffix = "+00:00") -> str:
    """Form a Rumble timestamp.

    Args:
        seconds (float): Timestamp in seconds since Epoch, UTC.

    Returns:
        Timestamp (str): The same timestamp value, in Rumble's API format.
        """

    return time.strftime(static.Misc.timestamp_format, time.gmtime(seconds)) + suffix

generate_request_id()

Generate a UUID for API requests

Returns:

Name Type Description
UUID str

Random base64 encoded UUID.

Source code in cocorum/utils.py
226
227
228
229
230
231
232
233
234
235
def generate_request_id() -> str:
    """Generate a UUID for API requests

    Returns:
        UUID (str): Random base64 encoded UUID.
        """

    random_uuid = uuid.uuid4().bytes + uuid.uuid4().bytes
    b64_encoded = base64.b64encode(random_uuid).decode(static.Misc.text_encoding)
    return b64_encoded.rstrip('=')[:43]

options_check(url, method, origin=static.URI.rumble_base, cookies={}, params={})

Check of we are allowed to do method on url via an options request

Parameters:

Name Type Description Default
url str

The URL to check at.

required
method str

The HTTP method to check permission for.

required
origin str

The origin header of the options request. Defaults to static.URI.rumble_base

rumble_base
cookies dict

Cookie dict to use in the request. Defaults to no cookies.

{}
params dict

Parameters to use in the request. Defaults to no parameters.

{}

Returns:

Name Type Description
Result bool

Is the HTTP method allowed at the URL?

Source code in cocorum/utils.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def options_check(url: str, method: str, origin = static.URI.rumble_base, cookies: dict = {}, params: dict = {}) -> bool:
    """Check of we are allowed to do method on url via an options request

    Args:
        url (str): The URL to check at.
        method (str): The HTTP method to check permission for.
        origin (str): The origin header of the options request.
            Defaults to static.URI.rumble_base
        cookies (dict): Cookie dict to use in the request.
            Defaults to no cookies.
        params (dict): Parameters to use in the request.
            Defaults to no parameters.

    Returns:
        Result (bool): Is the HTTP method allowed at the URL?
        """

    r = requests.options(
        url,
        headers = {
            'Access-Control-Request-Method' : method.upper(),
            'Access-Control-Request-Headers' : 'content-type',
            'Origin' : origin,
            },
        cookies = cookies,
        params = params,
        timeout = static.Delays.request_timeout,
        )
    return r.status_code == 200

parse_timestamp(timestamp)

Parse a Rumble timestamp.

Parameters:

Name Type Description Default
timestamp str

Timestamp in Rumble's API format.

required

Returns:

Name Type Description
Timestamp float

The same timestamp value, in seconds since Epoch, UTC.

Source code in cocorum/utils.py
60
61
62
63
64
65
66
67
68
69
70
71
def parse_timestamp(timestamp: str) -> float:
    """Parse a Rumble timestamp.

    Args:
        timestamp (str): Timestamp in Rumble's API format.

    Returns:
        Timestamp (float): The same timestamp value, in seconds since Epoch, UTC.
        """

    #Trims off the 6 TODO characters at the end
    return calendar.timegm(time.strptime(timestamp[:-6], static.Misc.timestamp_format))

Test if a session cookie dict is valid.

Parameters:

Name Type Description Default
session_cookie dict

The session cookie dict to test.

required

Returns:

Name Type Description
Result bool

Is the cookie dict valid?

Source code in cocorum/utils.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def test_session_cookie(session_cookie: dict) -> bool:
    """Test if a session cookie dict is valid.

    Args:
        session_cookie (dict): The session cookie dict to test.

    Returns:
        Result (bool): Is the cookie dict valid?
        """

    r = requests.get(static.URI.login_test,
            cookies = session_cookie,
            headers = static.RequestHeaders.user_agent,
            timeout = static.Delays.request_timeout,
        )

    assert r.status_code == 200, f"Testing session token failed: {r}"

    title = r.text.split("<title>")[1].split("</title>")[0]

    #If the session token is invalid, it won't log us in and "Login" will still be shown
    return "Login" not in title

S.D.G.