From 4fbe73455354c30e4e7e9d3f4a61dd0453671d04 Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Thu, 2 Jul 2026 13:02:02 +0100 Subject: [PATCH 1/6] git ignore --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index e43b0f98..a0f3a019 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,11 @@ .DS_Store +# Virtual environments +venv/ +.venv/ +env/ +.env/ +ENV/ + +# Python cache files +__pycache__/ +*.pyc From 3ab09c2ddfd6f49a1ed7af90cdad417657313658 Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Thu, 2 Jul 2026 14:48:43 +0100 Subject: [PATCH 2/6] db rebloom --- backend/data/blooms.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf3..3c6d23b0 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -15,6 +15,13 @@ class Bloom: sent_timestamp: datetime.datetime +class Rebloom: + id: int + resender: User + sender: User + content: str + sent_timestamp: datetime.datetime + def add_bloom(*, sender: User, content: str) -> Bloom: hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] @@ -37,6 +44,20 @@ def add_bloom(*, sender: User, content: str) -> Bloom: ) +def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str) -> Rebloom: + + with db_cursor() as cur: + cur.execute( + "INSERT INTO reblooms (id, resender_id, original_sender_id, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_id)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_id, content) DO UPDATE SET times_rebloomed = times_rebloomed + 1", + dict( + rebloom_id=rebloom_id, + resender_id=resender.id, + sender_id=sender.id, + content=content, + timestamp=datetime.datetime.now(datetime.UTC), + ), + ) + def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None ) -> List[Bloom]: From 97509443058a38cbb7552e58bfe8b17fe0c39478 Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Thu, 2 Jul 2026 14:56:33 +0100 Subject: [PATCH 3/6] rebloom-endpoint --- backend/endpoints.py | 20 ++++++++++++++++++++ backend/main.py | 2 ++ 2 files changed, 22 insertions(+) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a07..bd805204 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -166,6 +166,26 @@ def send_bloom(): } ) +@jwt_required() +def send_rebloom(): + type_check_error = verify_request_fields({"content": str}) + if type_check_error is not None: + return type_check_error + + user = get_current_user() + + if user.username == request.json["sender"]: + return make_response((f"Cannot rebloom own bloom", 422)) + + print(request.json["sender"]) + + blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"]) + + return jsonify( + { + "success": True, + } + ) def get_bloom(id_str): try: diff --git a/backend/main.py b/backend/main.py index 7ba155fa..f50a6567 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,6 +12,7 @@ register, self_profile, send_bloom, + send_rebloom, suggested_follows, user_blooms, ) @@ -59,6 +60,7 @@ def main(): app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) + app.add_url_rule("/rebloom", methods=["POST"], view_func=send_rebloom) app.add_url_rule("/hashtag/", view_func=hashtag) app.run(host="0.0.0.0", port="3000", debug=True) From 43b7d8b824dc024a9fbf7b680c39374ce79a5fa0 Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Thu, 2 Jul 2026 15:00:10 +0100 Subject: [PATCH 4/6] frontend api --- front-end/components/error.mjs | 1 + front-end/lib/api.mjs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/front-end/components/error.mjs b/front-end/components/error.mjs index 3fc9eeee..80ef1946 100644 --- a/front-end/components/error.mjs +++ b/front-end/components/error.mjs @@ -7,6 +7,7 @@ const _STATUS_MESSAGES = { 404: "Not Found - The requested resource does not exist.", 405: "Not Allowed - The server knows the request method, but the target resource doesn't support this method.", 418: "I'm a teapot - Server refuses to brew coffee with a teapot.", + 422: "Invalid data - The request was well-formed but was unable to be followed due to semantic errors.", 500: "Internal Server Error - Something went wrong on the server.", }; diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339b..8bd201fc 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -213,6 +213,24 @@ async function postBloom(content) { } // ======= USER methods +async function postRebloom(id, sender, content) { + try { + const data = await _apiRequest("/rebloom", { + method: "POST", + body: JSON.stringify({id, sender, content}), + }); + + if (data.success) { + await getBlooms(); + await getProfile(state.currentUser); + } + + return data; + } catch (error) { + + return {success: false}; + } +} async function getProfile(username) { const endpoint = username ? `/profile/${username}` : "/profile"; @@ -291,6 +309,7 @@ const apiService = { getBloom, getBlooms, postBloom, + postRebloom, getBloomsByHashtag, // User methods From d926be650f939c4843bb9eac0d64d650831a5b2c Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Thu, 2 Jul 2026 15:07:57 +0100 Subject: [PATCH 5/6] btn rebloom --- backend/data/blooms.py | 2 +- front-end/components/bloom.mjs | 15 +++++++++++++-- front-end/index.html | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 3c6d23b0..d7aaab5d 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -52,7 +52,7 @@ def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str) -> R dict( rebloom_id=rebloom_id, resender_id=resender.id, - sender_id=sender.id, + sender_name=sender, content=content, timestamp=datetime.datetime.now(datetime.UTC), ), diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c3..070d7b0b 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -1,3 +1,6 @@ +import { apiService } from "../lib/api.mjs" +import { state } from "../lib/state.mjs" + /** * Create a bloom component * @param {string} template - The ID of the template to clone @@ -20,6 +23,7 @@ const createBloom = (template, bloom) => { const bloomTime = bloomFrag.querySelector("[data-time]"); const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); + const rebloomButton = bloomFrag.querySelector("[data-rebloom]") bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); @@ -30,7 +34,14 @@ const createBloom = (template, bloom) => { ...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html") .body.childNodes ); - + rebloomButton.hidden = state.currentUser === bloom.sender + rebloomButton.addEventListener("click", async () => { + try { + await apiService.postRebloom(bloom.id, bloom.sender, bloom.content); + } catch (error) { + throw error; + } + }); return bloomFrag; }; @@ -84,4 +95,4 @@ function _formatTimestamp(timestamp) { } } -export {createBloom}; +export { createBloom }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b130..b4ea5cf8 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -239,6 +239,7 @@

