blatherskite

a toy discord-like chat app backend written for a swe class
Log | Files | Refs | README

commit b58967f77356283b89595ca5ae197e45ab97a6bc
parent a40113661478296e0ae8e0d8dfde4aab2c2bf00b
Author: Papayapaya <freifeld.david@gmail.com>
Date:   Sat, 25 Jul 2026 15:25:48 -0700

Clean up formatting for readability

Diffstat:
MCargo.toml | 2+-
MREADME.md | 62++++++++++++++++++++++++++++++++------------------------------
Mtest.py | 150++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
3 files changed, 152 insertions(+), 62 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -2,5 +2,5 @@ members = [ "scuttlebutt", - "chatterbox" + "chatterbox" ] diff --git a/README.md b/README.md @@ -15,13 +15,10 @@ brew install cassandra-cpp-driver ``` > **Warning** -> This is a little wonky on M1 Macs: you'll need to follow the advice of -> [this page](https://stackoverflow.com/questions/69486339/nativelibrarydarwin-java64-failed-to-link-the-c-library-against-jna-native-m) -> when you face the inevitable JNA link error. Download for more recent JNA version is -> [here](https://search.maven.org/artifact/net.java.dev.jna/jna/5.8.0/jar). I personally ran -> `sudo mv jna-5.8.0.jar /opt/homebrew/Cellar/cassandra/4.0.6/libexec/jna-5.6.0.jar` (I -> think...) once I downloaded the new version, and that fixed it. - +> This is a little wonky on M1 Macs: you'll need to follow the advice +> of [this page](https://stackoverflow.com/q/69486339) when you face +> the inevitable JNA link error. Download for more recent JNA version is +> [here](https://search.maven.org/artifact/net.java.dev.jna/jna/5.8.0/jar). You also need to install the Rust language: ``` @@ -35,8 +32,8 @@ Start by launching the database in the background with cassandra -f ``` -Then, launch `chatterbox` - the service for sending/getting messages - and `scuttlebutt` - -the service for everything else. +Then, launch `chatterbox` - the service for sending/getting messages - and +`scuttlebutt` - the service for everything else. ``` cargo run -p chatterbox & @@ -51,36 +48,41 @@ Beyond basic text messaging, `blatherskite` has support for: - Basic permissioning (owner/admin/none) ### Terminology -Here's a quick guide to to the terms used by the service (that you might see in the -`scuttlebutt` documentation): +Here's a quick guide to to the terms used by the service (that you might see in +the `scuttlebutt` documentation): - Users can create or be invited to *groups* which contain *channels*. - Groups have: - *members*, the users who are part of the group - - an *owner*, who made the group and is permitted to do specific actions (like deleting it) - - *admin*, users who have elevated permissions for a group (like adding/removing channels) -- *DMs* are a special kind of group that are made between users directly and limit certain - functionality. DMs only have one channel and have no admin. -- Channels also have *members* (which can be a subset of the group!). Channels by default - are *public*, which means when a user is invited to a group they will be added to the - channel. You can set them to *private* with another API call. + - an *owner*, who made the group and is permitted to do specific + actions (like deleting it) + - *admin*, users who have elevated permissions for a group (like + adding/removing channels) +- *DMs* are a special kind of group that are made between users + directly and limit certain functionality. DMs only have one channel + and have no admin. +- Channels also have *members* (which can be a subset of the + group!). Channels by default are *public*, which means when a user + is invited to a group they will be added to the channel. You can set + them to *private* with another API call. ## Scuttlebutt -Scuttlebutt is an HTTP service that handles the creation, deletion, and updating of -groups/channels/users as well as misc other actions. +Scuttlebutt is an HTTP service that handles the creation, deletion, and updating +of groups/channels/users as well as misc other actions. -The various methods and objects are documented at `localhost:3000`, and the basic usage -flow is something like: -- `POST /api/user` to make a user, which will return a User object (see Schemas on the docs) -- `GET /api/login` to login with said user. This will return a JWT that you'll use to - authenticate future requests. This token will expire in a day! -- Whatever requests you'd like at that point! Authenticate by including a `ScuttleKey` - header with the token you got. +The various methods and objects are documented at `localhost:3000`, and the +basic usage flow is something like: +- `POST /api/user` to make a user, which will return a User object (see Schemas + on the docs) +- `GET /api/login` to login with said user. This will return a JWT that you'll + use to authenticate future requests. This token will expire in a day! +- Whatever requests you'd like at that point! Authenticate by including a + `ScuttleKey` header with the token you got. ## Chatterbox -Chatterbox is a websocket service used for sending and receiving messages. To use: +Chatterbox is a websocket service used for sending and receiving messages: - Connect to the websocket at `ws://localhost:3001/` -- Send authentication in the form of `{"hash": "YOUR_PASSWORD_HASH", "id": "YOUR_ID"}` +- Send authentication with `{"hash": "YOUR_PASSWORD_HASH", "id": "YOUR_ID"}` - Then use the websocket as normal! - - Send message requests in the form of `{"content": "whee", "channel": "CHANNEL_ID"}` + - Send message requests with `{"content": "whee", "channel": "CHANNEL_ID"}` - Recieve messages! diff --git a/test.py b/test.py @@ -9,51 +9,112 @@ async def main(): # INIT CODE # ############# - # same hash for everyone for succintness + # same hash for everyone for succinctness HASH = "6c6e2b0cfda80007e693d52b5956083ea68770e1310d0ed02d195cb14113b284" # login as quantum for init step - r = requests.post(f'http://localhost:3000/api/user?name=quantum&email=test@example.com', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/user?name=quantum&email=test@example.com', + data=HASH, + headers={"Content-Type": "text/plain"} + ) quantum = r.json()["id"] - r = requests.post(f'http://localhost:3000/api/login?id={quantum}', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/login?id={quantum}', + data=HASH, + headers={"Content-Type": "text/plain"} + ) tok = r.text # init other users - r = requests.post(f'http://localhost:3000/api/user?name=jemoka&email=test@example.com', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/user?name=jemoka&email=test@example.com', + data=HASH, + headers={"Content-Type": "text/plain"} + ) jemoka = r.json()["id"] - r = requests.post(f'http://localhost:3000/api/user?name=exr0n&email=test@example.com',data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/user?name=exr0n&email=test@example.com', + data=HASH, + headers={"Content-Type": "text/plain"} + ) exr0n = r.json()["id"] - r = requests.post(f'http://localhost:3000/api/user?name=enquirer&email=test@example.com', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/user?name=enquirer&email=test@example.com', + data=HASH, + headers={"Content-Type": "text/plain"} + ) enquirer = r.json()["id"] - r = requests.post(f'http://localhost:3000/api/user?name=zbuster&email=test@example.com', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/user?name=zbuster&email=test@example.com', + data=HASH, + headers={"Content-Type": "text/plain"} + ) zbuster = r.json()["id"] # create group with multiple members - r = requests.post(f'http://localhost:3000/api/group?name=testing', headers={"Authorization": tok}) + r = requests.post( + f'http://localhost:3000/api/group?name=testing', + headers={"Authorization": tok} + ) testing = r.json()["id"] - r = requests.put(f'http://localhost:3000/api/group/members?gid={testing}&uid={zbuster}', headers={"Authorization": tok}) - r = requests.put(f'http://localhost:3000/api/group/members?gid={testing}&uid={exr0n}', headers={"Authorization": tok}) - r = requests.put(f'http://localhost:3000/api/group/members?gid={testing}&uid={jemoka}', headers={"Authorization": tok}) - + r = requests.put( + f'http://localhost:3000/api/group/members?gid={testing}&uid={zbuster}', + headers={"Authorization": tok} + ) + r = requests.put( + f'http://localhost:3000/api/group/members?gid={testing}&uid={exr0n}', + headers={"Authorization": tok} + ) + r = requests.put( + f'http://localhost:3000/api/group/members?gid={testing}&uid={jemoka}', + headers={"Authorization": tok} + ) # create another two groups - r = requests.post(f'http://localhost:3000/api/group?name=whoo', headers={"Authorization": tok}) + r = requests.post( + f'http://localhost:3000/api/group?name=whoo', + headers={"Authorization": tok} + ) whoo = r.json()["id"] - r = requests.put(f'http://localhost:3000/api/group/members?gid={whoo}&uid={zbuster}', headers={"Authorization": tok}) - r = requests.put(f'http://localhost:3000/api/group/members?gid={whoo}&uid={enquirer}', headers={"Authorization": tok}) + r = requests.put( + f'http://localhost:3000/api/group/members?gid={whoo}&uid={zbuster}', + headers={"Authorization": tok} + ) + r = requests.put( + f'http://localhost:3000/api/group/members?gid={whoo}&uid={enquirer}', + headers={"Authorization": tok} + ) - r = requests.post(f'http://localhost:3000/api/group?name=whee', headers={"Authorization": tok}) + r = requests.post( + f'http://localhost:3000/api/group?name=whee', + headers={"Authorization": tok} + ) whee = r.json()["id"] - r = requests.put(f'http://localhost:3000/api/group/members?gid={whee}&uid={enquirer}', headers={"Authorization": tok}) - r = requests.put(f'http://localhost:3000/api/group/members?gid={whee}&uid={jemoka}', headers={"Authorization": tok}) + r = requests.put( + f'http://localhost:3000/api/group/members?gid={whee}&uid={enquirer}', + headers={"Authorization": tok} + ) + r = requests.put( + f'http://localhost:3000/api/group/members?gid={whee}&uid={jemoka}', + headers={"Authorization": tok} + ) ############################## # INTEGRATION TEST 1: GROUPS # ############################## # log two users in - r = requests.post(f'http://localhost:3000/api/login?id={zbuster}', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/login?id={zbuster}', + data=HASH, + headers={"Content-Type": "text/plain"} + ) z_tok = r.text - r = requests.post(f'http://localhost:3000/api/login?id={jemoka}', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/login?id={jemoka}', + data=HASH, + headers={"Content-Type": "text/plain"} + ) j_tok = r.text # init websockets for both @@ -63,21 +124,30 @@ async def main(): await j_sock.send(f'{{"hash": "{HASH}", "id": {jemoka}}}') # check zbuster's groups - r = requests.get(f'http://localhost:3000/api/user/groups', headers={"Authorization": z_tok}) + r = requests.get( + f'http://localhost:3000/api/user/groups', + headers={"Authorization": z_tok} + ) assert(len(r.json()) == 2) group_ids = list(map(lambda x: x["id"], r.json())) assert(testing in group_ids) assert(whoo in group_ids) # check jemoka's groups - r = requests.get(f'http://localhost:3000/api/user/groups', headers={"Authorization": j_tok}) + r = requests.get( + f'http://localhost:3000/api/user/groups', + headers={"Authorization": j_tok} + ) assert(len(r.json()) == 2) group_ids = list(map(lambda x: x["id"], r.json())) assert(testing in group_ids) assert(whee in group_ids) # get + check main channel of testing - r = requests.get(f'http://localhost:3000/api/group?id={testing}', headers={"Authorization": z_tok}) + r = requests.get( + f'http://localhost:3000/api/group?id={testing}', + headers={"Authorization": z_tok} + ) testing_main = r.json()["channels"][0] assert(r.json()["name"] == "testing") assert(r.json()["is_dm"] == False) @@ -100,11 +170,18 @@ async def main(): assert(j_msg["channel"] == testing_main) # log another user in - r = requests.post(f'http://localhost:3000/api/login?id={enquirer}', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/login?id={enquirer}', + data=HASH, + headers={"Content-Type": "text/plain"} + ) h_tok = r.text # attempt invalid action - r = requests.delete(f'http://localhost:3000/api/group?id={testing}', headers={"Authorization": h_tok}) + r = requests.delete( + f'http://localhost:3000/api/group?id={testing}', + headers={"Authorization": h_tok} + ) assert(r.status_code == 401) print("Test 1 done!") @@ -113,7 +190,11 @@ async def main(): ########################## # log yet another user in - r = requests.post(f'http://localhost:3000/api/login?id={exr0n}', data=HASH, headers={"Content-Type": "text/plain"}) + r = requests.post( + f'http://localhost:3000/api/login?id={exr0n}', + data=HASH, + headers={"Content-Type": "text/plain"} + ) e_tok = r.text # init websockets for both exr0n and enquirer @@ -123,7 +204,10 @@ async def main(): await h_sock.send(f'{{"hash": "{HASH}", "id": {enquirer}}}') # create dm - r = requests.post(f'http://localhost:3000/api/dm?uid={exr0n}', headers={"Authorization": h_tok}) + r = requests.post( + f'http://localhost:3000/api/dm?uid={exr0n}', + headers={"Authorization": h_tok} + ) dm = r.json()["id"] assert(r.json()["name"] == "") assert(r.json()["is_dm"] == True) @@ -135,12 +219,18 @@ async def main(): dm_main = r.json()["channels"][0] # check that exr0n can see it - r = requests.get(f'http://localhost:3000/api/user/dms', headers={"Authorization": e_tok}) + r = requests.get( + f'http://localhost:3000/api/user/dms', + headers={"Authorization": e_tok} + ) assert(len(r.json()) == 1) assert(dm == r.json()[0]["id"]) # and enquirer for that matter - r = requests.get(f'http://localhost:3000/api/user/dms', headers={"Authorization": h_tok}) + r = requests.get( + f'http://localhost:3000/api/user/dms', + headers={"Authorization": h_tok} + ) assert(len(r.json()) == 1) assert(dm == r.json()[0]["id"]) # test basic messaging @@ -152,8 +242,6 @@ async def main(): print("Test 2 done!") - loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait_for(main(), 5)) -# asyncio.run(main(), timeout=5)