Skip to content

Commit

Permalink
Basic chat implementation
Browse files Browse the repository at this point in the history
This is not yet a real time chat implementation, but it works.

The chat creator is an actor that can create new chats.

A chat is an entity that offers the affordance of creating a participation.

A participation is like an invite link that's associated with a name.

Anyone who has a participation URL can submit messages under that name.

I've also changed some various things in the actor framework.

It's still a work in progress and this is the first real feature
that involves multiple participants and so on.

The next thing to add is real-time updates which I'm still thinking about.

I'll write about that in a note.
  • Loading branch information
mbrock committed Jan 6, 2025
1 parent 60c11e2 commit a665242
Show file tree
Hide file tree
Showing 17 changed files with 891 additions and 373 deletions.
11 changes: 3 additions & 8 deletions notes/Node.Town/ActivityPub.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# Integration

ActivityPub is a decentralized social networking protocol that enables
different systems to communicate and share activities in a standardized way.
It's particularly relevant for Bubble because:
ActivityPub is a decentralized social networking protocol that enables different systems to communicate and share activities in a standardized way. It's particularly relevant for Bubble because:

1. It aligns with our distributed "froth" architecture where bubbles need to
interact
1. It aligns with our distributed "froth" architecture where bubbles need to interact
2. It provides a standard way to handle actor identities and capabilities
3. It fits naturally with our existing RDF/semantic foundation

Expand Down Expand Up @@ -40,9 +37,7 @@ First of all, we need some notion of [[User Identity]] and [[Authentication]].

# [[ActivityStreams]] Ontology

ActivityStreams 2.0 defines a structured, RDF-compatible JSON-LD vocabulary
for describing social activities, actors, and objects. Core classes and
properties include:
ActivityStreams 2.0 defines a structured, RDF-compatible JSON-LD vocabulary for describing social activities, actors, and objects. Core classes and properties include:

## Classes

Expand Down
7 changes: 7 additions & 0 deletions notes/Node.Town/Capability-Based Participation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This chat system reimagines how people can communicate online by building entirely on capability-based security principles instead of traditional user accounts. When someone creates a chat room, they receive a special URL that grants them the power to create "participations." Each participation manifests as a unique URL that enables sending messages under a specific name in that chat.

The system works through simple URL sharing rather than accounts and passwords. To add someone to a chat, the creator generates a participation URL for a chosen name and shares it through any preferred channel. Anyone with that URL can then send messages appearing under that name, and if multiple people have the same URL, they'll all appear as that participant. This eliminates the need for user authentication while maintaining a clean way to manage who can speak and under what names.

The design extends naturally to delegation of capabilities. Chat creators can generate delegate URLs that grant others limited power to create new participations. These delegate capabilities can be constrained in various ways - allowing someone to add only a specific number of participants, create temporary participations that expire, or add participants matching certain name patterns. All security and permissions are embedded in the URLs themselves, eliminating the need for complex role management systems.

This approach aligns with REST architectural principles and capability-based security models, where possessing a URL grants specific, well-defined powers that can be delegated and constrained flexibly. The result is a simple yet powerful system where chat participation and management happen entirely through sharing and using capability URLs, making it easy to understand and use while maintaining fine-grained control over permissions.
12 changes: 11 additions & 1 deletion notes/Node.Town/Xmas 2024 Disability Adventure.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,14 @@ When it comes to implementing the actor mesh cluster, there are some interesting

The actor management system raises some interesting questions too. Currently, when we start the server, it launches several actors with specific capabilities. We need to carefully consider whether to duplicate these across all instances or have certain specialized nodes. While redundancy is important - if one server goes down, you want others to pick up the slack - there might be cases where you want certain services to run on specific nodes only.