Share a Bloom

+ From 87f745372aaa4ab5aa6a72fa94998a3a550889cc Mon Sep 17 00:00:00 2001 From: ZabihollahNamazi Date: Thu, 2 Jul 2026 15:25:17 +0100 Subject: [PATCH 6/6] rebloom done --- backend/data/blooms.py | 47 +++++++++++++++++++++++++++++----- backend/endpoints.py | 13 +++++++--- backend/main.py | 2 ++ db/schema.sql | 9 +++++++ front-end/components/bloom.mjs | 5 +++- front-end/index.html | 1 + front-end/lib/api.mjs | 4 +-- 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/backend/data/blooms.py b/backend/data/blooms.py index d7aaab5d..c9eb7f29 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -21,6 +21,7 @@ class Rebloom: sender: User content: str sent_timestamp: datetime.datetime + rebloomed: int def add_bloom(*, sender: User, content: str) -> Bloom: hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] @@ -44,19 +45,27 @@ def add_bloom(*, sender: User, content: str) -> Bloom: ) -def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str) -> Rebloom: +def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str, sent_timestamp: datetime.datetime) -> Rebloom: with db_cursor() as cur: cur.execute( "INSERT INTO reblooms (id, resender_id, original_sender_id, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_id)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_id, content) DO UPDATE SET times_rebloomed = times_rebloomed + 1", dict( rebloom_id=rebloom_id, - resender_id=resender.id, + resender_name=resender.username, sender_name=sender, content=content, - timestamp=datetime.datetime.now(datetime.UTC), + timestamp=sent_timestamp ), ) + return Rebloom( + id=rebloom_id, + resender=resender, + sender=sender, + content=content, + sent_timestamp=sent_timestamp, + rebloomed=1 + ) def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None @@ -71,7 +80,7 @@ def get_blooms_for_user( else: before_clause = "" - limit_clause = make_limit_clause(limit, kwargs) + #limit_clause = make_limit_clause(limit, kwargs) cur.execute( f"""SELECT @@ -81,8 +90,7 @@ def get_blooms_for_user( WHERE username = %(sender_username)s {before_clause} - ORDER BY send_timestamp DESC - {limit_clause} + """, kwargs, ) @@ -100,6 +108,33 @@ def get_blooms_for_user( ) return blooms +def get_reblooms_for_user(username: str) -> List[Rebloom]: + with db_cursor() as cur: + cur.execute( + """SELECT + reblooms.id, users.username, original_sender_name, content, send_timestamp, times_rebloomed + FROM + reblooms INNER JOIN users ON users.username = reblooms.resender_name + WHERE + users.username = %(username)s + """, + {"username": username}, + ) + rows = cur.fetchall() + reblooms = [] + for row in rows: + rebloom_id, resender_name, sender_name, content, timestamp, times_rebloomed = row + reblooms.append( + Rebloom( + id=rebloom_id, + resender=resender_name, + sender=sender_name, + content=content, + sent_timestamp=timestamp, + rebloomed=times_rebloomed + ) + ) + return reblooms def get_bloom(bloom_id: int) -> Optional[Bloom]: with db_cursor() as cur: diff --git a/backend/endpoints.py b/backend/endpoints.py index bd805204..7012a203 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -111,11 +111,14 @@ def other_profile(profile_username): followers = get_inverse_followed_usernames(profile_user) all_blooms = blooms.get_blooms_for_user(profile_username) - all_blooms.reverse() + all_reblooms = blooms.get_reblooms_for_user(profile_username) + + bloom_data = all_blooms + all_reblooms + bloom_data.reverse() return jsonify( { "username": profile_username, - "recent_blooms": all_blooms[:10], + "recent_blooms": bloom_data[:10], "follows": get_followed_usernames(profile_user), "followers": list(followers), "is_following": current_user is not None @@ -179,7 +182,7 @@ def send_rebloom(): print(request.json["sender"]) - blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"]) + blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"], sent_timestamp=request.json["sent_timestamp"]) return jsonify( { @@ -231,6 +234,10 @@ def user_blooms(profile_username): user_blooms.reverse() return jsonify(user_blooms) +def user_reblooms(profile_username): + user_reblooms = blooms.get_reblooms_for_user(profile_username) + user_reblooms.reverse() + return jsonify(user_reblooms) @jwt_required() def suggested_follows(limit_str): diff --git a/backend/main.py b/backend/main.py index f50a6567..6d2c5629 100644 --- a/backend/main.py +++ b/backend/main.py @@ -15,6 +15,7 @@ send_rebloom, suggested_follows, user_blooms, + user_reblooms, ) from dotenv import load_dotenv @@ -61,6 +62,7 @@ def main(): app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) app.add_url_rule("/rebloom", methods=["POST"], view_func=send_rebloom) + app.add_url_rule("/reblooms/", view_func=user_reblooms) app.add_url_rule("/hashtag/", view_func=hashtag) app.run(host="0.0.0.0", port="3000", debug=True) diff --git a/db/schema.sql b/db/schema.sql index 61e7580c..5b598cd3 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -13,6 +13,15 @@ CREATE TABLE blooms ( send_timestamp TIMESTAMP NOT NULL ); +CREATE TABLE reblooms ( + id BIGSERIAL NOT NULL PRIMARY KEY, + resender_name TEXT NOT NULL, + original_sender_name TEXT NOT NULL, + content TEXT NOT NULL, + send_timestamp TIMESTAMP NOT NULL, + times_rebloomed INT NOT NULL +); + CREATE TABLE follows ( id SERIAL PRIMARY KEY, follower INT NOT NULL REFERENCES users(id), diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 070d7b0b..39f2c415 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -24,6 +24,7 @@ const createBloom = (template, bloom) => { const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); const rebloomButton = bloomFrag.querySelector("[data-rebloom]") + const timesRebloomedCounter = bloomFrag.querySelector("[data-times-rebloomed]") bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); @@ -37,11 +38,13 @@ const createBloom = (template, bloom) => { rebloomButton.hidden = state.currentUser === bloom.sender rebloomButton.addEventListener("click", async () => { try { - await apiService.postRebloom(bloom.id, bloom.sender, bloom.content); + await apiService.postRebloom(bloom.id, bloom.sender, bloom.content, bloom.sent_timestamp); } catch (error) { throw error; } }); + timesRebloomedCounter.hidden = !bloom.rebloomed; + timesRebloomedCounter.textContent = `Times rebloomed: ${bloom.rebloomed}` return bloomFrag; }; diff --git a/front-end/index.html b/front-end/index.html index b4ea5cf8..dca15e76 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -239,6 +239,7 @@

Share a Bloom

+

diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index 8bd201fc..e84acb7d 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -213,11 +213,11 @@ async function postBloom(content) { } // ======= USER methods -async function postRebloom(id, sender, content) { +async function postRebloom(id, sender, content, sent_timestamp) { try { const data = await _apiRequest("/rebloom", { method: "POST", - body: JSON.stringify({id, sender, content}), + body: JSON.stringify({id, sender, content, sent_timestamp}), }); if (data.success) {