diff --git a/.gitignore b/.gitignore index c44fc3a..701e9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ .history __pycache__ *.pyc +examples/crawler/devrant.sqlite-shm +examples/crawler/devrant.sqlite-wal +examples/crawler/devrant.sqlite +examples/crawler/.venv +examples/crawler/__pycache__ diff --git a/README.md b/README.md index ea6d610..e2a856b 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,38 @@ # devRanta - devRanta is the best async devRant client written in Python. Authentication is only needed for half of the functionality; thus, the username and password are optional parameters when constructing the main class of this package (Api). You can find the latest packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages). - ## Running ``` make run ``` - ## Testing Tests are only made for methods not requireing authentication. I do not see value in mocking requests. ``` make test ``` - ## How to use Implementation: ``` -from devranta.api import Api - +from devranta.api import Api api = Api(username="optional!", password="optional!") - async def list_rants(): async for rant in api.get_rants(): print(rant["user_username"], ":", rant["text"]) ``` -See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use. - - +See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use. # devRant API Documentation - For people wanting to build their own client. - TODO: document responses. - ## Base URL `https://devrant.com/api` - ## Authentication - Uses `dr_token` cookie with `token_id`, `token_key`, and `user_id`. - Required for endpoints needing user authentication. - `guid`, `plat`, `sid`, `seid` included in requests for session tracking. - ## Endpoints - ### User Management -1. **Register User** - - **URL**: `/api/users` - - **Method**: POST - - **Parameters**: - - `app`: 3 (constant) - - `type`: 1 (constant) - - `email`: User email - - `username`: User username - - `password`: User password - - `guid`: Unique identifier (from `getMyGuid`) - - `plat`: 3 (constant) - - `sid`: Session start time (from `getSessionStartTime`) - - `seid`: Session event ID (from `getSessionEventId`) - - **Response**: JSON with `success`, `auth_token`, or `error` and `error_field` - - **Description**: Creates a new user account. - +1. **Registering user** + - Ommitted, you know why. 2. **Login User** - **URL**: `/api/users/auth-token` - **Method**: POST @@ -73,8 +45,26 @@ TODO: document responses. - `sid`: Session start time - `seid`: Session event ID - **Response**: JSON with `success`, `auth_token`, or `error` + - **Success Example**: + ```json + { + "success": true, + "auth_token": { + "id": 18966518, + "key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8", + "expire_time": 1756765587, + "user_id": 18959632 + } + } + ``` + - **Error Example**: + ```json + { + "success": false, + "error": "Invalid login credentials entered. Please try again." + } + ``` - **Description**: Authenticates user and returns auth token. - 3. **Edit Profile** - **URL**: `/api/users/me/edit-profile` - **Method**: POST @@ -90,8 +80,13 @@ TODO: document responses. - `profile_website`: User website - `profile_github`: GitHub username - **Response**: JSON with `success` + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Updates user profile information. - 4. **Forgot Password** - **URL**: `/api/users/forgot-password` - **Method**: POST @@ -100,8 +95,13 @@ TODO: document responses. - `username`: User username - `guid`, `plat`, `sid`, `seid` - **Response**: JSON with `success` + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Initiates password reset process. - 5. **Resend Confirmation Email** - **URL**: `/api/users/me/resend-confirm` - **Method**: POST @@ -109,8 +109,13 @@ TODO: document responses. - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - **Response**: JSON with `success` + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Resends account confirmation email. - 6. **Delete Account** - **URL**: `/api/users/me` - **Method**: DELETE @@ -119,7 +124,6 @@ TODO: document responses. - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - **Response**: JSON with `success` - **Description**: Deletes user account. - 7. **Mark News as Read** - **URL**: `/api/users/me/mark-news-read` - **Method**: POST @@ -128,8 +132,13 @@ TODO: document responses. - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `news_id`: News item ID - **Response**: JSON with `success` + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Marks a news item as read for logged-in users. - ### Rants 1. **Get Rant** - **URL**: `/api/devrant/rants/{rant_id}` @@ -139,9 +148,45 @@ TODO: document responses. - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `last_comment_id`: 999999999999 (optional) - `links`: 0 (optional) - - **Response**: JSON with `rant` (text, tags) + - **Response**: JSON with `rant` (text, tags), `comments`, `success`, `subscribed` + - **Success Example**: + ```json + { + "rant": { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 3, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 10, + "tags": [ + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": 0, + "edited": false, + "link": "rants/18960811/you-know-im-getting-tired-of-this-hr-speak-in-job-applications-specifically-this", + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + }, + "comments": [], + "success": true, + "subscribed": 0 + } + ``` - **Description**: Retrieves a specific rant by ID. - 2. **Post Rant** - **URL**: `/api/devrant/rants` - **Method**: POST @@ -153,8 +198,14 @@ TODO: document responses. - `type`: Rant type ID - `image`: Optional image file (img/gif) - **Response**: JSON with `success`, `rant_id`, or `error` + - **Error Example**: + ```json + { + "success": false, + "error": "It looks like you just posted this same rant! Your connection might have timed out while posting so you might have seen an error, but sometimes the rant still gets posted and in this case it seems it did, so please check :) If this was not the case please contact info@devrant.io. Thanks!" + } + ``` - **Description**: Creates a new rant. - 3. **Edit Rant** - **URL**: `/api/devrant/rants/{rant_id}` - **Method**: POST @@ -165,17 +216,29 @@ TODO: document responses. - `token_id`, `token_key`, `user_id` - `image`: Optional image file - **Response**: JSON with `success` or `fail_reason` + - **Error Example**: + ```json + { + "success": false, + "fail_reason": "" + } + ``` - **Description**: Updates an existing rant. - 4. **Delete Rant** - **URL**: `/api/devrant/rants/{rant_id}` - **Method**: DELETE - **Parameters**: - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - - **Response**: JSON with `success` + - **Response**: JSON with `success` or `error` + - **Error Example**: + ```json + { + "success": false, + "error": "An unknown error occurred and this rant can't be deleted. Please contact support@devrant.com for help with this." + } + ``` - **Description**: Deletes a rant. - 5. **Vote on Rant** - **URL**: `/api/devrant/rants/{rant_id}/vote` - **Method**: POST @@ -184,9 +247,43 @@ TODO: document responses. - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `vote`: 1 (upvote), -1 (downvote), 0 (remove vote) - `reason`: Downvote reason ID (required for downvote) - - **Response**: JSON with `success` or `confirmed` (false if unverified) + - **Response**: JSON with `success`, `rant`, or `confirmed` (false if unverified) + - **Success Example**: + ```json + { + "success": true, + "rant": { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 3, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 10, + "tags": [ + "rant", + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": -1, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4180, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + } + } + ``` - **Description**: Votes on a rant. - 6. **Favorite/Unfavorite Rant** - **URL**: `/api/devrant/rants/{rant_id}/{favorite|unfavorite}` - **Method**: POST @@ -194,8 +291,13 @@ TODO: document responses. - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - **Response**: JSON with `success` + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Favorites or unfavorites a rant. - 7. **Get Rant Feed** - **URL**: `/api/devrant/rants` - **Method**: GET @@ -203,9 +305,66 @@ TODO: document responses. - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `ids`: JSON string of IDs (optional) - - **Response**: JSON with `success`, `num_notifs` + - **Response**: JSON with `success`, `rants`, `settings`, `set`, `wrw`, `dpp`, `num_notifs`, `unread`, `news` + - **Success Example**: + ```json + { + "success": true, + "rants": [ + { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 3, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 9, + "tags": [ + "rant", + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + } + // ... more rants + ], + "settings": { + "notif_state": -1, + "notif_token": "" + }, + "set": "688e90b7cf77f", + "wrw": 385, + "dpp": 0, + "num_notifs": 0, + "unread": { + "total": 0 + }, + "news": { + "id": 356, + "type": "intlink", + "headline": "Weekly Group Rant", + "body": "Tips for staying productive?", + "footer": "Add tag 'wk247' to your rant", + "height": 100, + "action": "grouprant" + } + } + ``` - **Description**: Retrieves rant feed with notification count. - ### Comments 1. **Get Comment** - **URL**: `/api/comments/{comment_id}` @@ -214,9 +373,15 @@ TODO: document responses. - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `links`: 0 (optional) - - **Response**: JSON with `comment` (body) + - **Response**: JSON with `comment` (body), or `error` + - **Error Example**: + ```json + { + "success": false, + "error": "Invalid comment specified in path." + } + ``` - **Description**: Retrieves a specific comment by ID. - 2. **Post Comment** - **URL**: `/api/devrant/rants/{rant_id}/comments` - **Method**: POST @@ -226,8 +391,13 @@ TODO: document responses. - `token_id`, `token_key`, `user_id` - `image`: Optional image file (img/gif) - **Response**: JSON with `success` or `confirmed` (false if unverified) + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Posts a comment on a rant. - 3. **Edit Comment** - **URL**: `/api/comments/{comment_id}` - **Method**: POST @@ -236,17 +406,29 @@ TODO: document responses. - `comment`: Comment text - `token_id`, `token_key`, `user_id` - **Response**: JSON with `success` or `fail_reason` + - **Error Example**: + ```json + { + "success": false, + "error": "Invalid comment specified in path." + } + ``` - **Description**: Updates an existing comment. - 4. **Delete Comment** - **URL**: `/api/comments/{comment_id}` - **Method**: DELETE - **Parameters**: - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - - **Response**: JSON with `success` + - **Response**: JSON with `success` or `error` + - **Error Example**: + ```json + { + "success": false, + "error": "Invalid comment specified in path." + } + ``` - **Description**: Deletes a comment. - 5. **Vote on Comment** - **URL**: `/api/comments/{comment_id}/vote` - **Method**: POST @@ -255,9 +437,15 @@ TODO: document responses. - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `vote`: 1 (upvote), -1 (downvote), 0 (remove vote) - `reason`: Downvote reason ID (required for downvote) - - **Response**: JSON with `success` or `confirmed` (false if unverified) + - **Response**: JSON with `success` or `error` + - **Error Example**: + ```json + { + "success": false, + "error": "Invalid comment specified in path." + } + ``` - **Description**: Votes on a comment. - ### Notifications 1. **Get Notification Feed** - **URL**: `/api/users/me/notif-feed` @@ -267,9 +455,28 @@ TODO: document responses. - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - `ext_prof`: 1 (optional) - `last_time`: Last notification check time - - **Response**: JSON with `success`, `data` (items, check_time, username_map) + - **Response**: JSON with `success`, `data` (items, check_time, username_map, unread, num_unread) + - **Success Example**: + ```json + { + "success": true, + "data": { + "items": [], + "check_time": 1754173634, + "username_map": [], + "unread": { + "all": 0, + "upvotes": 0, + "mentions": 0, + "comments": 0, + "subs": 0, + "total": 0 + }, + "num_unread": 0 + } + } + ``` - **Description**: Retrieves user notifications. - 2. **Clear Notifications** - **URL**: `/api/users/me/notif-feed` - **Method**: DELETE @@ -277,9 +484,14 @@ TODO: document responses. - `app`: 3 - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` - **Response**: JSON with `success` + - **Success Example**: + ```json + { + "success": true + } + ``` - **Description**: Clears user notifications. - -## External API +### External API - **Beta List Signup** - **URL**: `https://www.hexicallabs.com/api/beta-list` - **Method**: GET (JSONP) @@ -287,8 +499,14 @@ TODO: document responses. - `email`: User email - `platform`: Platform name - `app`: 3 + - **Response**: JSON with `error` + - **Error Example**: + ```json + { + "error": "Expecting value: line 1 column 1 (char 0)" + } + ``` - **Description**: Signs up user for beta list (external service). - ## Notes - All endpoints expect `app=3` for identification. - Authenticated endpoints require `dr_token` cookie with `token_id`, `token_key`, `user_id`. @@ -297,5 +515,3 @@ TODO: document responses. - Downvotes require a reason ID, prompting a modal if not provided. - Responses typically include `success` boolean; errors include `error` or `fail_reason`. - Cookies (`dr_token`, `dr_guid`, `dr_session_start`, `dr_event_id`, `dr_feed_sort`, `dr_theme`, `dr_rants_viewed`, `dr_stickers_seen`, `rant_type_filters`, `news_seen`) manage state and preferences. - - diff --git a/api_test_results.json b/api_test_results.json new file mode 100644 index 0000000..b6fecd8 --- /dev/null +++ b/api_test_results.json @@ -0,0 +1,2053 @@ +[ + { + "url": "https://devrant.com/api/users/auth-token", + "method": "POST", + "status_code": 200, + "response": { + "success": true, + "auth_token": { + "id": 18966518, + "key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8", + "expire_time": 1756765587, + "user_id": 18959632 + } + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:27 GMT", + "Content-Type": "application/json", + "Content-Length": "138", + "Connection": "keep-alive" + }, + "request_body": { + "username": "power-to@the-puff.com", + "password": "powerpuffgirl", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3 + }, + "timestamp": "2025-08-03T00:26:28.055348" + }, + { + "url": "https://devrant.com/api/devrant/rants", + "method": "POST", + "status_code": 200, + "response": { + "success": false, + "error": "It looks like you just posted this same rant! Your connection might have timed out while posting so you might have seen an error, but sometimes the rant still gets posted and in this case it seems it did, so please check :) If this was not the case please contact info@devrant.io. Thanks!" + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:29 GMT", + "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Vary": "Accept-Encoding", + "Content-Encoding": "gzip" + }, + "request_body": { + "rant": "Test rant for API testing (ignore)", + "tags": "test,api", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:29.902375" + }, + { + "url": "https://devrant.com/api/devrant/rants?plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "GET", + "status_code": 200, + "response": { + "success": true, + "rants": [ + { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 3, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 9, + "tags": [ + "rant", + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + }, + { + "id": 18961721, + "text": "You are considered a junior developer when you need 'a lot of hand-holding'. Now, I can figure out most things on my own, but how do you distinguish between 'needs too much hand-holding and therefore is a junior' and 'does ok and therefore is a medior'? \n\nFor example, I can do well on basic things and getting projects set up, but then I might need more help if errors truly become too cryptic or difficult to solve and I haven't found solutions. A few examples here:\r\n- having messed up the git branches and releases so much that you need someone with a deep knowledge and troubleshooting of git to set the situation straight\r\n- spending a week on trying to figure out why Azure doesn't want to successfully build your super custom build and it takes ages to figure it out because it requires in-depth Docker, linux knowledge and stellar, MIT-level troubleshooting and analytical skills\n\nAnd so, someone who needs help with these is considered a junior?\n\nHow do you really identify a junior? Seems vague.", + "score": 3, + "created_time": 1754082038, + "attached_image": "", + "num_comments": 9, + "tags": [ + "question", + "proficiency", + "junior-vs-medior", + "job-hunting" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 4, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + }, + { + "id": 18961977, + "text": "So the gaming industry was having huge profits (2024?), then they decided to layoff a bunch of people for \"reasons\", which displaced a bunch of people in the industry, who then became indies and are making good money, and people working for studios are salty about something they caused?\n\nhttps://reddit.com/r/gaming/...\n\nHow cancerous do you have to be in your industry to be pissed at people trying to recover from your cancer?\n\n\"deprofessionalization\" is this corporate speak for \"doesn't have a cushy middle management job at EA\"? I tried looking up the meaning and came away even more confused. You aren't a real gaming dev if you don't work for a huge studio?\n\nWeird way to de-legitimize people who had to look for work elsewhere. The people who left or got laid off are escaping a shitscape for sure.", + "score": 4, + "created_time": 1754086762, + "attached_image": "", + "num_comments": 3, + "tags": [ + "random", + "wtf", + "studios are stupid", + "your mom" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "links": [ + { + "type": "url", + "url": "https://www.reddit.com/r/gaming/comments/1kskew5/the_deprofessionalization_of_video_games_was_on/", + "short_url": "https://reddit.com/r/gaming/...", + "title": "https://reddit.com/r/gaming/...", + "start": 289, + "end": 320, + "special": 1 + } + ], + "special": true, + "user_id": 18170221, + "user_username": "YourMom", + "user_score": 535, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18966144, + "text": "me is make search:\r\nc fun strsub name was?\n\naI oVeRQuEEfvIew:\r\ntHeRE iZ nO fUnKCHon iN ceE foR gEtTen sUb sTrEnggGzEn\n\nno, that is lie! you are liar!\r\nme is thumb you down. bad gugul.\r\nme is make search again now.\r\noh wait memory return.\r\nstrstr.\r\nright.\r\nthat the name silly me.\r\ni be alias fun to strsub because more mnemonic.\n\nbut why is gugul degenerating search don't know.\r\nruining experience not OK.\n\nplease someone tell mister pichai is make search bad thanks.\r\ni want good search back, can switch to duck?\r\nopen duck.\r\noh noes.\r\nduck is degenerate now.\r\nhas duck.ai on frontpage.\n\nwhyyyyyyyyyyy me is make tears now.\r\nneed good search not gorgabe.", + "score": 2, + "created_time": 1754166024, + "attached_image": "", + "num_comments": 1, + "tags": [ + "random", + "the gorgabe be real" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "user_id": 10429171, + "user_username": "Liebranca", + "user_score": 1116, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-m_9-1_1-9_16-6_3-3_8-1_7-1_5-1_12-9_6-26_2-1_22-2_15-10_11-3_19-1_4-1_20-12.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-m_9-1_1-9_16-6_3-3_8-1_7-1_5-1_12-9_6-26_2-1_22-2_15-10_11-3_19-1_4-1_20-12.png" + } + }, + { + "id": 18963683, + "text": "Got fed up with my newcomer PO's bullshit planning and constant gaslighting whenever deadlines get missed. \n\nMade a spreadsheet showing our actual capacity vs his unrealistic expectations - turns out all of Q3 work is spilling into Q4, so I'll be spending October finishing July/August/September tasks. \n\nBrought this up multiple times asking for more time or resources. Got denied and blamed instead. \n\nIt got really bad to the point where I had to start doing his work for him - make business decisions, create tickets, define acceptance criteria, requirements and etc. Just so I would have some paper trail so that I wouldnt get blamed again in case I'm missing some undefined requirement. Finally couldn't take it anymore and escalated to my dev lead.\n\nNow the PO is getting absolutely chewed out. Feels fucking good to finally see him face consequences for their terrible planning.\n\nOn another note - is it a disease or do the most useless people just naturally gravitate toward management positions? For example, this guy is supposed to be a decision maker but answers almost every business question vaguely, ambiguously, or just straight up ignores it. How the fuck such people even exist, it just blows my mind.", + "score": 3, + "created_time": 1754120321, + "attached_image": "", + "num_comments": 7, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 6029955, + "user_username": "topsecret230", + "user_score": 787, + "user_avatar": { + "b": "f99a66", + "i": "v-37_c-3_b-3_g-m_9-1_1-2_16-3_3-1_8-1_7-1_5-1_12-2_6-45_10-1_2-18_22-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "f99a66", + "i": "v-37_c-1_b-3_g-m_9-1_1-2_16-3_3-1_8-1_7-1_5-1_12-2_6-45_10-1_2-18_22-1_4-1.png" + } + }, + { + "id": 18964458, + "text": "People, please don't eat when I smoke, it annoys me. \n\nI don't care anymore if someone has problem with my smoke. It's for three minutes pussies. Live and let live! It's getting out of control with those complainers. \n\nI really stop taking the people around me in consideration regarding that. Because they also don't give a fuck about my comfort. Always passive aggressive like moving to another table with complaining just loud enough that I can hear about the smoking. \n\nI do not smoke when someone is eating next to me - but people are up front already pissed off. You know what freaking stinks? Your 16,- tosti! \n\nAnd you know- since people are too weak these days to say it straight in your face (they also don't because they're wrong, they weren't eating yet). This happens to me on such regular basis, I'm done. \n\nPussies, all freaking pussies. They're not complaining about the smell, they're just complaining because threy're sheep. \n\n(i still hear her complaining, also she has like a njet-dog. A dog that is njet a dog, size of a cat with even a bigger mouth than the owner). \n\nThat fucking untrained beast is freaking barking the shit outta here, like that's considerate. Train your dog or leave it home. Probably also trained with passive aggressiveness. \n\nThe Dutch, famous about being direct - it's over. All pussies now.", + "score": 2, + "created_time": 1754134811, + "attached_image": "", + "num_comments": 15, + "tags": [ + "rant", + "all", + "them", + "smoking", + "njet-dog", + "fuck", + "aggressive", + "passive" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 18926947, + "user_username": "tamagotchi", + "user_score": 301, + "user_avatar": { + "b": "d55161", + "i": "v-37_c-3_b-5_g-m_9-1_1-13_16-1_3-1_8-1_7-1_5-1_12-13_6-88_10-1_2-30_22-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "d55161", + "i": "v-37_c-1_b-5_g-m_9-1_1-13_16-1_3-1_8-1_7-1_5-1_12-13_6-88_10-1_2-30_22-1_4-1.png" + } + }, + { + "id": 18965428, + "text": "modern cars are awful. cheeky fuckers.\n\nwas driving somewhere with my mon in her BMW and for some reason our conversation in notEnglish triggered the AI assistant and my mom told it \"fuck you\" in our native tongue and it replied with \"watch your tone\" LMAO.\n\nIt's so funny, but awful in a way. And it keeps jerking the steering wheel when you go too close to a lane. It's quite jarring to drive like that.", + "score": 5, + "created_time": 1754152622, + "attached_image": "", + "num_comments": 4, + "tags": [ + "rant", + "smartcar" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 13503897, + "user_username": "int32", + "user_score": 430, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-f_9-1_1-1_16-11_3-15_8-1_7-1_5-1_12-1_6-40_2-35_22-11_11-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-f_9-1_1-1_16-11_3-15_8-1_7-1_5-1_12-1_6-40_2-35_22-11_11-1_4-1.png" + } + }, + { + "id": 18963248, + "text": "It\u2019s so sad that this \u201cAuth for Google authenticator\u201d app whose logo definitely doesn\u2019t try to impersonate google with a similar color scheme, with in-app purchases, is allowed to exist, while Lensflare\u2019s JoyRant isn\u2019t.", + "score": 7, + "created_time": 1754112038, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18963248_7ZDdW.jpg", + "width": 800, + "height": 780 + }, + "num_comments": 8, + "tags": [ + "devrant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 5, + "user_id": 1222469, + "user_username": "kiki", + "user_score": 37423, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.png" + } + }, + { + "id": 18965139, + "text": "Everything hurts. Starting from DevRant preventing me from making separate posts except I wait two hours. I'm infuriated at such a stupid rule like wtf is it supposed to achieve ffs? I'll just merge both rants into this\n\nNext are the discriminatory fucks on reddit. I hate this platform for a number of reasons but the one currently on my nerves right now is the warped stigma against contributions from users with site rep below a certain threshold. How do myopic freaks like this make it to a position of authority?? Being knowledgeable in a given field is not exclusive to reddit users who were able to amass a ton of upvotes. It automatically excludes everyone with potential to stir valuable discourse. Just cuz they're not well liked on the platform\n\nBasically, you earn points on cheap subs, come leverage them on the \"prestigious\" leagues, where those animals could lynch you. This isn't even hypothetical. Several times, all the visitors on a thread would launch a vendetta on a user (usually the OP), mass downvoting all their content on that thread. They're like wild beasts feasting on carcass\n\nThe next group absolutely pissing me off are organisations that use 2fa. How did this crazy design get so popular?? It makes absolutely no sense cuz in the event of losing my device or if the dev to whose device it was tied to leaves, you're fucked. You'll lose all your accounts. You're always calling some colleague holding the phone for codes. You need to snap barcodes on one phone and scan on auth app. Everything about it is so frustrating and painful\n\nI was forced by github to use sms for 2fa. I'm already reluctant about working on this project but I drag myself to the system and try to sign in. Turns out the security conscious dickheads let me view my secret gists without being signed in, but they won't send the 2fa code. I faintly recall a mail that it was getting deprecated. So after what felt like eternity in perdition, I manage to setup the app type 2fa but what if I lose the phone or it's formatted?? I'm locked out! So so stupid. One of my banks sends OTPs to my line. The other doesn't. But a useless organization hosting my OSS that nobody wants to use is bent on ruining my life with their insane security measures. So So daft. What's with the excessive paranoia?? Same goes for facebook. Just send an alert to my mail if ip or location is suspicious and I'll click a confirmation mail. What if my screen is bad and I'm trying to login on another device?? How don't they think of all this before tying authentication to a physical device for christ's sakes?????\n\nAnd then there's the horrible customers I've been getting on my fledgling business, along with a supplier as well. One requests for location, implying seriousness. Then ghosts. Another one wants to buy a commodity for less than half its price. How do you bargain from 330k to 150k? I rallied around for an alternative at 170k just to make sales. At the end of the day, he only wants the one for 330k. I wish I could take my skin off and black out. I'm so tired and cranky. I woke up so early today to be productive but everything I've met ever since has been nothing but pain and sorrow", + "score": 2, + "created_time": 1754147247, + "attached_image": "", + "num_comments": 3, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 5910183, + "user_username": "Nmeri17", + "user_score": 257, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18964689, + "text": "for fucks sake, if you start with me, do it properly. don't run away whimpering like a puppy. \n\nGrow some balls next time, oh wait. some can't.", + "score": 1, + "created_time": 1754138930, + "attached_image": "", + "num_comments": 28, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 2258929, + "user_username": "SidTheITGuy", + "user_score": 9678, + "user_avatar": { + "b": "7bc8a4", + "i": "v-37_c-3_b-1_g-m_9-1_1-6_16-12_3-6_8-3_7-3_5-2_12-6_6-2_2-31_22-8_19-1_4-2.jpg" + }, + "user_avatar_lg": { + "b": "7bc8a4", + "i": "v-37_c-1_b-1_g-m_9-1_1-6_16-12_3-6_8-3_7-3_5-2_12-6_6-2_2-31_22-8_19-1_4-2.png" + } + }, + { + "id": 18961184, + "text": "Ah yes. ubiquitous-octo-engine. Short and memorable.", + "score": 3, + "created_time": 1754072089, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18961184_8FURW.jpg", + "width": 776, + "height": 191 + }, + "num_comments": 2, + "tags": [ + "random", + "github" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "user_id": 18961166, + "user_username": "Tenebris", + "user_score": 3, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18960897, + "text": "Writing concurrent code is hard, but testing it, is something else. I'm almost sure my implementation is flawed, but I can't get the tests to fail!", + "score": 4, + "created_time": 1754066844, + "attached_image": "", + "num_comments": 2, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 18417415, + "user_username": "kdvps", + "user_score": 30, + "user_avatar": { + "b": "e6c653", + "i": "v-37_c-3_b-7_g-m_9-1_1-8_16-5_3-12_8-1_7-1_5-1_12-8_6-19_2-31_22-8_15-4_11-6_4-1.jpg" + }, + "user_avatar_lg": { + "b": "e6c653", + "i": "v-37_c-1_b-7_g-m_9-1_1-8_16-5_3-12_8-1_7-1_5-1_12-8_6-19_2-31_22-8_15-4_11-6_4-1.png" + } + }, + { + "id": 18960901, + "text": "I don't know about your country but this feature is novel among Nigeria's financial institutions. What usually happens in a typical bank app is same as above: fields are provided for entering account details. There is no way to know the outcome of the transfer until it's made. If it fails in transit (often, you're debited but the recipient gets nothing), you might get a reversal if you're lucky, after an indefinite period of time. Otherwise, you have to take it up with your bank or the recipient's bank. Or worse, with the central bank, when the first two are not being helpful enough \n\nEnter this new generation fintech (Opay). They offer an addition that impresses all customers: after selecting the bank, a popup appears that notifies you on the stability of the receiver's network. Someone sent me this screenshot seeking my permission or provision of another bank. I didn't think much of it and asked them to proceed. To my surprise, transaction failed and their money instantly reversed\n\nThose traditional banks clearly have no api for health checks, otherwise they'd all adopt it within their own apps. So, how is this possible? My only guess is that Opay maintains their own health checks system that is updated maybe by periodically pinging those banks with nominal fees like N1 and verifying whether money was received\n\nIt's obviously primitive but I doubt traditional bank apis return a failure response (since none currently tells you when transaction failed). So you'd have to rely on workarounds emulating manual and automated testing \n\nTo those in the fintech sector or with a faint idea of what's going on, can you explain?", + "score": 0, + "created_time": 1754066883, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18960901_nRj1i.jpg", + "width": 460, + "height": 998 + }, + "num_comments": 10, + "tags": [ + "question", + "fintech", + "transaction" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 4, + "user_id": 5910183, + "user_username": "Nmeri17", + "user_score": 257, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18964412, + "text": "Realization today:\n-I have serious traumas from my childhood after:\nworking on animal agriculture and industrial farms \n-Living in a shit hole country during bombardment and war\n-Couldn't get any job with a degree and was forced to leave with 300\u20ac in my pocket to stay alive \n\nI'm glad that all of this is over for more than 10+ years but I feel it caused a serious damage in my mental health.", + "score": 5, + "created_time": 1754134032, + "attached_image": "", + "num_comments": 3, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 3316724, + "user_username": "blindXfish", + "user_score": 2453, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-m_9-1_1-2_16-12_3-7_8-2_7-2_5-2_12-2_6-3_2-47_22-4_15-3_18-3_19-3_4-2_20-10.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-m_9-1_1-2_16-12_3-7_8-2_7-2_5-2_12-2_6-3_2-47_22-4_15-3_18-3_19-3_4-2_20-10.png" + } + }, + { + "id": 18961990, + "text": "these pills are literally giving me suffering all over my body\n\nit isn't aches or pain. it's always suffering. I can deal with aches and pain. but this is just pure suffering. all the fucking time. jesus fuck I feel so violent. if I get mad and explode I feel better for about 5 minutes but then I exhaust myself doing it, and then next time it gets harder to do. how do I escape this cage\n\ncan't sit down still much less fucking concentrate on anything. both these cause suffering to mount. what the fuuuuck\n\nbefore magic would get me out of it but now I'm demoralized and exhausted. I keep eating people's energy and it only helps for a couple hours. I wonder if turning into a crying vegetable would help. or maybe I'll just scream like a crazy person for a few hours. surely you must get used to it but it's been over a week now\n\nkicker is, suffering isn't one of the symptoms of the pill. \"restlessness\" is as if they don't know why a person would feel fucking restless. joke.\n\n... you know one dude said you gotta feel it to heal it so maybe I repressed my sickness so much for the 3 years I stored it all in my body as fibromyalgia (which I did have inflammation of) and now I just get to feel echoes of what happened that didn't previously go through my nerves because I was doing my damn best to block them out but maybe that's too crazy a thought. better explanation than these doctors just being oblivious though", + "score": 3, + "created_time": 1754086955, + "attached_image": "", + "num_comments": 11, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 4894369, + "user_username": "jestdotty", + "user_score": 5953, + "user_avatar": { + "b": "7bc8a4", + "i": "v-37_c-3_b-1_g-f_9-1_1-3_16-11_3-15_8-3_7-3_5-2_12-3_6-8_2-53_22-1_11-8_19-3_4-2.jpg" + }, + "user_avatar_lg": { + "b": "7bc8a4", + "i": "v-37_c-1_b-1_g-f_9-1_1-3_16-11_3-15_8-3_7-3_5-2_12-3_6-8_2-53_22-1_11-8_19-3_4-2.png" + } + }, + { + "id": 18963761, + "text": "the scariest part about the worldwide musical degeneracy propaganda campaign is that people are doing it of their own accord. there is no hand pulling the strings behind the scenes.\r\nalso, since when exposed labea majora become synonymous to music? this shit is repulsive.", + "score": 2, + "created_time": 1754121732, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18963761_StCDM.jpg", + "width": 640, + "height": 640 + }, + "num_comments": 5, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 1222469, + "user_username": "kiki", + "user_score": 37423, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.png" + } + }, + { + "id": 18961474, + "text": "What the fuck is this AI bullshit?\n\nI mean, whatever you try to do takes 2834570892 gigs of space, $19357413 worth of GPU VMs and a metric fuck ton of \"what the fucks?!?!\" per minute of trying to figure out how to get this fucking bullshit to just.fucking.work.\n\nBackstory: trying to put together a service that answers a simple fucking section: \"does any other user use this photo?\"\n\nObviously, you can't just pass two photos to a function and have it say true or false without ending up paying more than you'd make by selling half of england's kids' kidneys. What if it's cropped? what if it's inverted? what if it's been used in a different resolution? what if it's been converted to black& white? what if it's been resized?\n\nFUCK\n\nWHAT THE FUCK\n\nIt had to turn into yet another PhD research session\n\nFUCK ME\n\n2 million years of evolution and we can't have a simple mother fucking gem that tells you if a picture is there or not\n\nEnter AI, mankind's latest saviour that \"does everything and it does it better than people\", and allegedly cheaper since pretty much everybody and their fucking rabid pekingese are whining that \"AI WilL taKE mY FucKinG JoB omG it's BooMerS faUlT they destroy our planet and our lives\". \r\nAnyway, let's fucking google this shit because some other idiot must have figured this out by now: 20+ different web services doing what I need (basically \"describe an image in detail\" so I can plug whatever it spits out into a method that fuzzy matches against other descriptions in a table... along with other methods that check using other strategies) so yeah, let me just click the \"Pricing\" page and... what the actual FUCK $1 to describe an image? I will buy a 787 full of chinese and paki kids and force them to describe images 24/7 in my basement for less than what I'd pay those fuckers for their service\n\nAnyway, let's pull out plan C, forget those wankers, let me see how the fuck it's done and I'll set it up to run on some machine somewhere... llm llava ollama fuck-in-the-face... what the fuck were those people high on when they named all this crap? Nevermind. Spent 6 fucking days getting it to install and run (have you fuckers ever heard of developer experience or user experience? fuckheads) but still can't use it because it takes $2000/mo in GPUs to DESCRIBE A FUCKING IMAGE.\n\nFUCK.ALL.OF.THIS.", + "score": 5, + "created_time": 1754077816, + "attached_image": "", + "num_comments": 12, + "tags": [ + "rant", + "ai sucks dicks" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 18961411, + "user_username": "molaram", + "user_score": 34, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18962488, + "text": "Lensflare! WTF did you do?!\n\nhttps://reddit.com/r/Asmongold/...\n\nI know some of Germany has been regressing, but this is terrible!\n\nLOL", + "score": 2, + "created_time": 1754096831, + "attached_image": "", + "num_comments": 11, + "tags": [ + "rant", + "the reich way", + "your mom", + "ostream will never let this go", + "wtf", + "lol", + "salute" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "links": [ + { + "type": "url", + "url": "https://www.reddit.com/r/Asmongold/comments/1mf0bzx/germany_going_back_to_its_roots/", + "short_url": "https://reddit.com/r/Asmongold/...", + "title": "https://reddit.com/r/Asmongold/...", + "start": 30, + "end": 64, + "special": 1 + } + ], + "special": true, + "user_id": 18170221, + "user_username": "YourMom", + "user_score": 535, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18961399, + "text": "intellij no longer recognizes java imports, but the non-java imports are still recognized\n\ni didnt do anything, fml\n\nat least invalidate caches seems to have fixed?", + "score": 3, + "created_time": 1754076418, + "attached_image": "", + "num_comments": 1, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 1763168, + "user_username": "sjwsjwsjw", + "user_score": 1695, + "user_avatar": { + "b": "e6c653", + "i": "v-37_c-3_b-7_g-f_9-1_1-13_16-1_3-16_8-1_7-1_5-1_12-13_6-42_10-1_2-4_22-3_11-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "e6c653", + "i": "v-37_c-1_b-7_g-f_9-1_1-13_16-1_3-16_8-1_7-1_5-1_12-13_6-42_10-1_2-4_22-3_11-1_4-1.png" + } + }, + { + "id": 18965199, + "text": "Watching a YouTube video where a legit, professional MMA fighter can do nothing to remain alive during the 20 seconds he got to spend in a room with an attacker who has a knife. Every single round, he can knock the attacker out, only to then succumb to 18 stabs he received while grappling.\n\nhttps://youtube.com/watch/...", + "score": 1, + "created_time": 1754148345, + "attached_image": "", + "num_comments": 6, + "tags": [ + "random" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "links": [ + { + "type": "url", + "url": "https://www.youtube.com/watch?v=ipf1mROm6rg", + "short_url": "https://youtube.com/watch/...", + "title": "https://youtube.com/watch/...", + "start": 292, + "end": 321, + "special": 1 + } + ], + "special": true, + "user_id": 1222469, + "user_username": "kiki", + "user_score": 37423, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.png" + } + } + ], + "settings": { + "notif_state": -1, + "notif_token": "" + }, + "set": "688e90989028a", + "wrw": 385, + "dpp": 0, + "num_notifs": 0, + "unread": { + "total": 0 + }, + "news": { + "id": 356, + "type": "intlink", + "headline": "Weekly Group Rant", + "body": "Tips for staying productive?", + "footer": "Add tag 'wk247' to your rant", + "height": 100, + "action": "grouprant" + } + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:33 GMT", + "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Vary": "Accept-Encoding", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "GET, PUT, OPTIONS", + "Content-Encoding": "gzip" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:33.384041" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811/comments", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:36 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "comment": "Test comment for API testing (ignore)", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:36.232632" + }, + { + "url": "https://devrant.com/api/users", + "method": "POST", + "status_code": 200, + "response": { + "success": false, + "error": "Your username must be between 4 and 15 characters.", + "error_field": "username" + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:37 GMT", + "Content-Type": "application/json", + "Content-Length": "103", + "Connection": "keep-alive" + }, + "request_body": { + "type": 1, + "email": "test@example.com", + "username": "testuser1754173585", + "password": "Test1234!", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:37.182302" + }, + { + "url": "https://devrant.com/api/users", + "method": "POST", + "status_code": 200, + "response": { + "success": false, + "error": "Please supply a valid email address.", + "error_field": "email" + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:37 GMT", + "Content-Type": "application/json", + "Content-Length": "86", + "Connection": "keep-alive" + }, + "request_body": { + "type": 1, + "username": "testuser1754173585", + "password": "Test1234!", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:38.041992" + }, + { + "url": "https://devrant.com/api/users/auth-token", + "method": "POST", + "status_code": 400, + "response": { + "success": false, + "error": "Invalid login credentials entered. Please try again." + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:38 GMT", + "Content-Type": "application/json", + "Content-Length": "80", + "Connection": "keep-alive" + }, + "request_body": { + "username": "testuser1754173585", + "password": "WrongPass", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:38.999225" + }, + { + "url": "https://devrant.com/api/users/me/edit-profile", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:40 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "profile_about": "Test bio", + "profile_skills": "Python, JS", + "profile_location": "Test City", + "profile_website": "http://example.com", + "profile_github": "testuser", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:40.224358" + }, + { + "url": "https://devrant.com/api/users/forgot-password", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:41 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "username": "larrylewis@molodetz.nl", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:41.963070" + }, + { + "url": "https://devrant.com/api/users/me/resend-confirm", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:43 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:43.699272" + }, + { + "url": "https://devrant.com/api/users/me/mark-news-read", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:45 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "news_id": "1", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:45.328105" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811?last_comment_id=999999999999&links=0&plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "GET", + "status_code": 200, + "response": { + "rant": { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 3, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 10, + "tags": [ + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": 0, + "edited": false, + "link": "rants/18960811/you-know-im-getting-tired-of-this-hr-speak-in-job-applications-specifically-this", + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + }, + "comments": [], + "success": true, + "subscribed": 0 + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:48 GMT", + "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Vary": "Accept-Encoding", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "GET, PUT, OPTIONS", + "Content-Encoding": "gzip" + }, + "request_body": { + "last_comment_id": "999999999999", + "links": 0, + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:48.691256" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811", + "method": "POST", + "status_code": 200, + "response": { + "success": false, + "fail_reason": "" + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:50 GMT", + "Content-Type": "application/json", + "Content-Length": "34", + "Connection": "keep-alive" + }, + "request_body": { + "rant": "Updated test rant", + "tags": "test,python,update", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:50.195194" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811/vote", + "method": "POST", + "status_code": 200, + "response": { + "success": true, + "rant": { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 4, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 10, + "tags": [ + "rant", + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": 1, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + } + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:53 GMT", + "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Vary": "Accept-Encoding", + "Content-Encoding": "gzip" + }, + "request_body": { + "vote": -1, + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8", + "reason": "1" + }, + "timestamp": "2025-08-03T00:26:53.381324" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811/vote", + "method": "POST", + "status_code": 200, + "response": { + "success": true, + "rant": { + "id": 18960811, + "text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.", + "score": 3, + "created_time": 1754065322, + "attached_image": "", + "num_comments": 10, + "tags": [ + "rant", + "too-much", + "qualifications", + "job-hunting" + ], + "vote_state": -1, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4180, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + } + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:56 GMT", + "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Vary": "Accept-Encoding", + "Content-Encoding": "gzip" + }, + "request_body": { + "vote": -1, + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8", + "reason": "1" + }, + "timestamp": "2025-08-03T00:26:56.847526" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811/favorite", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:26:58 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:26:58.732555" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811/unfavorite", + "method": "POST", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:00 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:00.577474" + }, + { + "url": "https://devrant.com/api/devrant/rants?plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "GET", + "status_code": 200, + "response": { + "success": true, + "rants": [ + { + "id": 18964458, + "text": "People, please don't eat when I smoke, it annoys me. \n\nI don't care anymore if someone has problem with my smoke. It's for three minutes pussies. Live and let live! It's getting out of control with those complainers. \n\nI really stop taking the people around me in consideration regarding that. Because they also don't give a fuck about my comfort. Always passive aggressive like moving to another table with complaining just loud enough that I can hear about the smoking. \n\nI do not smoke when someone is eating next to me - but people are up front already pissed off. You know what freaking stinks? Your 16,- tosti! \n\nAnd you know- since people are too weak these days to say it straight in your face (they also don't because they're wrong, they weren't eating yet). This happens to me on such regular basis, I'm done. \n\nPussies, all freaking pussies. They're not complaining about the smell, they're just complaining because threy're sheep. \n\n(i still hear her complaining, also she has like a njet-dog. A dog that is njet a dog, size of a cat with even a bigger mouth than the owner). \n\nThat fucking untrained beast is freaking barking the shit outta here, like that's considerate. Train your dog or leave it home. Probably also trained with passive aggressiveness. \n\nThe Dutch, famous about being direct - it's over. All pussies now.", + "score": 2, + "created_time": 1754134811, + "attached_image": "", + "num_comments": 15, + "tags": [ + "rant", + "all", + "them", + "smoking", + "njet-dog", + "fuck", + "aggressive", + "passive" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 18926947, + "user_username": "tamagotchi", + "user_score": 301, + "user_avatar": { + "b": "d55161", + "i": "v-37_c-3_b-5_g-m_9-1_1-13_16-1_3-1_8-1_7-1_5-1_12-13_6-88_10-1_2-30_22-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "d55161", + "i": "v-37_c-1_b-5_g-m_9-1_1-13_16-1_3-1_8-1_7-1_5-1_12-13_6-88_10-1_2-30_22-1_4-1.png" + } + }, + { + "id": 18964412, + "text": "Realization today:\n-I have serious traumas from my childhood after:\nworking on animal agriculture and industrial farms \n-Living in a shit hole country during bombardment and war\n-Couldn't get any job with a degree and was forced to leave with 300\u20ac in my pocket to stay alive \n\nI'm glad that all of this is over for more than 10+ years but I feel it caused a serious damage in my mental health.", + "score": 5, + "created_time": 1754134032, + "attached_image": "", + "num_comments": 3, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 3316724, + "user_username": "blindXfish", + "user_score": 2453, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-m_9-1_1-2_16-12_3-7_8-2_7-2_5-2_12-2_6-3_2-47_22-4_15-3_18-3_19-3_4-2_20-10.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-m_9-1_1-2_16-12_3-7_8-2_7-2_5-2_12-2_6-3_2-47_22-4_15-3_18-3_19-3_4-2_20-10.png" + } + }, + { + "id": 18960897, + "text": "Writing concurrent code is hard, but testing it, is something else. I'm almost sure my implementation is flawed, but I can't get the tests to fail!", + "score": 4, + "created_time": 1754066844, + "attached_image": "", + "num_comments": 2, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 18417415, + "user_username": "kdvps", + "user_score": 30, + "user_avatar": { + "b": "e6c653", + "i": "v-37_c-3_b-7_g-m_9-1_1-8_16-5_3-12_8-1_7-1_5-1_12-8_6-19_2-31_22-8_15-4_11-6_4-1.jpg" + }, + "user_avatar_lg": { + "b": "e6c653", + "i": "v-37_c-1_b-7_g-m_9-1_1-8_16-5_3-12_8-1_7-1_5-1_12-8_6-19_2-31_22-8_15-4_11-6_4-1.png" + } + }, + { + "id": 18962488, + "text": "Lensflare! WTF did you do?!\n\nhttps://reddit.com/r/Asmongold/...\n\nI know some of Germany has been regressing, but this is terrible!\n\nLOL", + "score": 2, + "created_time": 1754096831, + "attached_image": "", + "num_comments": 11, + "tags": [ + "rant", + "the reich way", + "your mom", + "ostream will never let this go", + "wtf", + "lol", + "salute" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "links": [ + { + "type": "url", + "url": "https://www.reddit.com/r/Asmongold/comments/1mf0bzx/germany_going_back_to_its_roots/", + "short_url": "https://reddit.com/r/Asmongold/...", + "title": "https://reddit.com/r/Asmongold/...", + "start": 30, + "end": 64, + "special": 1 + } + ], + "special": true, + "user_id": 18170221, + "user_username": "YourMom", + "user_score": 535, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18961990, + "text": "these pills are literally giving me suffering all over my body\n\nit isn't aches or pain. it's always suffering. I can deal with aches and pain. but this is just pure suffering. all the fucking time. jesus fuck I feel so violent. if I get mad and explode I feel better for about 5 minutes but then I exhaust myself doing it, and then next time it gets harder to do. how do I escape this cage\n\ncan't sit down still much less fucking concentrate on anything. both these cause suffering to mount. what the fuuuuck\n\nbefore magic would get me out of it but now I'm demoralized and exhausted. I keep eating people's energy and it only helps for a couple hours. I wonder if turning into a crying vegetable would help. or maybe I'll just scream like a crazy person for a few hours. surely you must get used to it but it's been over a week now\n\nkicker is, suffering isn't one of the symptoms of the pill. \"restlessness\" is as if they don't know why a person would feel fucking restless. joke.\n\n... you know one dude said you gotta feel it to heal it so maybe I repressed my sickness so much for the 3 years I stored it all in my body as fibromyalgia (which I did have inflammation of) and now I just get to feel echoes of what happened that didn't previously go through my nerves because I was doing my damn best to block them out but maybe that's too crazy a thought. better explanation than these doctors just being oblivious though", + "score": 3, + "created_time": 1754086955, + "attached_image": "", + "num_comments": 11, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 4894369, + "user_username": "jestdotty", + "user_score": 5953, + "user_avatar": { + "b": "7bc8a4", + "i": "v-37_c-3_b-1_g-f_9-1_1-3_16-11_3-15_8-3_7-3_5-2_12-3_6-8_2-53_22-1_11-8_19-3_4-2.jpg" + }, + "user_avatar_lg": { + "b": "7bc8a4", + "i": "v-37_c-1_b-1_g-f_9-1_1-3_16-11_3-15_8-3_7-3_5-2_12-3_6-8_2-53_22-1_11-8_19-3_4-2.png" + } + }, + { + "id": 18965199, + "text": "Watching a YouTube video where a legit, professional MMA fighter can do nothing to remain alive during the 20 seconds he got to spend in a room with an attacker who has a knife. Every single round, he can knock the attacker out, only to then succumb to 18 stabs he received while grappling.\n\nhttps://youtube.com/watch/...", + "score": 1, + "created_time": 1754148345, + "attached_image": "", + "num_comments": 6, + "tags": [ + "random" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "links": [ + { + "type": "url", + "url": "https://www.youtube.com/watch?v=ipf1mROm6rg", + "short_url": "https://youtube.com/watch/...", + "title": "https://youtube.com/watch/...", + "start": 292, + "end": 321, + "special": 1 + } + ], + "special": true, + "user_id": 1222469, + "user_username": "kiki", + "user_score": 37423, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.png" + } + }, + { + "id": 18961399, + "text": "intellij no longer recognizes java imports, but the non-java imports are still recognized\n\ni didnt do anything, fml\n\nat least invalidate caches seems to have fixed?", + "score": 3, + "created_time": 1754076418, + "attached_image": "", + "num_comments": 1, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 1763168, + "user_username": "sjwsjwsjw", + "user_score": 1695, + "user_avatar": { + "b": "e6c653", + "i": "v-37_c-3_b-7_g-f_9-1_1-13_16-1_3-16_8-1_7-1_5-1_12-13_6-42_10-1_2-4_22-3_11-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "e6c653", + "i": "v-37_c-1_b-7_g-f_9-1_1-13_16-1_3-16_8-1_7-1_5-1_12-13_6-42_10-1_2-4_22-3_11-1_4-1.png" + } + }, + { + "id": 18966144, + "text": "me is make search:\r\nc fun strsub name was?\n\naI oVeRQuEEfvIew:\r\ntHeRE iZ nO fUnKCHon iN ceE foR gEtTen sUb sTrEnggGzEn\n\nno, that is lie! you are liar!\r\nme is thumb you down. bad gugul.\r\nme is make search again now.\r\noh wait memory return.\r\nstrstr.\r\nright.\r\nthat the name silly me.\r\ni be alias fun to strsub because more mnemonic.\n\nbut why is gugul degenerating search don't know.\r\nruining experience not OK.\n\nplease someone tell mister pichai is make search bad thanks.\r\ni want good search back, can switch to duck?\r\nopen duck.\r\noh noes.\r\nduck is degenerate now.\r\nhas duck.ai on frontpage.\n\nwhyyyyyyyyyyy me is make tears now.\r\nneed good search not gorgabe.", + "score": 2, + "created_time": 1754166024, + "attached_image": "", + "num_comments": 1, + "tags": [ + "random", + "the gorgabe be real" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "user_id": 10429171, + "user_username": "Liebranca", + "user_score": 1116, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-m_9-1_1-9_16-6_3-3_8-1_7-1_5-1_12-9_6-26_2-1_22-2_15-10_11-3_19-1_4-1_20-12.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-m_9-1_1-9_16-6_3-3_8-1_7-1_5-1_12-9_6-26_2-1_22-2_15-10_11-3_19-1_4-1_20-12.png" + } + }, + { + "id": 18960901, + "text": "I don't know about your country but this feature is novel among Nigeria's financial institutions. What usually happens in a typical bank app is same as above: fields are provided for entering account details. There is no way to know the outcome of the transfer until it's made. If it fails in transit (often, you're debited but the recipient gets nothing), you might get a reversal if you're lucky, after an indefinite period of time. Otherwise, you have to take it up with your bank or the recipient's bank. Or worse, with the central bank, when the first two are not being helpful enough \n\nEnter this new generation fintech (Opay). They offer an addition that impresses all customers: after selecting the bank, a popup appears that notifies you on the stability of the receiver's network. Someone sent me this screenshot seeking my permission or provision of another bank. I didn't think much of it and asked them to proceed. To my surprise, transaction failed and their money instantly reversed\n\nThose traditional banks clearly have no api for health checks, otherwise they'd all adopt it within their own apps. So, how is this possible? My only guess is that Opay maintains their own health checks system that is updated maybe by periodically pinging those banks with nominal fees like N1 and verifying whether money was received\n\nIt's obviously primitive but I doubt traditional bank apis return a failure response (since none currently tells you when transaction failed). So you'd have to rely on workarounds emulating manual and automated testing \n\nTo those in the fintech sector or with a faint idea of what's going on, can you explain?", + "score": 0, + "created_time": 1754066883, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18960901_nRj1i.jpg", + "width": 460, + "height": 998 + }, + "num_comments": 10, + "tags": [ + "question", + "fintech", + "transaction" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 4, + "user_id": 5910183, + "user_username": "Nmeri17", + "user_score": 257, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18965139, + "text": "Everything hurts. Starting from DevRant preventing me from making separate posts except I wait two hours. I'm infuriated at such a stupid rule like wtf is it supposed to achieve ffs? I'll just merge both rants into this\n\nNext are the discriminatory fucks on reddit. I hate this platform for a number of reasons but the one currently on my nerves right now is the warped stigma against contributions from users with site rep below a certain threshold. How do myopic freaks like this make it to a position of authority?? Being knowledgeable in a given field is not exclusive to reddit users who were able to amass a ton of upvotes. It automatically excludes everyone with potential to stir valuable discourse. Just cuz they're not well liked on the platform\n\nBasically, you earn points on cheap subs, come leverage them on the \"prestigious\" leagues, where those animals could lynch you. This isn't even hypothetical. Several times, all the visitors on a thread would launch a vendetta on a user (usually the OP), mass downvoting all their content on that thread. They're like wild beasts feasting on carcass\n\nThe next group absolutely pissing me off are organisations that use 2fa. How did this crazy design get so popular?? It makes absolutely no sense cuz in the event of losing my device or if the dev to whose device it was tied to leaves, you're fucked. You'll lose all your accounts. You're always calling some colleague holding the phone for codes. You need to snap barcodes on one phone and scan on auth app. Everything about it is so frustrating and painful\n\nI was forced by github to use sms for 2fa. I'm already reluctant about working on this project but I drag myself to the system and try to sign in. Turns out the security conscious dickheads let me view my secret gists without being signed in, but they won't send the 2fa code. I faintly recall a mail that it was getting deprecated. So after what felt like eternity in perdition, I manage to setup the app type 2fa but what if I lose the phone or it's formatted?? I'm locked out! So so stupid. One of my banks sends OTPs to my line. The other doesn't. But a useless organization hosting my OSS that nobody wants to use is bent on ruining my life with their insane security measures. So So daft. What's with the excessive paranoia?? Same goes for facebook. Just send an alert to my mail if ip or location is suspicious and I'll click a confirmation mail. What if my screen is bad and I'm trying to login on another device?? How don't they think of all this before tying authentication to a physical device for christ's sakes?????\n\nAnd then there's the horrible customers I've been getting on my fledgling business, along with a supplier as well. One requests for location, implying seriousness. Then ghosts. Another one wants to buy a commodity for less than half its price. How do you bargain from 330k to 150k? I rallied around for an alternative at 170k just to make sales. At the end of the day, he only wants the one for 330k. I wish I could take my skin off and black out. I'm so tired and cranky. I woke up so early today to be productive but everything I've met ever since has been nothing but pain and sorrow", + "score": 2, + "created_time": 1754147247, + "attached_image": "", + "num_comments": 3, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 5910183, + "user_username": "Nmeri17", + "user_score": 257, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18963248, + "text": "It\u2019s so sad that this \u201cAuth for Google authenticator\u201d app whose logo definitely doesn\u2019t try to impersonate google with a similar color scheme, with in-app purchases, is allowed to exist, while Lensflare\u2019s JoyRant isn\u2019t.", + "score": 7, + "created_time": 1754112038, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18963248_7ZDdW.jpg", + "width": 800, + "height": 780 + }, + "num_comments": 8, + "tags": [ + "devrant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 5, + "user_id": 1222469, + "user_username": "kiki", + "user_score": 37423, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.png" + } + }, + { + "id": 18961721, + "text": "You are considered a junior developer when you need 'a lot of hand-holding'. Now, I can figure out most things on my own, but how do you distinguish between 'needs too much hand-holding and therefore is a junior' and 'does ok and therefore is a medior'? \n\nFor example, I can do well on basic things and getting projects set up, but then I might need more help if errors truly become too cryptic or difficult to solve and I haven't found solutions. A few examples here:\r\n- having messed up the git branches and releases so much that you need someone with a deep knowledge and troubleshooting of git to set the situation straight\r\n- spending a week on trying to figure out why Azure doesn't want to successfully build your super custom build and it takes ages to figure it out because it requires in-depth Docker, linux knowledge and stellar, MIT-level troubleshooting and analytical skills\n\nAnd so, someone who needs help with these is considered a junior?\n\nHow do you really identify a junior? Seems vague.", + "score": 3, + "created_time": 1754082038, + "attached_image": "", + "num_comments": 9, + "tags": [ + "question", + "proficiency", + "junior-vs-medior", + "job-hunting" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 4, + "user_id": 1366654, + "user_username": "CaptainRant", + "user_score": 4179, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png" + } + }, + { + "id": 18963761, + "text": "the scariest part about the worldwide musical degeneracy propaganda campaign is that people are doing it of their own accord. there is no hand pulling the strings behind the scenes.\r\nalso, since when exposed labea majora become synonymous to music? this shit is repulsive.", + "score": 2, + "created_time": 1754121732, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18963761_StCDM.jpg", + "width": 640, + "height": 640 + }, + "num_comments": 5, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 1222469, + "user_username": "kiki", + "user_score": 37423, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-f_9-2_1-9_16-17_3-12_8-4_7-4_5-1_12-9_17-2_6-32_10-9_2-59_22-1_11-15_18-1_19-2_4-1_20-3_21-4.png" + } + }, + { + "id": 18960705, + "text": "I'm in the process of suing my employer and I've mentally checked out of my job.\n\nFunny enough, threatening the assholes that threatened to fire me if I didn't come into the office \"for the benefit of my mental health\" has significantly improved my mental health.", + "score": 5, + "created_time": 1754063410, + "attached_image": "", + "num_comments": 10, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 225714, + "user_username": "atheist", + "user_score": 10689, + "user_avatar": { + "b": "69c9cd", + "i": "v-37_c-3_b-6_g-m_9-1_1-2_16-15_3-11_8-4_7-4_5-3_12-2_17-1_6-96_10-9_2-46_22-1_15-3_11-17_18-1_19-2_4-3_20-9_21-1.jpg" + }, + "user_avatar_lg": { + "b": "69c9cd", + "i": "v-37_c-1_b-6_g-m_9-1_1-2_16-15_3-11_8-4_7-4_5-3_12-2_17-1_6-96_10-9_2-46_22-1_15-3_11-17_18-1_19-2_4-3_20-9_21-1.png" + }, + "user_dpp": 1 + }, + { + "id": 18961184, + "text": "Ah yes. ubiquitous-octo-engine. Short and memorable.", + "score": 3, + "created_time": 1754072089, + "attached_image": { + "url": "https://img.devrant.com/devrant/rant/r_18961184_8FURW.jpg", + "width": 776, + "height": 191 + }, + "num_comments": 2, + "tags": [ + "random", + "github" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "user_id": 18961166, + "user_username": "Tenebris", + "user_score": 3, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18961474, + "text": "What the fuck is this AI bullshit?\n\nI mean, whatever you try to do takes 2834570892 gigs of space, $19357413 worth of GPU VMs and a metric fuck ton of \"what the fucks?!?!\" per minute of trying to figure out how to get this fucking bullshit to just.fucking.work.\n\nBackstory: trying to put together a service that answers a simple fucking section: \"does any other user use this photo?\"\n\nObviously, you can't just pass two photos to a function and have it say true or false without ending up paying more than you'd make by selling half of england's kids' kidneys. What if it's cropped? what if it's inverted? what if it's been used in a different resolution? what if it's been converted to black& white? what if it's been resized?\n\nFUCK\n\nWHAT THE FUCK\n\nIt had to turn into yet another PhD research session\n\nFUCK ME\n\n2 million years of evolution and we can't have a simple mother fucking gem that tells you if a picture is there or not\n\nEnter AI, mankind's latest saviour that \"does everything and it does it better than people\", and allegedly cheaper since pretty much everybody and their fucking rabid pekingese are whining that \"AI WilL taKE mY FucKinG JoB omG it's BooMerS faUlT they destroy our planet and our lives\". \r\nAnyway, let's fucking google this shit because some other idiot must have figured this out by now: 20+ different web services doing what I need (basically \"describe an image in detail\" so I can plug whatever it spits out into a method that fuzzy matches against other descriptions in a table... along with other methods that check using other strategies) so yeah, let me just click the \"Pricing\" page and... what the actual FUCK $1 to describe an image? I will buy a 787 full of chinese and paki kids and force them to describe images 24/7 in my basement for less than what I'd pay those fuckers for their service\n\nAnyway, let's pull out plan C, forget those wankers, let me see how the fuck it's done and I'll set it up to run on some machine somewhere... llm llava ollama fuck-in-the-face... what the fuck were those people high on when they named all this crap? Nevermind. Spent 6 fucking days getting it to install and run (have you fuckers ever heard of developer experience or user experience? fuckheads) but still can't use it because it takes $2000/mo in GPUs to DESCRIBE A FUCKING IMAGE.\n\nFUCK.ALL.OF.THIS.", + "score": 5, + "created_time": 1754077816, + "attached_image": "", + "num_comments": 12, + "tags": [ + "rant", + "ai sucks dicks" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 18961411, + "user_username": "molaram", + "user_score": 34, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18961977, + "text": "So the gaming industry was having huge profits (2024?), then they decided to layoff a bunch of people for \"reasons\", which displaced a bunch of people in the industry, who then became indies and are making good money, and people working for studios are salty about something they caused?\n\nhttps://reddit.com/r/gaming/...\n\nHow cancerous do you have to be in your industry to be pissed at people trying to recover from your cancer?\n\n\"deprofessionalization\" is this corporate speak for \"doesn't have a cushy middle management job at EA\"? I tried looking up the meaning and came away even more confused. You aren't a real gaming dev if you don't work for a huge studio?\n\nWeird way to de-legitimize people who had to look for work elsewhere. The people who left or got laid off are escaping a shitscape for sure.", + "score": 4, + "created_time": 1754086762, + "attached_image": "", + "num_comments": 3, + "tags": [ + "random", + "wtf", + "studios are stupid", + "your mom" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 6, + "links": [ + { + "type": "url", + "url": "https://www.reddit.com/r/gaming/comments/1kskew5/the_deprofessionalization_of_video_games_was_on/", + "short_url": "https://reddit.com/r/gaming/...", + "title": "https://reddit.com/r/gaming/...", + "start": 289, + "end": 320, + "special": 1 + } + ], + "special": true, + "user_id": 18170221, + "user_username": "YourMom", + "user_score": 535, + "user_avatar": { + "b": "7bc8a4" + }, + "user_avatar_lg": { + "b": "7bc8a4" + } + }, + { + "id": 18963683, + "text": "Got fed up with my newcomer PO's bullshit planning and constant gaslighting whenever deadlines get missed. \n\nMade a spreadsheet showing our actual capacity vs his unrealistic expectations - turns out all of Q3 work is spilling into Q4, so I'll be spending October finishing July/August/September tasks. \n\nBrought this up multiple times asking for more time or resources. Got denied and blamed instead. \n\nIt got really bad to the point where I had to start doing his work for him - make business decisions, create tickets, define acceptance criteria, requirements and etc. Just so I would have some paper trail so that I wouldnt get blamed again in case I'm missing some undefined requirement. Finally couldn't take it anymore and escalated to my dev lead.\n\nNow the PO is getting absolutely chewed out. Feels fucking good to finally see him face consequences for their terrible planning.\n\nOn another note - is it a disease or do the most useless people just naturally gravitate toward management positions? For example, this guy is supposed to be a decision maker but answers almost every business question vaguely, ambiguously, or just straight up ignores it. How the fuck such people even exist, it just blows my mind.", + "score": 3, + "created_time": 1754120321, + "attached_image": "", + "num_comments": 7, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 6029955, + "user_username": "topsecret230", + "user_score": 787, + "user_avatar": { + "b": "f99a66", + "i": "v-37_c-3_b-3_g-m_9-1_1-2_16-3_3-1_8-1_7-1_5-1_12-2_6-45_10-1_2-18_22-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "f99a66", + "i": "v-37_c-1_b-3_g-m_9-1_1-2_16-3_3-1_8-1_7-1_5-1_12-2_6-45_10-1_2-18_22-1_4-1.png" + } + }, + { + "id": 18965428, + "text": "modern cars are awful. cheeky fuckers.\n\nwas driving somewhere with my mon in her BMW and for some reason our conversation in notEnglish triggered the AI assistant and my mom told it \"fuck you\" in our native tongue and it replied with \"watch your tone\" LMAO.\n\nIt's so funny, but awful in a way. And it keeps jerking the steering wheel when you go too close to a lane. It's quite jarring to drive like that.", + "score": 5, + "created_time": 1754152622, + "attached_image": "", + "num_comments": 4, + "tags": [ + "rant", + "smartcar" + ], + "vote_state": 0, + "edited": true, + "rt": 1, + "rc": 1, + "user_id": 13503897, + "user_username": "int32", + "user_score": 430, + "user_avatar": { + "b": "2a8b9d", + "i": "v-37_c-3_b-4_g-f_9-1_1-1_16-11_3-15_8-1_7-1_5-1_12-1_6-40_2-35_22-11_11-1_4-1.jpg" + }, + "user_avatar_lg": { + "b": "2a8b9d", + "i": "v-37_c-1_b-4_g-f_9-1_1-1_16-11_3-15_8-1_7-1_5-1_12-1_6-40_2-35_22-11_11-1_4-1.png" + } + }, + { + "id": 18964689, + "text": "for fucks sake, if you start with me, do it properly. don't run away whimpering like a puppy. \n\nGrow some balls next time, oh wait. some can't.", + "score": 1, + "created_time": 1754138930, + "attached_image": "", + "num_comments": 28, + "tags": [ + "rant" + ], + "vote_state": 0, + "edited": false, + "rt": 1, + "rc": 1, + "user_id": 2258929, + "user_username": "SidTheITGuy", + "user_score": 9678, + "user_avatar": { + "b": "7bc8a4", + "i": "v-37_c-3_b-1_g-m_9-1_1-6_16-12_3-6_8-3_7-3_5-2_12-6_6-2_2-31_22-8_19-1_4-2.jpg" + }, + "user_avatar_lg": { + "b": "7bc8a4", + "i": "v-37_c-1_b-1_g-m_9-1_1-6_16-12_3-6_8-3_7-3_5-2_12-6_6-2_2-31_22-8_19-1_4-2.png" + } + } + ], + "settings": { + "notif_state": -1, + "notif_token": "" + }, + "set": "688e90b7cf77f", + "wrw": 385, + "dpp": 0, + "num_notifs": 0, + "unread": { + "total": 0 + }, + "news": { + "id": 356, + "type": "intlink", + "headline": "Weekly Group Rant", + "body": "Tips for staying productive?", + "footer": "Add tag 'wk247' to your rant", + "height": 100, + "action": "grouprant" + } + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:04 GMT", + "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "Vary": "Accept-Encoding", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "GET, PUT, OPTIONS", + "Content-Encoding": "gzip" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:04.853336" + }, + { + "url": "https://devrant.com/api/comments/1?links=0&plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "GET", + "status_code": 200, + "response": { + "success": false, + "error": "Invalid comment specified in path." + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:06 GMT", + "Content-Type": "text/html; charset=UTF-8", + "Content-Length": "62", + "Connection": "keep-alive" + }, + "request_body": { + "links": 0, + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:06.927067" + }, + { + "url": "https://devrant.com/api/comments/1", + "method": "POST", + "status_code": 200, + "response": { + "success": false, + "error": "Invalid comment specified in path." + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:08 GMT", + "Content-Type": "text/html; charset=UTF-8", + "Content-Length": "62", + "Connection": "keep-alive" + }, + "request_body": { + "comment": "Updated test comment", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:08.920648" + }, + { + "url": "https://devrant.com/api/comments/1?plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "DELETE", + "status_code": 200, + "response": { + "success": false, + "error": "Invalid comment specified in path." + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:10 GMT", + "Content-Type": "text/html; charset=UTF-8", + "Content-Length": "62", + "Connection": "keep-alive" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:10.845029" + }, + { + "url": "https://devrant.com/api/comments/1/vote", + "method": "POST", + "status_code": 200, + "response": { + "success": false, + "error": "Invalid comment specified in path." + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:12 GMT", + "Content-Type": "text/html; charset=UTF-8", + "Content-Length": "62", + "Connection": "keep-alive" + }, + "request_body": { + "vote": 1, + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:12.957294" + }, + { + "url": "https://devrant.com/api/users/me/notif-feed?ext_prof=1&last_time=1754173632&plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "GET", + "status_code": 200, + "response": { + "success": true, + "data": { + "items": [], + "check_time": 1754173634, + "username_map": [], + "unread": { + "all": 0, + "upvotes": 0, + "mentions": 0, + "comments": 0, + "subs": 0, + "total": 0 + }, + "num_unread": 0 + } + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:14 GMT", + "Content-Type": "application/json", + "Content-Length": "169", + "Connection": "keep-alive" + }, + "request_body": { + "ext_prof": 1, + "last_time": "1754173632", + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:14.631972" + }, + { + "url": "https://devrant.com/api/users/me/notif-feed?plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "DELETE", + "status_code": 200, + "response": { + "success": true + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:16 GMT", + "Content-Type": "application/json", + "Content-Length": "16", + "Connection": "keep-alive" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:16.615229" + }, + { + "url": "https://www.hexicallabs.com/api/beta-list", + "method": "GET", + "status_code": null, + "response": { + "error": "Expecting value: line 1 column 1 (char 0)" + }, + "headers": {}, + "request_body": { + "email": "test@example.com", + "platform": "test_platform", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:17.487604" + }, + { + "url": "https://devrant.com/api/devrant/rants/18960811?plat=3&guid=9dfc8004-9779-4291-a571-49008cd8a1ad&sid=1754173585&seid=6e74d3c1-818b-497c-a1cf-e32d4d1acf41&app=3&user_id=18959632&token_id=18966518&token_key=z6uXRZrQ_Ekx59wfYEjpbnS%21fDeVznVqmmKujhT8", + "method": "DELETE", + "status_code": 200, + "response": { + "success": false, + "error": "An unknown error occurred and this rant can't be deleted. Please contact support@devrant.com for help with this." + }, + "headers": { + "Server": "nginx/1.10.3 (Ubuntu)", + "Date": "Sat, 02 Aug 2025 22:27:19 GMT", + "Content-Type": "application/json", + "Content-Length": "140", + "Connection": "keep-alive" + }, + "request_body": { + "plat": 3, + "guid": "9dfc8004-9779-4291-a571-49008cd8a1ad", + "sid": "1754173585", + "seid": "6e74d3c1-818b-497c-a1cf-e32d4d1acf41", + "app": 3, + "user_id": "18959632", + "token_id": "18966518", + "token_key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8" + }, + "timestamp": "2025-08-03T00:27:19.262400" + } +] diff --git a/dist/devranta-1.0.0-py3-none-any.whl b/dist/devranta-1.0.0-py3-none-any.whl index 600846e..49f7351 100644 Binary files a/dist/devranta-1.0.0-py3-none-any.whl and b/dist/devranta-1.0.0-py3-none-any.whl differ diff --git a/dist/devranta-1.0.0.tar.gz b/dist/devranta-1.0.0.tar.gz index 38b4613..66d324a 100644 Binary files a/dist/devranta-1.0.0.tar.gz and b/dist/devranta-1.0.0.tar.gz differ diff --git a/dist/devranta-1.1.0-py3-none-any.whl b/dist/devranta-1.1.0-py3-none-any.whl new file mode 100644 index 0000000..f8dc617 Binary files /dev/null and b/dist/devranta-1.1.0-py3-none-any.whl differ diff --git a/dist/devranta-1.1.0.tar.gz b/dist/devranta-1.1.0.tar.gz new file mode 100644 index 0000000..f2c80df Binary files /dev/null and b/dist/devranta-1.1.0.tar.gz differ diff --git a/examples/crawler/Makefile b/examples/crawler/Makefile new file mode 100644 index 0000000..2c74caf --- /dev/null +++ b/examples/crawler/Makefile @@ -0,0 +1,17 @@ +.PHONY: all env install run clean + +all: env install run + +env: + python3 -m venv .venv + . + +install: + . .venv/bin/activate && pip install -r requirements.txt + . .venv/bin/activate && pip install -e ../../. + +run: + . .venv/bin/activate && python main.py + +clean: + rm -rf .venv diff --git a/examples/crawler/README.md b/examples/crawler/README.md new file mode 100644 index 0000000..d6e85ac --- /dev/null +++ b/examples/crawler/README.md @@ -0,0 +1,34 @@ +# Example Crawler Project + +This is a simple example crawler project. Follow the instructions below to set up and run the crawler. + +## Setup + +1. Clone the repository or copy the project files to your local machine. +2. Make sure you have Python 3 installed. + +## Usage + +1. Open a terminal in the project directory. +2. Run `make` to set up the environment, install dependencies, and start the crawler: + +```bash +make +``` + +This will create a virtual environment, install the package in editable mode from the parent directory, and run the main script. + +## Cleanup + +To remove the virtual environment, run: + +```bash +make clean +``` + +## Notes + +- The project installs the package with `-e ../../.` to include the parent package `devranta` in editable mode. +- Ensure that the parent package is correctly set up in the directory structure. + +Happy crawling! \ No newline at end of file diff --git a/examples/crawler/crawler.py b/examples/crawler/crawler.py new file mode 100644 index 0000000..cfa39d8 --- /dev/null +++ b/examples/crawler/crawler.py @@ -0,0 +1,214 @@ +import asyncio +import logging +from typing import Set +from devranta.api import Api, Rant +from database import DatabaseManager + +class DevRantCrawler: + def __init__(self, api: Api, db: DatabaseManager, rant_consumers: int, user_consumers: int): + self.api = api + self.db = db + self.rant_queue = asyncio.Queue(maxsize=1000000) + self.user_queue = asyncio.Queue(maxsize=1000000) + self.shutdown_event = asyncio.Event() + + self.num_rant_consumers = rant_consumers + self.num_user_consumers = user_consumers + + self.seen_rant_ids: Set[int] = set() + self.seen_user_ids: Set[int] = set() + self.stats = { + "rants_processed": 0, "rants_added_to_db": 0, + "comments_added_to_db": 0, "users_processed": 0, "users_added_to_db": 0, + "api_errors": 0, "producer_loops": 0, "end_of_feed_hits": 0, + "rants_queued": 0, "users_queued": 0 + } + + async def _queue_user_if_new(self, user_id: int): + if user_id in self.seen_user_ids: + return + + self.seen_user_ids.add(user_id) + if not await self.db.user_exists(user_id): + await self.user_queue.put(user_id) + self.stats["users_queued"] += 1 + + async def _queue_rant_if_new(self, rant_obj: Rant): + rant_id = rant_obj['id'] + if rant_id in self.seen_rant_ids: + return + + self.seen_rant_ids.add(rant_id) + if not await self.db.rant_exists(rant_id): + await self.db.add_rant(rant_obj) + self.stats["rants_added_to_db"] += 1 + await self.rant_queue.put(rant_id) + self.stats["rants_queued"] += 1 + + async def _initial_seed(self): + logging.info("Starting initial seeder to re-ignite crawling process...") + user_ids = await self.db.get_random_user_ids(limit=2000) + if not user_ids: + logging.info("Seeder found no existing users. Crawler will start from scratch.") + return + + for user_id in user_ids: + if user_id not in self.seen_user_ids: + self.seen_user_ids.add(user_id) + await self.user_queue.put(user_id) + self.stats["users_queued"] += 1 + logging.info(f"Seeder finished: Queued {len(user_ids)} users to kickstart exploration.") + + async def _rant_producer(self): + logging.info("Rant producer started.") + skip = 0 + consecutive_empty_responses = 0 + + while not self.shutdown_event.is_set(): + try: + logging.info(f"Producer: Fetching rants with skip={skip}...") + rants = await self.api.get_rants(sort="recent", limit=50, skip=skip) + self.stats["producer_loops"] += 1 + + if not rants: + consecutive_empty_responses += 1 + logging.info(f"Producer: Feed returned empty. Consecutive empty hits: {consecutive_empty_responses}.") + if consecutive_empty_responses >= 5: + self.stats["end_of_feed_hits"] += 1 + logging.info("Producer: End of feed likely reached. Pausing for 15 minutes before reset.") + await asyncio.sleep(900) + skip = 0 + consecutive_empty_responses = 0 + else: + await asyncio.sleep(10) + continue + + consecutive_empty_responses = 0 + new_rants_found = 0 + for rant in rants: + await self._queue_rant_if_new(rant) + new_rants_found += 1 + + logging.info(f"Producer: Processed {new_rants_found} rants from feed. Total queued: {self.stats['rants_queued']}.") + skip += len(rants) + await asyncio.sleep(2) + + except Exception as e: + logging.critical(f"Producer: Unhandled exception: {e}. Retrying in 60s.") + self.stats["api_errors"] += 1 + await asyncio.sleep(60) + + async def _rant_consumer(self, worker_id: int): + logging.info(f"Rant consumer #{worker_id} started.") + while not self.shutdown_event.is_set(): + try: + rant_id = await self.rant_queue.get() + logging.info(f"Rant consumer #{worker_id}: Processing rant ID {rant_id}.") + + rant_details = await self.api.get_rant(rant_id) + if not rant_details or not rant_details.get("success"): + logging.warning(f"Rant consumer #{worker_id}: Failed to fetch details for rant {rant_id}.") + self.rant_queue.task_done() + continue + + await self._queue_user_if_new(rant_details['rant']['user_id']) + + comments = rant_details.get("comments", []) + for comment in comments: + await self.db.add_comment(comment) + self.stats["comments_added_to_db"] += 1 + await self._queue_user_if_new(comment['user_id']) + + logging.info(f"Rant consumer #{worker_id}: Finished processing rant {rant_id}, found {len(comments)} comments.") + self.stats["rants_processed"] += 1 + self.rant_queue.task_done() + + except Exception as e: + logging.error(f"Rant consumer #{worker_id}: Unhandled exception: {e}") + self.rant_queue.task_done() + + async def _user_consumer(self, worker_id: int): + logging.info(f"User consumer #{worker_id} started.") + while not self.shutdown_event.is_set(): + try: + user_id = await self.user_queue.get() + logging.info(f"User consumer #{worker_id}: Processing user ID {user_id}.") + + profile = await self.api.get_profile(user_id) + if not profile: + logging.warning(f"User consumer #{worker_id}: Could not fetch profile for user {user_id}.") + self.user_queue.task_done() + continue + + await self.db.add_user(profile, user_id) + self.stats["users_added_to_db"] += 1 + + rants_found_on_profile = 0 + content_sections = profile.get("content", {}).get("content", {}) + for section_name in ["rants", "upvoted", "favorites"]: + for rant_obj in content_sections.get(section_name, []): + await self._queue_rant_if_new(rant_obj) + rants_found_on_profile += 1 + + logging.info(f"User consumer #{worker_id}: Finished user {user_id}, found and queued {rants_found_on_profile} associated rants.") + self.stats["users_processed"] += 1 + self.user_queue.task_done() + except Exception as e: + logging.error(f"User consumer #{worker_id}: Unhandled exception: {e}") + self.user_queue.task_done() + + async def _stats_reporter(self): + logging.info("Stats reporter started.") + while not self.shutdown_event.is_set(): + await asyncio.sleep(15) + logging.info( + f"[STATS] Rants Q'd/Proc: {self.stats['rants_queued']}/{self.stats['rants_processed']} | " + f"Users Q'd/Proc: {self.stats['users_queued']}/{self.stats['users_processed']} | " + f"Comments DB: {self.stats['comments_added_to_db']} | " + f"Queues (R/U): {self.rant_queue.qsize()}/{self.user_queue.qsize()} | " + f"API Errors: {self.stats['api_errors']}" + ) + + async def run(self): + logging.info("Exhaustive crawler starting...") + await self._initial_seed() + + logging.info("Starting main producer and consumer tasks...") + tasks = [] + try: + tasks.append(asyncio.create_task(self._rant_producer())) + tasks.append(asyncio.create_task(self._stats_reporter())) + + for i in range(self.num_rant_consumers): + tasks.append(asyncio.create_task(self._rant_consumer(i + 1))) + + for i in range(self.num_user_consumers): + tasks.append(asyncio.create_task(self._user_consumer(i + 1))) + + await asyncio.gather(*tasks, return_exceptions=True) + except asyncio.CancelledError: + logging.info("Crawler run cancelled.") + finally: + await self.shutdown() + + async def shutdown(self): + if self.shutdown_event.is_set(): + return + logging.info("Shutting down... sending signal to all tasks.") + self.shutdown_event.set() + + logging.info("Waiting for queues to empty... Press Ctrl+C again to force exit.") + try: + await asyncio.wait_for(self.rant_queue.join(), timeout=30) + await asyncio.wait_for(self.user_queue.join(), timeout=30) + except (asyncio.TimeoutError, asyncio.CancelledError): + logging.warning("Could not empty queues in time, proceeding with shutdown.") + + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + for task in tasks: + task.cancel() + + await asyncio.gather(*tasks, return_exceptions=True) + logging.info("All tasks cancelled.") + logging.info(f"--- FINAL STATS ---\n{self.stats}") + diff --git a/examples/crawler/database.py b/examples/crawler/database.py new file mode 100644 index 0000000..f484ffe --- /dev/null +++ b/examples/crawler/database.py @@ -0,0 +1,96 @@ +import logging +import aiosqlite +from typing import List +from devranta.api import Rant, Comment, UserProfile + +class DatabaseManager: + def __init__(self, db_path: str): + self.db_path = db_path + self._conn: aiosqlite.Connection | None = None + + async def __aenter__(self): + logging.info(f"Connecting to database at {self.db_path}...") + self._conn = await aiosqlite.connect(self.db_path) + await self._conn.execute("PRAGMA journal_mode=WAL;") + await self._conn.execute("PRAGMA foreign_keys=ON;") + await self.create_tables() + logging.info("Database connection successful.") + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self._conn: + await self._conn.close() + logging.info("Database connection closed.") + + async def create_tables(self): + logging.info("Ensuring database tables exist...") + await self._conn.executescript(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + score INTEGER, + about TEXT, + location TEXT, + created_time INTEGER, + skills TEXT, + github TEXT, + website TEXT + ); + CREATE TABLE IF NOT EXISTS rants ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + text TEXT, + score INTEGER, + created_time INTEGER, + num_comments INTEGER + ); + CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY, + rant_id INTEGER, + user_id INTEGER, + body TEXT, + score INTEGER, + created_time INTEGER + ); + """) + await self._conn.commit() + logging.info("Table schema verified.") + + async def add_rant(self, rant: Rant): + await self._conn.execute( + "INSERT OR IGNORE INTO rants (id, user_id, text, score, created_time, num_comments) VALUES (?, ?, ?, ?, ?, ?)", + (rant['id'], rant['user_id'], rant['text'], rant['score'], rant['created_time'], rant['num_comments']) + ) + await self._conn.commit() + + async def add_comment(self, comment: Comment): + await self._conn.execute( + "INSERT OR IGNORE INTO comments (id, rant_id, user_id, body, score, created_time) VALUES (?, ?, ?, ?, ?, ?)", + (comment['id'], comment['rant_id'], comment['user_id'], comment['body'], comment['score'], comment['created_time']) + ) + await self._conn.commit() + + async def add_user(self, user: UserProfile, user_id: int): + await self._conn.execute( + "INSERT OR IGNORE INTO users (id, username, score, about, location, created_time, skills, github, website) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + (user_id, user['username'], user['score'], user['about'], user['location'], user['created_time'], user['skills'], user['github'], user['website']) + ) + await self._conn.commit() + + async def rant_exists(self, rant_id: int) -> bool: + async with self._conn.execute("SELECT 1 FROM rants WHERE id = ? LIMIT 1", (rant_id,)) as cursor: + return await cursor.fetchone() is not None + + async def user_exists(self, user_id: int) -> bool: + async with self._conn.execute("SELECT 1 FROM users WHERE id = ? LIMIT 1", (user_id,)) as cursor: + return await cursor.fetchone() is not None + + async def get_random_user_ids(self, limit: int) -> List[int]: + logging.info(f"Fetching up to {limit} random user IDs from database for seeding...") + query = "SELECT id FROM users ORDER BY RANDOM() LIMIT ?" + async with self._conn.execute(query, (limit,)) as cursor: + rows = await cursor.fetchall() + user_ids = [row[0] for row in rows] + logging.info(f"Found {len(user_ids)} user IDs to seed.") + return user_ids + diff --git a/examples/crawler/main.py b/examples/crawler/main.py new file mode 100644 index 0000000..6a60285 --- /dev/null +++ b/examples/crawler/main.py @@ -0,0 +1,46 @@ +# main.py +import asyncio +import logging +import signal + +from devranta.api import Api +from database import DatabaseManager +from crawler import DevRantCrawler + +# --- Configuration --- +DB_FILE = "devrant.sqlite" +CONCURRENT_RANT_CONSUMERS = 10 # How many rants to process at once +CONCURRENT_USER_CONSUMERS = 5 # How many user profiles to fetch at once + +async def main(): + """Initializes and runs the crawler.""" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + api = Api() + + async with DatabaseManager(DB_FILE) as db: + crawler = DevRantCrawler( + api=api, + db=db, + rant_consumers=CONCURRENT_RANT_CONSUMERS, + user_consumers=CONCURRENT_USER_CONSUMERS + ) + + # Set up a signal handler for graceful shutdown on Ctrl+C + loop = asyncio.get_running_loop() + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler( + sig, lambda s=sig: asyncio.create_task(crawler.shutdown()) + ) + + await crawler.run() + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Main loop interrupted. Exiting.") diff --git a/examples/crawler/requirements.txt b/examples/crawler/requirements.txt new file mode 100644 index 0000000..efa2b38 --- /dev/null +++ b/examples/crawler/requirements.txt @@ -0,0 +1 @@ +aiosqlite diff --git a/setup.cfg b/setup.cfg index f2a2726..1721da4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = devranta -version = 1.0.0 +version = 1.1.0 description = Async devRant API client made with aiohttp. author = retoor author_email = retoor@molodetz.nl diff --git a/src/devranta.egg-info/PKG-INFO b/src/devranta.egg-info/PKG-INFO index b750c1a..6317f0d 100644 --- a/src/devranta.egg-info/PKG-INFO +++ b/src/devranta.egg-info/PKG-INFO @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: devranta -Version: 1.0.0 +Version: 1.1.0 Summary: Async devRant API client made with aiohttp. Author: retoor Author-email: retoor@molodetz.nl @@ -13,10 +13,7 @@ Requires-Dist: dataset # devRanta -devRanta is an async devrant client written in and for Python. -Authentication is only needed for half of the functionality and thus username and password are optional parameters by constructing the main class of this package (Api). - -You can find last packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages). +devRanta is the best async devRant client written in Python. Authentication is only needed for half of the functionality; thus, the username and password are optional parameters when constructing the main class of this package (Api). You can find the latest packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages). ## Running ``` @@ -44,3 +41,274 @@ async def list_rants(): See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use. +# devRant API Documentation + +For people wanting to build their own client. + +TODO: document responses. + +## Base URL +`https://devrant.com/api` + +## Authentication +- Uses `dr_token` cookie with `token_id`, `token_key`, and `user_id`. +- Required for endpoints needing user authentication. +- `guid`, `plat`, `sid`, `seid` included in requests for session tracking. + +## Endpoints + +### User Management +1. **Register User** + - **URL**: `/api/users` + - **Method**: POST + - **Parameters**: + - `app`: 3 (constant) + - `type`: 1 (constant) + - `email`: User email + - `username`: User username + - `password`: User password + - `guid`: Unique identifier (from `getMyGuid`) + - `plat`: 3 (constant) + - `sid`: Session start time (from `getSessionStartTime`) + - `seid`: Session event ID (from `getSessionEventId`) + - **Response**: JSON with `success`, `auth_token`, or `error` and `error_field` + - **Description**: Creates a new user account. + +2. **Login User** + - **URL**: `/api/users/auth-token` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `username`: User username + - `password`: User password + - `guid`: Unique identifier + - `plat`: 3 + - `sid`: Session start time + - `seid`: Session event ID + - **Response**: JSON with `success`, `auth_token`, or `error` + - **Description**: Authenticates user and returns auth token. + +3. **Edit Profile** + - **URL**: `/api/users/me/edit-profile` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `token_id`: Token ID + - `token_key`: Token key + - `user_id`: User ID + - `guid`, `plat`, `sid`, `seid` + - `profile_about`: User bio + - `profile_skills`: User skills + - `profile_location`: User location + - `profile_website`: User website + - `profile_github`: GitHub username + - **Response**: JSON with `success` + - **Description**: Updates user profile information. + +4. **Forgot Password** + - **URL**: `/api/users/forgot-password` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `username`: User username + - `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Initiates password reset process. + +5. **Resend Confirmation Email** + - **URL**: `/api/users/me/resend-confirm` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Resends account confirmation email. + +6. **Delete Account** + - **URL**: `/api/users/me` + - **Method**: DELETE + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Deletes user account. + +7. **Mark News as Read** + - **URL**: `/api/users/me/mark-news-read` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `news_id`: News item ID + - **Response**: JSON with `success` + - **Description**: Marks a news item as read for logged-in users. + +### Rants +1. **Get Rant** + - **URL**: `/api/devrant/rants/{rant_id}` + - **Method**: GET + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `last_comment_id`: 999999999999 (optional) + - `links`: 0 (optional) + - **Response**: JSON with `rant` (text, tags) + - **Description**: Retrieves a specific rant by ID. + +2. **Post Rant** + - **URL**: `/api/devrant/rants` + - **Method**: POST + - **Parameters** (FormData): + - `app`: 3 + - `rant`: Rant text + - `tags`: Comma-separated tags + - `token_id`, `token_key`, `user_id` + - `type`: Rant type ID + - `image`: Optional image file (img/gif) + - **Response**: JSON with `success`, `rant_id`, or `error` + - **Description**: Creates a new rant. + +3. **Edit Rant** + - **URL**: `/api/devrant/rants/{rant_id}` + - **Method**: POST + - **Parameters** (FormData): + - `app`: 3 + - `rant`: Rant text + - `tags`: Comma-separated tags + - `token_id`, `token_key`, `user_id` + - `image`: Optional image file + - **Response**: JSON with `success` or `fail_reason` + - **Description**: Updates an existing rant. + +4. **Delete Rant** + - **URL**: `/api/devrant/rants/{rant_id}` + - **Method**: DELETE + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Deletes a rant. + +5. **Vote on Rant** + - **URL**: `/api/devrant/rants/{rant_id}/vote` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `vote`: 1 (upvote), -1 (downvote), 0 (remove vote) + - `reason`: Downvote reason ID (required for downvote) + - **Response**: JSON with `success` or `confirmed` (false if unverified) + - **Description**: Votes on a rant. + +6. **Favorite/Unfavorite Rant** + - **URL**: `/api/devrant/rants/{rant_id}/{favorite|unfavorite}` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Favorites or unfavorites a rant. + +7. **Get Rant Feed** + - **URL**: `/api/devrant/rants` + - **Method**: GET + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `ids`: JSON string of IDs (optional) + - **Response**: JSON with `success`, `num_notifs` + - **Description**: Retrieves rant feed with notification count. + +### Comments +1. **Get Comment** + - **URL**: `/api/comments/{comment_id}` + - **Method**: GET + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `links`: 0 (optional) + - **Response**: JSON with `comment` (body) + - **Description**: Retrieves a specific comment by ID. + +2. **Post Comment** + - **URL**: `/api/devrant/rants/{rant_id}/comments` + - **Method**: POST + - **Parameters** (FormData): + - `app`: 3 + - `comment`: Comment text + - `token_id`, `token_key`, `user_id` + - `image`: Optional image file (img/gif) + - **Response**: JSON with `success` or `confirmed` (false if unverified) + - **Description**: Posts a comment on a rant. + +3. **Edit Comment** + - **URL**: `/api/comments/{comment_id}` + - **Method**: POST + - **Parameters** (FormData): + - `app`: 3 + - `comment`: Comment text + - `token_id`, `token_key`, `user_id` + - **Response**: JSON with `success` or `fail_reason` + - **Description**: Updates an existing comment. + +4. **Delete Comment** + - **URL**: `/api/comments/{comment_id}` + - **Method**: DELETE + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Deletes a comment. + +5. **Vote on Comment** + - **URL**: `/api/comments/{comment_id}/vote` + - **Method**: POST + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `vote`: 1 (upvote), -1 (downvote), 0 (remove vote) + - `reason`: Downvote reason ID (required for downvote) + - **Response**: JSON with `success` or `confirmed` (false if unverified) + - **Description**: Votes on a comment. + +### Notifications +1. **Get Notification Feed** + - **URL**: `/api/users/me/notif-feed` + - **Method**: GET + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - `ext_prof`: 1 (optional) + - `last_time`: Last notification check time + - **Response**: JSON with `success`, `data` (items, check_time, username_map) + - **Description**: Retrieves user notifications. + +2. **Clear Notifications** + - **URL**: `/api/users/me/notif-feed` + - **Method**: DELETE + - **Parameters**: + - `app`: 3 + - `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid` + - **Response**: JSON with `success` + - **Description**: Clears user notifications. + +## External API +- **Beta List Signup** + - **URL**: `https://www.hexicallabs.com/api/beta-list` + - **Method**: GET (JSONP) + - **Parameters**: + - `email`: User email + - `platform`: Platform name + - `app`: 3 + - **Description**: Signs up user for beta list (external service). + +## Notes +- All endpoints expect `app=3` for identification. +- Authenticated endpoints require `dr_token` cookie with `token_id`, `token_key`, `user_id`. +- `guid`, `plat`, `sid`, `seid` are used for session tracking. +- Image uploads use FormData for rants and comments. +- Downvotes require a reason ID, prompting a modal if not provided. +- Responses typically include `success` boolean; errors include `error` or `fail_reason`. +- Cookies (`dr_token`, `dr_guid`, `dr_session_start`, `dr_event_id`, `dr_feed_sort`, `dr_theme`, `dr_rants_viewed`, `dr_stickers_seen`, `rant_type_filters`, `news_seen`) manage state and preferences. + + diff --git a/src/devranta.egg-info/SOURCES.txt b/src/devranta.egg-info/SOURCES.txt index e880c53..900d079 100644 --- a/src/devranta.egg-info/SOURCES.txt +++ b/src/devranta.egg-info/SOURCES.txt @@ -4,6 +4,8 @@ setup.cfg src/devranta/__init__.py src/devranta/__main__.py src/devranta/api.py +src/devranta/api_plain.py +src/devranta/api_requests.py src/devranta/tests.py src/devranta.egg-info/PKG-INFO src/devranta.egg-info/SOURCES.txt diff --git a/src/devranta/api.py b/src/devranta/api.py index a3cdf63..76e74a5 100644 --- a/src/devranta/api.py +++ b/src/devranta/api.py @@ -29,8 +29,8 @@ class Image(TypedDict): height: int class UserAvatar(TypedDict): - b: str # background color - i: NotRequired[str] # image identifier + b: str # background color + i: Optional[str] # image identifier class Rant(TypedDict): id: int @@ -81,7 +81,7 @@ class Notification(TypedDict): comment_id: int created_time: int read: int - uid: int # User ID of the notifier + uid: int # User ID of the notifier username: str # --- API Class --- @@ -162,7 +162,7 @@ class Api: """ if not self.username or not self.password: raise Exception("No authentication details supplied.") - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.post( url=self.patch_url("users/auth-token"), data={ @@ -180,7 +180,7 @@ class Api: self.user_id = self.auth.get("user_id") self.token_id = self.auth.get("id") self.token_key = self.auth.get("key") - return bool(self.auth) + return bool(self.auth) async def ensure_login(self) -> bool: """Ensures the user is logged in before making a request.""" @@ -188,17 +188,6 @@ class Api: return await self.login() return True - async def __aenter__(self) -> aiohttp.ClientSession: - """Asynchronous context manager entry.""" - self.session = aiohttp.ClientSession() - return self.session - - async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Asynchronous context manager exit.""" - if self.session and not self.session.closed: - await self.session.close() - self.session = None - async def register_user(self, email: str, username: str, password: str) -> bool: """ Registers a new user. @@ -220,8 +209,7 @@ class Api: } ``` """ - response = None - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.post( url=self.patch_url(f"users"), data=self.patch_auth({ @@ -231,10 +219,8 @@ class Api: "plat": 3 }), ) - if not response: - return False - obj = await response.json() - return obj.get('success', False) + obj = await response.json() + return obj.get('success', False) async def get_comments_from_user(self, username: str) -> List[Comment]: """ @@ -244,8 +230,7 @@ class Api: username (str): The username of the user. Returns: - List[Comment]: A list of comment objects. The structure of each comment - is inferred from the general API design. + List[Comment]: A list of comment objects. """ user_id = await self.get_user_id(username) if not user_id: @@ -253,7 +238,6 @@ class Api: profile = await self.get_profile(user_id) if not profile: return [] - # The API nests content twice return profile.get("content", {}).get("content", {}).get("comments", []) async def post_comment(self, rant_id: int, comment: str) -> bool: @@ -269,13 +253,13 @@ class Api: """ if not await self.ensure_login(): return False - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.post( url=self.patch_url(f"devrant/rants/{rant_id}/comments"), data=self.patch_auth({"comment": comment, "plat": 2}), ) - obj = await response.json() - return obj.get("success", False) + obj = await response.json() + return obj.get("success", False) async def get_comment(self, id_: int) -> Optional[Comment]: """ @@ -287,16 +271,12 @@ class Api: Returns: Optional[Comment]: A dictionary representing the comment, or None if not found. """ - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( url=self.patch_url(f"comments/{id_}"), params=self.patch_auth() ) - obj = await response.json() - - if not obj.get("success"): - return None - - return obj.get("comment") + obj = await response.json() + return obj.get("comment") if obj.get("success") else None async def delete_comment(self, id_: int) -> bool: """ @@ -310,12 +290,12 @@ class Api: """ if not await self.ensure_login(): return False - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.delete( url=self.patch_url(f"comments/{id_}"), params=self.patch_auth() ) - obj = await response.json() - return obj.get("success", False) + obj = await response.json() + return obj.get("success", False) async def get_profile(self, id_: int) -> Optional[UserProfile]: """ @@ -326,38 +306,13 @@ class Api: Returns: Optional[UserProfile]: A dictionary with the user's profile data. - - Profile Structure: - ```json - { - "username": "string", - "score": int, - "about": "string", - "location": "string", - "created_time": int, - "skills": "string", - "github": "string", - "website": "string", - "avatar": { "b": "hex_color", "i": "image_id" }, - "content": { - "content": { - "rants": [ RantObject, ... ], - "upvoted": [ RantObject, ... ], - "comments": [ CommentObject, ... ], - "favorites": [ RantObject, ... ] - } - } - } - ``` """ - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( url=self.patch_url(f"users/{id_}"), params=self.patch_auth() ) - obj = await response.json() - if not obj.get("success"): - return None - return obj.get("profile") + obj = await response.json() + return obj.get("profile") if obj.get("success") else None async def search(self, term: str) -> List[Rant]: """ @@ -369,15 +324,13 @@ class Api: Returns: List[Rant]: A list of rant objects from the search results. """ - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( url=self.patch_url("devrant/search"), params=self.patch_auth({"term": term}), ) - obj = await response.json() - if not obj.get("success"): - return [] - return obj.get("results", []) + obj = await response.json() + return obj.get("results", []) if obj.get("success") else [] async def get_rant(self, id: int) -> Dict[str, Any]: """ @@ -388,23 +341,13 @@ class Api: Returns: Dict[str, Any]: The full API response object. - - Response Structure: - ```json - { - "rant": { RantObject }, - "comments": [ CommentObject, ... ], - "success": true, - "subscribed": 0 or 1 - } - ``` """ - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( self.patch_url(f"devrant/rants/{id}"), params=self.patch_auth(), ) - return await response.json() + return await response.json() async def get_rants(self, sort: str = "recent", limit: int = 20, skip: int = 0) -> List[Rant]: """ @@ -418,15 +361,13 @@ class Api: Returns: List[Rant]: A list of rant objects. """ - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( url=self.patch_url("devrant/rants"), params=self.patch_auth({"sort": sort, "limit": limit, "skip": skip}), ) - obj = await response.json() - if not obj.get("success"): - return [] - return obj.get("rants", []) + obj = await response.json() + return obj.get("rants", []) if obj.get("success") else [] async def get_user_id(self, username: str) -> Optional[int]: """ @@ -437,26 +378,15 @@ class Api: Returns: Optional[int]: The user's ID, or None if not found. - - Response Structure: - ```json - { - "success": true, - "user_id": int - } - ``` """ - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( url=self.patch_url("get-user-id"), params=self.patch_auth({"username": username}), ) - obj = await response.json() - if not obj.get("success"): - return None - return obj.get("user_id") + obj = await response.json() + return obj.get("user_id") if obj.get("success") else None - @property async def mentions(self) -> List[Notification]: """ Fetches notifications where the user was mentioned. @@ -464,7 +394,7 @@ class Api: Returns: List[Notification]: A list of mention notification objects. """ - notifications = await self.notifs + notifications = await self.notifs() return [ notif for notif in notifications if notif.get("type") == "comment_mention" ] @@ -482,13 +412,13 @@ class Api: """ if not await self.ensure_login(): return False - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.post( url=self.patch_url(f"comments/{comment_id}"), data=self.patch_auth({"comment": comment}), ) - obj = await response.json() - return obj.get("success", False) + obj = await response.json() + return obj.get("success", False) async def vote_rant(self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool: """ @@ -504,13 +434,13 @@ class Api: """ if not await self.ensure_login(): return False - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.post( url=self.patch_url(f"devrant/rants/{rant_id}/vote"), data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}), ) - obj = await response.json() - return obj.get("success", False) + obj = await response.json() + return obj.get("success", False) async def vote_comment(self, comment_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool: """ @@ -526,44 +456,27 @@ class Api: """ if not await self.ensure_login(): return False - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.post( url=self.patch_url(f"comments/{comment_id}/vote"), data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}), ) - obj = await response.json() - return obj.get("success", False) + obj = await response.json() + return obj.get("success", False) - @property async def notifs(self) -> List[Notification]: """ Fetches the user's notification feed. Returns: List[Notification]: A list of notification items. - - Response Structure: - ```json - { - "success": true, - "data": { - "items": [ NotificationObject, ... ], - "check_time": int, // Timestamp of the check - "username_map": [], // Deprecated or unused - "unread": { - "all": int, "upvotes": int, "mentions": int, - "comments": int, "subs": int, "total": int - }, - "num_unread": int - } - } - ``` """ if not await self.ensure_login(): return [] - async with self as session: + async with aiohttp.ClientSession() as session: response = await session.get( url=self.patch_url("users/me/notif-feed"), params=self.patch_auth() ) - obj = await response.json() - return obj.get("data", {}).get("items", []) + obj = await response.json() + return obj.get("data", {}).get("items", []) + diff --git a/test.py b/test.py new file mode 100644 index 0000000..efd66fb --- /dev/null +++ b/test.py @@ -0,0 +1,574 @@ +import requests +import json +import uuid +from datetime import datetime +from typing import Dict, Any, Optional, List + +# Configuration +BASE_URL: str = "https://devrant.com/api" +RESULTS_FILE: str = "api_test_results.json" +APP: int = 3 +PLAT: int = 3 +GUID: str = str(uuid.uuid4()) +SID: str = str(int(datetime.now().timestamp())) +SEID: str = str(uuid.uuid4()) + +# Real credentials for login (using provided username/email and password) +LOGIN_USERNAME: str = "power-to@the-puff.com" +LOGIN_PASSWORD: str = "powerpuffgirl" + +# Variables to store auth after login +AUTH_TOKEN_ID: Optional[str] = None +AUTH_TOKEN_KEY: Optional[str] = None +AUTH_USER_ID: Optional[str] = None + +# Mock/fallback values (overridden after login or fetch) +TEST_EMAIL: str = "test@example.com" +TEST_USERNAME: str = "testuser" + str(int(datetime.now().timestamp())) # Make unique for registration +TEST_PASSWORD: str = "Test1234!" +TEST_RANT_ID: str = "1" # Will be overridden with real one +TEST_COMMENT_ID: str = "1" # Will be overridden with real one +TEST_NEWS_ID: str = "1" # Assuming this might work; adjust if needed + +# Initialize results +results: List[Dict[str, Any]] = [] + +def save_results() -> None: + """Save the accumulated test results to JSON file.""" + with open(RESULTS_FILE, 'w') as f: + json.dump(results, f, indent=2) + +def test_endpoint(method: str, url: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]: + """ + Execute an API request and record the result. + + Payload: + - method: HTTP method (GET, POST, DELETE, etc.) + - url: Full API URL + - params: Query parameters (dict) + - data: POST/PUT body (dict) + - files: Multipart files (dict) + - headers: Custom headers (dict) + + Response: + - Returns a dict with url, method, status_code, response (JSON or error), headers, request_body, timestamp + """ + try: + response = requests.request(method, url, params=params, data=data, files=files, headers=headers) + result: Dict[str, Any] = { + "url": response.url, + "method": method, + "status_code": response.status_code, + "response": response.json() if response.content else {}, + "headers": dict(response.headers), + "request_body": data or params or {}, + "timestamp": datetime.now().isoformat() + } + results.append(result) + return result + except Exception as e: + result: Dict[str, Any] = { + "url": url, + "method": method, + "status_code": None, + "response": {"error": str(e)}, + "headers": {}, + "request_body": data or params or {}, + "timestamp": datetime.now().isoformat() + } + results.append(result) + return result + +# Helper to patch auth into params/data +def patch_auth(base_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Add authentication to request dict if available. + + Payload: base_dict (params or data) + Response: Updated dict with app, user_id, token_id, token_key if auth present + """ + auth_dict: Dict[str, Any] = {"app": APP} + if AUTH_USER_ID and AUTH_TOKEN_ID and AUTH_TOKEN_KEY: + auth_dict.update({ + "user_id": AUTH_USER_ID, + "token_id": AUTH_TOKEN_ID, + "token_key": AUTH_TOKEN_KEY + }) + base_dict.update(auth_dict) + return base_dict + +# Login function to get real auth tokens +def login_user() -> bool: + """ + Perform login to obtain real auth tokens. + + Payload: POST to /users/auth-token with username, password, app + Response: success (bool), sets global AUTH_* variables on success + """ + params: Dict[str, Any] = { + "username": LOGIN_USERNAME, + "password": LOGIN_PASSWORD, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + result = test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params)) + if result["status_code"] == 200 and result.get("response", {}).get("success"): + auth_token = result["response"].get("auth_token", {}) + global AUTH_USER_ID, AUTH_TOKEN_ID, AUTH_TOKEN_KEY + AUTH_USER_ID = str(auth_token.get("user_id", "")) + AUTH_TOKEN_ID = str(auth_token.get("id", "")) + AUTH_TOKEN_KEY = auth_token.get("key", "") + return True + return False + +# Fetch a real rant_id from feed +def fetch_real_rant_id() -> Optional[str]: + """ + Fetch rants feed to get a real rant_id. + + Payload: GET to /devrant/rants with auth + Response: First rant_id if success, else None + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + result = test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params)) + if result["status_code"] == 200 and result.get("response", {}).get("success"): + rants = result["response"].get("rants", []) + if rants: + return str(rants[0]["id"]) + return None + +# Post a test rant and return its id +def post_test_rant() -> Optional[str]: + """ + Post a test rant to get a real rant_id for further tests. + + Payload: POST to /devrant/rants with rant content, tags, auth + Response: rant_id if success, else None + """ + data: Dict[str, Any] = { + "rant": "Test rant for API testing (ignore)", + "tags": "test,api", + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + result = test_endpoint("POST", f"{BASE_URL}/devrant/rants", data=patch_auth(data)) + if result["status_code"] == 200 and result.get("response", {}).get("success"): + return str(result["response"].get("rant_id", "")) + return None + +# Post a test comment and return its id +def post_test_comment(rant_id: str) -> Optional[str]: + """ + Post a test comment to get a real comment_id. + + Payload: POST to /devrant/rants/{rant_id}/comments with comment, auth + Response: comment_id if success, else None + """ + data: Dict[str, Any] = { + "comment": "Test comment for API testing (ignore)", + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + result = test_endpoint("POST", f"{BASE_URL}/devrant/rants/{rant_id}/comments", data=patch_auth(data)) + if result["status_code"] == 200 and result.get("response", {}).get("success"): + return str(result["response"].get("comment_id", "")) + return None + +# Test cases with docstrings + +def test_register_user() -> None: + """ + Test user registration (valid and invalid). + + Payload (valid): POST /users with email, username, password, type=1, plat, guid, sid, seid, app + Expected: success=true if unique, else error + + Payload (invalid): Missing email + Expected: error on email field + """ + params: Dict[str, Any] = { + "type": 1, + "email": TEST_EMAIL, + "username": TEST_USERNAME, + "password": TEST_PASSWORD, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(params.copy())) + invalid_params = params.copy() + del invalid_params["email"] + test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(invalid_params)) + +def test_login_user() -> None: + """ + Test user login (valid and invalid). Already done in login_user(), but record here. + + Payload (valid): POST /users/auth-token with username, password, plat, guid, sid, seid, app + Expected: success=true, auth_token + + Payload (invalid): Wrong password + Expected: error=Invalid login credentials + """ + # Valid is handled in login_user(); here test invalid + params: Dict[str, Any] = { + "username": TEST_USERNAME, + "password": "WrongPass", + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params)) + +def test_edit_profile() -> None: + """ + Test editing user profile. + + Payload: POST /users/me/edit-profile with profile fields, plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "profile_about": "Test bio", + "profile_skills": "Python, JS", + "profile_location": "Test City", + "profile_website": "http://example.com", + "profile_github": "testuser", + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/users/me/edit-profile", data=patch_auth(params)) + +def test_forgot_password() -> None: + """ + Test forgot password. + + Payload: POST /users/forgot-password with username, plat, guid, sid, seid, app + Expected: success=true (even without auth) + """ + params: Dict[str, Any] = { + "username": LOGIN_USERNAME, # Use real one + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/users/forgot-password", data=patch_auth(params)) + +def test_resend_confirm() -> None: + """ + Test resend confirmation email. + + Payload: POST /users/me/resend-confirm with plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/users/me/resend-confirm", data=patch_auth(params)) + +def test_delete_account() -> None: + """ + Test delete account (caution: irreversible). + + Payload: DELETE /users/me with plat, guid, sid, seid, auth + Expected: success=true + """ + # Comment out to avoid accidental deletion + # params: Dict[str, Any] = { + # "plat": PLAT, + # "guid": GUID, + # "sid": SID, + # "seid": SEID + # } + # test_endpoint("DELETE", f"{BASE_URL}/users/me", params=patch_auth(params)) + pass + +def test_mark_news_read() -> None: + """ + Test mark news as read. + + Payload: POST /users/me/mark-news-read with news_id, plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "news_id": TEST_NEWS_ID, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/users/me/mark-news-read", data=patch_auth(params)) + +def test_get_rant() -> None: + """ + Test get single rant. + + Payload: GET /devrant/rants/{rant_id} with last_comment_id, links, plat, guid, sid, seid, auth + Expected: success=true, rant details + """ + params: Dict[str, Any] = { + "last_comment_id": "999999999999", + "links": 0, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("GET", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params)) + +def test_post_rant() -> None: + """ + Test post new rant. (Already done in post_test_rant for id) + + Payload: POST /devrant/rants with rant, tags, auth + Expected: success=true, rant_id + """ + # Handled in setup + pass + +def test_edit_rant() -> None: + """ + Test edit rant. + + Payload: POST /devrant/rants/{rant_id} with updated rant, tags, auth + Expected: success=true + """ + data: Dict[str, Any] = { + "rant": "Updated test rant", + "tags": "test,python,update" + } + test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", data=patch_auth(data)) + +def test_delete_rant() -> None: + """ + Test delete rant. + + Payload: DELETE /devrant/rants/{rant_id} with plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("DELETE", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params)) + +def test_vote_rant() -> None: + """ + Test vote on rant (upvote and downvote with reason). + + Payload: POST /devrant/rants/{rant_id}/vote with vote (1/-1), reason (optional), plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "vote": 1, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params)) + params["vote"] = -1 + params["reason"] = "1" + test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params)) + +def test_favorite_rant() -> None: + """ + Test favorite/unfavorite rant. + + Payload: POST /devrant/rants/{rant_id}/favorite or /unfavorite with plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/favorite", data=patch_auth(params)) + test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/unfavorite", data=patch_auth(params)) + +def test_get_rant_feed() -> None: + """ + Test get rant feed. + + Payload: GET /devrant/rants with plat, guid, sid, seid, auth + Expected: success=true, list of rants + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params)) + +def test_get_comment() -> None: + """ + Test get single comment. + + Payload: GET /comments/{comment_id} with links, plat, guid, sid, seid, auth + Expected: success=true, comment details + """ + params: Dict[str, Any] = { + "links": 0, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("GET", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params)) + +def test_post_comment() -> None: + """ + Test post comment. (Handled in post_test_comment for id) + + Payload: POST /devrant/rants/{rant_id}/comments with comment, auth + Expected: success=true, comment_id + """ + # Handled in setup + pass + +def test_edit_comment() -> None: + """ + Test edit comment. + + Payload: POST /comments/{comment_id} with updated comment, auth + Expected: success=true + """ + data: Dict[str, Any] = { + "comment": "Updated test comment" + } + test_endpoint("POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", data=patch_auth(data)) + +def test_delete_comment() -> None: + """ + Test delete comment. + + Payload: DELETE /comments/{comment_id} with plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("DELETE", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params)) + +def test_vote_comment() -> None: + """ + Test vote on comment. + + Payload: POST /comments/{comment_id}/vote with vote (1), plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "vote": 1, + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}/vote", data=patch_auth(params)) + +def test_get_notif_feed() -> None: + """ + Test get notification feed. + + Payload: GET /users/me/notif-feed with ext_prof, last_time, plat, guid, sid, seid, auth + Expected: success=true, notifications + """ + params: Dict[str, Any] = { + "ext_prof": 1, + "last_time": str(int(datetime.now().timestamp())), + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("GET", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params)) + +def test_clear_notifications() -> None: + """ + Test clear notifications. + + Payload: DELETE /users/me/notif-feed with plat, guid, sid, seid, auth + Expected: success=true + """ + params: Dict[str, Any] = { + "plat": PLAT, + "guid": GUID, + "sid": SID, + "seid": SEID + } + test_endpoint("DELETE", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params)) + +def test_beta_list_signup() -> None: + """ + Test beta list signup (external API). + + Payload: GET https://www.hexicallabs.com/api/beta-list with email, platform, app + Expected: Whatever the API returns (may not be JSON) + """ + params: Dict[str, Any] = { + "email": TEST_EMAIL, + "platform": "test_platform" + } + test_endpoint("GET", "https://www.hexicallabs.com/api/beta-list", params=patch_auth(params)) + + +def main() -> None: + # Setup: Login and fetch real IDs + if login_user(): + global TEST_RANT_ID + TEST_RANT_ID = post_test_rant() or fetch_real_rant_id() or "1" + global TEST_COMMENT_ID + TEST_COMMENT_ID = post_test_comment(TEST_RANT_ID) or "1" + + test_register_user() + test_login_user() + test_edit_profile() + test_forgot_password() + test_resend_confirm() + test_mark_news_read() + test_get_rant() + test_post_rant() + test_edit_rant() + test_vote_rant() + test_favorite_rant() + test_get_rant_feed() + test_get_comment() + test_post_comment() + test_edit_comment() + test_delete_comment() + test_vote_comment() + test_get_notif_feed() + test_clear_notifications() + test_beta_list_signup() + test_delete_rant() + + test_delete_account() + save_results() + +if __name__ == "__main__": + main()