| cmd/love-machine | ||
| internal | ||
| pkg/remilia | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.md | ||
love machine
a bot that lives inside your browser, borrowing your soul to reach out and poke strangers on the internet — making friends while you sleep, harvesting names from a shoutbox, walking the graph of who knows who.
it doesn't have credentials. it just is you, at scale.
all HTTP runs through your authenticated browser tab via Chrome DevTools Protocol. the session is yours. the bot just uses it.
status: research project. this codebase is unfinished and no longer maintained. it was built as an experiment — use at your own risk, expect rough edges, and don't rely on it for anything important.
structure
├── cmd/love-machine/ # main binary + loops + TUI
│ ├── main.go # entry point, orchestrates all goroutines
│ ├── chromium.go # headless Chromium lifecycle
│ ├── args.go # CLI flags
│ ├── auth_flow.go # startup auth classification
│ ├── loops/ # concurrent loops
│ │ ├── poker.go # burst-pokes ~80% of pool per session
│ │ ├── pokeback.go # polls poke notifications, pokes back
│ │ ├── accept.go # accepts incoming friend requests
│ │ ├── friendadd.go # reciprocal friend + poke after accept
│ │ ├── miladychan.go # shoutbox WebSocket harvester
│ │ ├── beetle.go # beetle game automation
│ │ ├── friendenum.go # BFS friend graph walker
│ │ ├── profilefetch.go # background profile cache
│ │ ├── interaction_guard.go # protected-user filtering
│ │ └── paths.go # external runtime data resolution
│ └── tui/ # Bubble Tea terminal UI
│ ├── model.go # TUI state machine
│ └── styles.go # lipgloss styles
├── internal/
│ ├── cache/ # file-based response cache
│ ├── config/ # timing constants, URLs, junk items, protected users, quiet hours
│ ├── pool/ # user pool (thread-safe set)
│ ├── profile/ # profile aggregation, leaderboard, stale detection
│ └── state/ # persisted state (cooldowns, accepted, beetle, poked-back, notifications)
├── pkg/remilia/ # Remilia CDP client library
├── go.mod
├── go.sum
└── README.md
what it does
nine concurrent loops over a single CDP connection:
| loop | what |
|---|---|
poker_loop |
bursts ~80% of the pool per session, ~7s between pokes, long gaussian gap between sessions |
poke_back_loop |
polls unread poke notifications every 3 min, pokes back |
accept_loop |
polls incoming friend requests every 5 min, accepts all |
add_back_loop |
drains the add-back queue, sends friend request + poke after 5–30 min delay |
friend_add_loop |
fetches top friends' friend lists, sends friend requests to mutuals not in pool/cooldown/protected, 3–10 per session at 30–90 min intervals |
miladychan_ws_loop |
connects to the miladychan shoutbox WebSocket, harvests usernames |
beetle_loop |
monitors the beetle game, runs safe cooldown actions and crafting |
friend_enum_loop |
BFS walks the friend graph in the background, grows the pool |
profile_fetch_loop |
fetches and caches user profiles in the background |
the pool starts from seed files under data/ (miladychan_users.json + friends_pool.json) at launch and grows live from the activity feed and shoutbox. the pool itself is in-memory — runtime membership is not written back to those files.
cooldowns are ~20h server-enforced per user. the remaining time is parsed from the error message and stored in data/milady_state.json. state persists across restarts.
build
go build -o love-machine ./cmd/love-machine
seed data dir
seed/state files live under data/ at the project root:
mkdir -p data
| file | purpose |
|---|---|
miladychan_users.json |
shoutbox-harvested usernames to seed the pool |
friends_pool.json |
friend-graph walker output to seed the pool |
milady_state.json |
persisted cooldowns, accepted friends, beetle state |
cache/ |
API response caches (friend list, beetle) |
profile_data/ |
fetched user profiles |
leaderboard.json / aggregates.json |
rebuilt by profile fetch loop |
the bot reads pool seeds at startup and writes state, cache, and profile data back. the in-memory pool itself does not persist.
running locally (against Brave)
launch Brave with remote debugging:
brave --remote-debugging-port=9223
verify:
curl -s http://localhost:9223/json/version
then:
./love-machine
./love-machine --dry-run --mean-minutes 60 --collect-seconds 60
login mode
if you need to authenticate first (fresh profile):
./love-machine --login --chromium-profile ~/.config/chromium-remilia
this opens a visible browser window. log in to remilia.net, then close the window — the session is saved.
running on a server (headless Chromium)
1. install Chromium
# Arch
sudo pacman -S chromium
# Debian/Ubuntu
sudo apt install -y chromium
2. copy your session to the server
on your local machine:
tar czf /tmp/session.tar.gz \
-C ~/.config/BraveSoftware/Brave-Browser/Default \
Cookies "Local Storage" "Session Storage" IndexedDB
scp /tmp/session.tar.gz user@yourserver:~/
on the server:
mkdir -p ~/.config/chromium-remilia/Default
cd ~/.config/chromium-remilia/Default
tar xzf ~/session.tar.gz
3. run
./love-machine --chromium-profile ~/.config/chromium-remilia
the binary auto-launches headless Chromium and connects to it via CDP. on shutdown it kills the Chromium instance it spawned.
in tmux:
tmux new -d -s love './love-machine --chromium-profile ~/.config/chromium-remilia'
if you manage Chromium separately, use --no-chromium:
./love-machine --no-chromium --cdp-host http://localhost:9223
session expiry
the OIDC refresh token keeps the session alive while Chromium is running. if auth lapses, re-sync from your local machine:
pkill chromium
rsync -av \
--include="Cookies" \
--include="Local Storage/***" \
--include="Session Storage/***" \
--include="IndexedDB/***" \
--exclude="*" \
~/.config/BraveSoftware/Brave-Browser/Default/ \
user@yourserver:~/.config/chromium-remilia/Default/
remote inspection
SSH tunnel the CDP port:
ssh -L 9223:localhost:9223 user@yourserver
then open chrome://inspect locally — you'll see the headless instance.
CLI flags
--dry-run don't send any requests
--mean-minutes float mean gap between poke sessions (default 60)
--collect-seconds int activity feed collection window per session (default 60)
--no-beetle disable beetle loop
--no-friend-add disable friend add loop
--no-profile-fetch disable background profile fetch loop
--beetle-daily-min float min delay before daily cheese claim (seconds, default 30)
--beetle-daily-max float max delay before daily cheese claim (seconds, default 180)
--profile-rate float profile fetch rate limit (requests/second, default 1)
--refresh-days float days before a cached profile is stale (default 30)
--backfill fetch missing profiles for all pool members on start
--cdp-host string CDP HTTP endpoint (default http://localhost:9223)
--chromium-bin string path to chromium binary (auto-detected if empty)
--chromium-profile string chromium user-data-dir (default ~/.config/chromium-remilia)
--no-chromium skip automatic chromium launch
--login open visible browser to authenticate, then exit
--chat-reactions int reactions to add to each sent Miladychan chat message (0 disables)
--chat-reaction-emoji int reaction emoji index (-1 random, 0 😹, 1 🤍, 2 🫵)
--chat-reaction-min-delay-ms int minimum delay between queued reactions
--chat-reaction-max-delay-ms int maximum delay between queued reactions
chat reactions only affect messages you send through the love-machine Miladychan composer. they reuse the authenticated browser websocket, so keep them opt-in.
example:
./love-machine --chat-reactions 3 --chat-reaction-emoji 1
REMILIA_LIVE_MUTATE=1 is only for TestChatReactionsLiveMutation; it posts a probe message.
API reference
key endpoints used by the client (see pkg/remilia/api.go for the full list):
POST /api/pokeUser {username}
POST /api/friendship/request {username}
POST /api/friendship/accept {username}
GET /api/profile/friends/incoming ?page=&limit=
GET /api/notifications ?type=poke&states=unread&limit=
GET /api/activityFeed SSE stream (used inline in poker.go)
GET /api/beetle/user
POST /api/beetle/action/{action} catchBeetle | junkFaucet | claimUBC | craft
POST /api/whoami
GET /api/health
GET /api/profile/{username}
GET /api/profile/friends/{username} ?page=&limit=
POST /api/beetle/action/craft CraftRequest body
POST /api/beetle/action/beetleHunt
POST /api/beetle/action/unlockCraftingSlot3