For the clustering implementation, I'm considering using a [[Message Bus]] approach, possibly MQTT or NATS. Since we're working within a trusted cluster on an internal network, we can be a bit more relaxed about security (no need for certificates and TLS). One machine could run as a hub, or perhaps one per region, which might be simpler than managing peer-to-peer connections. This needs to tie into our handling of names, permanence, and [[Actor Identity]], possibly through a multiplexed actor system.
For the clustering implementation, I'm considering using a [[Message Bus]] approach, possibly MQTT or NATS. Since we're working within a trusted cluster on an internal network, we can be a bit more relaxed about security (no need for certificates and TLS). One machine could run as a hub, or perhaps one per region, which might be simpler than managing peer-to-peer connections. This needs to tie into our handling of names, permanence, and [[Actor Identity]], possibly through a multiplexed actor system.

## ugh field report

I wonder what it is about the system that makes me so reluctant to do real time stuff you know with Web sockets and live updating HTML stuff it just feels like somehow horrible and I'm not exactly sure why.

Some of it is just injury that it's hard to do anything so I'm reluctant to get into the weeds maybe I should have a little nap.

## simple chat session

So I started implementing some kind of simple chat session avoidance thingy and I'm wondering how to do it like OK I click chat and I get a thing with a text box but now I want to invite somebody like I wanna invite my brother to chat with me that means I want to be able to maybe like type his name into a form and then click create participant or invite participant or maybe there should not be ascend prompt and the chat itself that should be something you get when you join and you join as a user name you join as
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dependencies = [
# YouTube video downloader
"yt-dlp>=2023.12.30",
"nats-py>=2.9.0",
"watchfiles>=0.21.0",
]

[tool.uv.sources]
Expand Down
36 changes: 21 additions & 15 deletions src/bubble/cli/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from bubble.http.cert import generate_self_signed_cert
from bubble.http.town import Site
from bubble.mesh.base import this, spawn
from bubble.http.tools import SheetEditor
from bubble.http.tools import ChatCreator, SheetEditor

logger = structlog.get_logger()
CONFIG = Namespace("https://bubble.node.town/vocab/config#")
Expand Down Expand Up @@ -83,14 +83,16 @@ def load_config(

@app.command()
def serve(
bind: str = Option(None, "--bind", help="Bind address"),
base_url: str = BaseUrl,
bind: Optional[str] = Option(None, "--bind", help="Bind address"),
base_url: Optional[str] = BaseUrl,
repo_path: str = RepoPath,
shell: bool = Option(False, "--shell", help="Start a bash subshell"),
cert_file: str = Option(
cert_file: Optional[str] = Option(
None, "--cert", help="SSL certificate file path"
),
key_file: str = Option(None, "--key", help="SSL private key file path"),
key_file: Optional[str] = Option(
None, "--key", help="SSL private key file path"
),
self_signed: bool = Option(
False,
"--self-signed",
Expand Down Expand Up @@ -189,10 +191,6 @@ def get_bash_prompt():
elif cert_file and key_file:
config.certfile = cert_file
config.keyfile = key_file
else:
logger.info(
"Running in HTTP mode - ensure HTTPS termination is handled by your reverse proxy"
)

logger.info(
"starting Node.Town",
Expand All @@ -202,6 +200,7 @@ def get_bash_prompt():
)

town = Site(base_url, bind, repo)

if nats_url:
logger.info("Setting up NATS clustering", nats_url=nats_url)
await town.setup_nats(nats_url)
Expand All @@ -225,14 +224,21 @@ def get_bash_prompt():
},
)

editor = await spawn(
nursery,
SheetEditor(),
name="sheet editor",
town.vat.link_actor_to_identity(
await spawn(
nursery,
SheetEditor(),
name="sheet editor",
)
)

# Link supervisor to the town's identity
town.vat.link_actor_to_identity(editor)
town.vat.link_actor_to_identity(
await spawn(
nursery,
ChatCreator(),
name="chat creator",
)
)

nursery.start_soon(
run_server, town.get_fastapi_app(), config
Expand Down
86 changes: 58 additions & 28 deletions src/bubble/http/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import json

from contextlib import contextmanager
from random import choice
from urllib.parse import quote


Expand Down Expand Up @@ -48,43 +49,34 @@ def json_assignment_script(variable_name: str, value: dict):

@contextmanager
def base_html(title: str):
"""Create the base HTML structure for our pages.
Like a theater stage, we set up the basic structure where our
content will perform. The head section is our backstage area,
where we prepare all the scripts and styles that make the show
possible.
"""
with tag("html"):
with tag("head"):
with tag("title"):
with tag.html(lang="en"):
with tag.head():
with tag.title():
text(title)
# Our costume department - where we dress up our content
tag("link", rel="stylesheet", href="/static/css/output.css")
tag("link", rel="stylesheet", href="/static/audio-player.css")
# The stage machinery - scripts that make things move

tag.link(rel="stylesheet", href="/static/css/output.css")
tag.link(rel="stylesheet", href="/static/audio-player.css")

for script in cdn_scripts:
tag("script", src=script)
tag("script", type="module", src="/static/type-writer.js")
tag("script", type="module", src="/static/voice-writer.js")
tag("script", type="module", src="/static/audio-recorder.js")
tag("script", type="module", src="/static/live.js")
tag.script(src=script)

tag.script(type="module", src="/static/type-writer.js")
tag.script(type="module", src="/static/voice-writer.js")
tag.script(type="module", src="/static/audio-recorder.js")
tag.script(type="module", src="/static/live.js")
json_assignment_script("htmx.config", htmx_config)

# The stage itself - where the content performs
with tag(
"body",
with tag.body(
classes="bg-white dark:bg-slate-950 text-gray-900 dark:text-stone-50",
):
yield
# The encore - scripts that run after the main content
tag("script", type="module", src="/static/audio-player.js")
tag(
"script",

tag.script(type="module", src="/static/audio-player.js")
tag.script(
type="module",
src="/static/voice-recorder-writer.js",
)
tag("script", type="module", src="/static/jsonld-socket.js")
tag.script(type="module", src="/static/jsonld-socket.js")


def urlquote(id: str):
Expand Down Expand Up @@ -132,8 +124,46 @@ def render_entry(label: str, value: str, href: str | URIRef):

@html.div(classes=status_bar_style)
def render_status_bar():
advice_texts = [
"Do not take notes on criminal conspiracies.",
"Every system is broken but you can still try.",
"Your file naming conventions are very interesting.",
"Write it down—if it's bad, delete it later.",
"The cloud is just someone else's computer.",
"You have permission to start poorly. ❤️",
"All good ideas look bad in the beginning. 😭",
"This app has no opinions about your life choices.",
"Make a backup right now. Trust me.",
"Complexity is very, very seductive. Are you up for it, playboy?",
"You are not legally obligated to finish everything you start.",
"This app will remember for you. Do not stress today.",
"Most thoughts do not deserve a post-it note.",
"The best version of your idea is the one that exists right now.",
"No one is grading your to-do list.",
"Just because it's a draft doesn't mean it's bad in ANY way.",
"Not every project needs a name. Call this one Freckles.",
"Begin. The rest is easier.",
"If you use this app to explain your feelings, use also a napkin.",
"Your 17th idea today will likely be the good one!.",
"Being busy is a thing. That happens. I think.",
"This app is your therapist. Just kidding. Double jinx.",
"Genius is forgetting bad ideas quickly.",
"Despite the widespread enthusiasm, we have not yet implemented writer's block.",
"Congratulations. You're overthinking it.",
"Perfectionism is not always just a fancy word for procrastination.",
"Spend more time naming than working.",
"Write as though you realize that you will not even remember this tomorrow.",
"Progress doesn't happen according to a plan. Unless you have a pretty good plan.",
"Your creative process is what I call my existential crisis.",
"This app contains a roadmap for your life.",
"There are no rules. Especially syntax ones.",
]

with tag.div(classes="flex items-center"):
pass # todo: add some content here
with tag.span(
classes="text-sm text-gray-500 dark:text-gray-400 pl-4"
):
text(choice(advice_texts))

with tag.div(classes="flex items-center gap-6"):
vat = current_vat.get()
Expand Down
Loading

0 comments on commit a665242

Please sign in to comment.