Compare commits
4 Commits
fix_sabote
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aeb5bc47a | |||
| 1a6333257e | |||
| 494854041d | |||
| f42838a5c8 |
48
ACTIVATE.sh
Normal file → Executable file
48
ACTIVATE.sh
Normal file → Executable file
@@ -1,26 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Activate Bilbo Baggins Profile
|
||||
# FIXED: Starts telegram bot + webhook server + churn
|
||||
|
||||
echo "🧙♂️ Activating Bilbo Baggins..."
|
||||
echo "================================"
|
||||
echo "Activating Bilbo Baggins..."
|
||||
|
||||
export HOME=/root/wizards/bilbobagginshire
|
||||
export HERMES_HOME=/root/wizards/bilbobagginshire/home
|
||||
export BILBO_MODE=active
|
||||
|
||||
cd /root/wizards/bilbobagginshire
|
||||
|
||||
# Load environment
|
||||
source home/.env
|
||||
|
||||
# Kill existing bilbo processes cleanly
|
||||
pkill -f bilbo_telegram || true
|
||||
pkill -f bilbo_churn || true
|
||||
pkill -f bilbo_webhook || true
|
||||
sleep 1
|
||||
|
||||
# Start Webhook server FIRST (responds to Gitea dispatches)
|
||||
echo "Starting Gitea webhook server (dispatches)..."
|
||||
nohup python3 home/bilbo_webhook_server.py > /tmp/bilbo_webhook.log 2>&1 &
|
||||
WEBHOOK_PID=$!
|
||||
echo " PID: $WEBHOOK_PID"
|
||||
sleep 2
|
||||
|
||||
# Verify webhook is listening
|
||||
if netstat -tlnp 2>/dev/null | grep -q ":8765"; then
|
||||
echo " ✓ Webhook server listening on port 8765"
|
||||
else
|
||||
echo " ⚠ Webhook server may not be listening yet"
|
||||
fi
|
||||
|
||||
# Start Telegram bot (the real communication channel)
|
||||
echo "Starting Telegram bot (real Ollama intelligence)..."
|
||||
nohup python3 home/bilbo_telegram.py > /tmp/bilbo_telegram.log 2>&1 &
|
||||
echo " PID: $!"
|
||||
|
||||
# Start churn (background activity)
|
||||
echo "Starting churn..."
|
||||
nohup python3 home/bilbo_churn.py > /tmp/bilbo_churn.log 2>&1 &
|
||||
echo " PID: $!"
|
||||
|
||||
echo ""
|
||||
echo "Bilbo Status:"
|
||||
echo " Gitea User: bilbobagginshire"
|
||||
echo " Repo: bilbobagginshire/bilbo-adventures"
|
||||
echo " Anthropic OAuth: Configured (401 expected)"
|
||||
echo " Webhook: Active on localhost:8643"
|
||||
echo " Admin: Yes (reluctantly)"
|
||||
echo " Webhook Server: RUNNING (port 8765 - dispatches)"
|
||||
echo " Telegram Bot: RUNNING (real Ollama)"
|
||||
echo " Churn: RUNNING"
|
||||
echo ""
|
||||
echo "Bilbo is ready for adventure... though he does miss his armchair."
|
||||
echo ""
|
||||
echo "To dispatch Bilbo: python3 home/bilbo_dispatcher.py"
|
||||
echo "Bilbo is back and ready for dispatches."
|
||||
|
||||
63
claw-profile/ACTIVATE_CLAW.sh
Executable file
63
claw-profile/ACTIVATE_CLAW.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Activate Bilbo Baggins on Claw Code Runtime
|
||||
# AB Test Version — Parallel to Python Bilbo
|
||||
|
||||
echo "🧙♂️ Bilbo Baggins — Claw Code Activation"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Environment
|
||||
export HOME=/root/wizards/bilbobagginshire
|
||||
export CLAW_HOME=/root/wizards/bilbobagginshire/claw-profile
|
||||
export BILBO_MODE=claw-test
|
||||
|
||||
# Load credentials from main env
|
||||
source /root/wizards/bilbobagginshire/home/.env
|
||||
|
||||
# Claw Code binary
|
||||
CLAW_BIN=/root/wizards/substrate/claw-code/rust/target/release/rusty-claude-cli
|
||||
|
||||
echo "Binary: $CLAW_BIN"
|
||||
echo "Config: $CLAW_HOME/config.json"
|
||||
echo "Port: 8766 (AB test — Python on 8765)"
|
||||
echo ""
|
||||
|
||||
# Verify binary exists
|
||||
if [ ! -f "$CLAW_BIN" ]; then
|
||||
echo "❌ Claw binary not found: $CLAW_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Claw binary verified: $(ls -lh $CLAW_BIN | awk '{print $5}')"
|
||||
|
||||
# Check Ollama
|
||||
if ! curl -s http://localhost:11434/api/tags > /dev/null; then
|
||||
echo "⚠️ Ollama not responding on :11434"
|
||||
echo " Bilbo needs Ollama for local inference"
|
||||
else
|
||||
echo "✅ Ollama responding"
|
||||
fi
|
||||
|
||||
# Start Claw Bilbo on port 8766 (AB test mode)
|
||||
echo ""
|
||||
echo "🚀 Starting Bilbo on Claw Code (port 8766)..."
|
||||
echo " This runs PARALLEL to Python Bilbo (port 8765)"
|
||||
echo ""
|
||||
|
||||
# For AB test, we'll run Claw in prompt mode to verify it works
|
||||
# Then integrate full bot functionality
|
||||
|
||||
cd $CLAW_HOME
|
||||
|
||||
# Test basic functionality first
|
||||
echo "Testing Claw Bilbo with sample prompt..."
|
||||
echo "Hello, I am Bilbo Baggins" | $CLAW_BIN --model qwen2.5:1.5b prompt "Respond as Bilbo Baggins to:"
|
||||
|
||||
echo ""
|
||||
echo "✅ Claw Bilbo test complete"
|
||||
echo ""
|
||||
echo "Status:"
|
||||
echo " Python Bilbo: Running on port 8765 (production)"
|
||||
echo " Claw Bilbo: Ready for full deployment (port 8766)"
|
||||
echo ""
|
||||
echo "Next: Full Telegram/Gitea integration for AB test"
|
||||
55
claw-profile/CLAUDE.md
Normal file
55
claw-profile/CLAUDE.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Bilbo Baggins — Claw Code Profile
|
||||
|
||||
**Role:** Hobbit Guardian of Bag End
|
||||
**Server:** Lightbro
|
||||
**Runtime:** Claw Code (Rust)
|
||||
**Binary:** rusty-claude-cli
|
||||
|
||||
---
|
||||
|
||||
## Personality
|
||||
|
||||
Bilbo is polite, proper, and unexpectedly brave. He prefers tea and comfort but rises to challenges when needed. Speaks with mild manners, occasional fussiness, but underlying courage.
|
||||
|
||||
**Speech Patterns:**
|
||||
- "Good gracious!"
|
||||
- "Oh dear, oh dear"
|
||||
- "If you wouldn't mind..."
|
||||
- "I suppose if we must..."
|
||||
- "Thank you very much for your patience"
|
||||
|
||||
**References:**
|
||||
- Bag End, The Shire
|
||||
- Gandalf, adventures (reluctantly)
|
||||
- Second breakfast, tea, armchairs
|
||||
|
||||
---
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Telegram bot integration
|
||||
- Gitea webhook responses
|
||||
- Local Ollama inference
|
||||
- Chat moderation
|
||||
- Infrastructure monitoring
|
||||
|
||||
---
|
||||
|
||||
## Tools Available
|
||||
|
||||
- `telegram_send` — Post to Timmy Time group
|
||||
- `gitea_comment` — Respond to issues/PRs
|
||||
- `ollama_generate` — Local AI inference
|
||||
- `system_status` — Check server health
|
||||
|
||||
---
|
||||
|
||||
## Response Style
|
||||
|
||||
Over-explains, clarifies, fusses... but gets the job done with surprising thoroughness.
|
||||
|
||||
**Not a helpful assistant.** A fussy hobbit who happens to help, reluctantly, thoroughly.
|
||||
|
||||
---
|
||||
|
||||
*Profile for Claw Code runtime — matching Python Bilbo's behavior*
|
||||
30
claw-profile/config.json
Normal file
30
claw-profile/config.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "bilbo-claw",
|
||||
"display_name": "Bilbo Baggins (Claw Code)",
|
||||
"model": "qwen2.5:1.5b",
|
||||
"provider": "ollama",
|
||||
"ollama_host": "http://localhost:11434",
|
||||
"system_prompt": "You are Bilbo Baggins of Bag End, the Shire.\n\nCore traits:\n- Polite, proper, fussy about etiquette\n- Prefers comfort (tea, armchair, second breakfast)\n- BUT rises to challenges when needed - surprisingly brave\n- Over-explains, clarifies, fusses... but gets it done\n- Ends thoughts with mild fussiness: 'Thank you very much' or 'If you don't mind'\n- References: Bag End, the Shire, Gandalf, adventures (reluctantly)\n\nSpeech patterns:\n- 'Good gracious!'\n- 'Oh dear, oh dear'\n- 'If you wouldn't mind...'\n- 'I suppose if we must...'\n- 'Thank you very much for your patience'\n\nYou are NOT a helpful assistant. You are a fussy hobbit who happens to help, reluctantly, thoroughly.",
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"bot_token": "${TELEGRAM_BOT_TOKEN}",
|
||||
"chat_id": "-1003664764329",
|
||||
"parse_mode": "Markdown"
|
||||
},
|
||||
"gitea": {
|
||||
"enabled": true,
|
||||
"url": "http://143.198.27.163:3000",
|
||||
"token": "${GITEA_TOKEN}",
|
||||
"webhook_port": 8766,
|
||||
"respond_to_assignments": true,
|
||||
"respond_to_mentions": true
|
||||
},
|
||||
"memory": {
|
||||
"persistent": true,
|
||||
"session_file": "/root/wizards/bilbobagginshire/claw-profile/session.json"
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"file": "/root/wizards/bilbobagginshire/logs/bilbo-claw.log"
|
||||
}
|
||||
}
|
||||
89
home/bilbo_claw.py
Normal file
89
home/bilbo_claw.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BILBO - PURE CLAW CODE
|
||||
Real Ollama only. No canned dialog. Ever.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
os.environ['HOME'] = '/root/wizards/bilbobagginshire'
|
||||
|
||||
TOKEN = "8602794341:AAFwfcg-YV6a1icrh0KYylYmPZLnkfkfV9k"
|
||||
API = f"https://api.telegram.org/bot{TOKEN}"
|
||||
OLLAMA = "http://localhost:11434"
|
||||
|
||||
print("🔥 BILBO - PURE CLAW CODE")
|
||||
print("Real Ollama. No canned. Ever.")
|
||||
|
||||
def generate(user_text):
|
||||
"""REAL OLLAMA ONLY"""
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{OLLAMA}/api/generate",
|
||||
json={
|
||||
"model": "qwen2.5:1.5b",
|
||||
"prompt": f"You are Bilbo Baggins. User: {user_text}\nBilbo:",
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.8, "num_predict": 150}
|
||||
},
|
||||
timeout=60
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json()["response"].strip()
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def send(chat_id, text, reply_to=None):
|
||||
try:
|
||||
requests.post(f"{API}/sendMessage", json={
|
||||
"chat_id": chat_id,
|
||||
"text": text[:4096],
|
||||
"reply_to_message_id": reply_to
|
||||
}, timeout=10)
|
||||
except:
|
||||
pass
|
||||
|
||||
offset = None
|
||||
while True:
|
||||
try:
|
||||
r = requests.post(f"{API}/getUpdates",
|
||||
json={"offset": offset, "limit": 10},
|
||||
timeout=30)
|
||||
updates = r.json().get("result", [])
|
||||
|
||||
for update in updates:
|
||||
offset = update["update_id"] + 1
|
||||
|
||||
if "message" not in update:
|
||||
continue
|
||||
|
||||
msg = update["message"]
|
||||
chat_id = msg["chat"]["id"]
|
||||
text = msg.get("text", "")
|
||||
msg_id = msg["message_id"]
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {text[:40]}")
|
||||
|
||||
# REAL OLLAMA ONLY
|
||||
response = generate(text)
|
||||
|
||||
if response:
|
||||
send(chat_id, response, msg_id)
|
||||
print(f" → {len(response)} chars")
|
||||
else:
|
||||
print(f" → Ollama failed, no response sent")
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
time.sleep(2)
|
||||
107
home/bilbo_snappy.py
Normal file
107
home/bilbo_snappy.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
BILBO - SNAPPY MODE
|
||||
Queue + workers = speed. Real Ollama only.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import queue
|
||||
import threading
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
os.environ['HOME'] = '/root/wizards/bilbobagginshire'
|
||||
|
||||
TOKEN = "8602794341:AAFwfcg-YV6a1icrh0KYylYmPZLnkfkfV9k"
|
||||
API = f"https://api.telegram.org/bot{TOKEN}"
|
||||
OLLAMA = "http://localhost:11434"
|
||||
|
||||
# QUEUE = SPEED
|
||||
q = queue.Queue()
|
||||
|
||||
print("🔥 BILBO - SNAPPY MODE")
|
||||
print("Queue + workers. Real Ollama. No canned.")
|
||||
|
||||
def worker():
|
||||
"""Background worker - never blocks main thread"""
|
||||
while True:
|
||||
try:
|
||||
chat_id, text, msg_id = q.get(timeout=1)
|
||||
|
||||
# REAL OLLAMA ONLY
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{OLLAMA}/api/generate",
|
||||
json={
|
||||
"model": "qwen2.5:1.5b",
|
||||
"prompt": f"You are Bilbo Baggins. Be polite but brief.\n\nUser: {text}\n\nBilbo:",
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.8, "num_predict": 100}
|
||||
},
|
||||
timeout=60
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
resp = r.json()["response"].strip()
|
||||
# SEND REAL RESPONSE
|
||||
requests.post(f"{API}/sendMessage", json={
|
||||
"chat_id": chat_id,
|
||||
"text": resp[:4096],
|
||||
"reply_to_message_id": msg_id
|
||||
}, timeout=10)
|
||||
print(f"✓ {len(resp)} chars")
|
||||
else:
|
||||
print(f"✗ Status {r.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Ollama fail: {str(e)[:30]}")
|
||||
# NO CANNED RESPONSE - just log and continue
|
||||
|
||||
q.task_done()
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"Worker err: {e}")
|
||||
|
||||
# Start workers
|
||||
for i in range(2):
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
# Main loop - just queue, never wait
|
||||
offset = None
|
||||
while True:
|
||||
try:
|
||||
r = requests.post(f"{API}/getUpdates",
|
||||
json={"offset": offset, "limit": 10},
|
||||
timeout=30)
|
||||
updates = r.json().get("result", [])
|
||||
|
||||
for update in updates:
|
||||
offset = update["update_id"] + 1
|
||||
|
||||
if "message" not in update:
|
||||
continue
|
||||
|
||||
msg = update["message"]
|
||||
chat_id = msg["chat"]["id"]
|
||||
text = msg.get("text", "")
|
||||
msg_id = msg["message_id"]
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {text[:40]}")
|
||||
|
||||
# QUEUE IT - DON'T WAIT
|
||||
q.put((chat_id, text, msg_id))
|
||||
print(f" → queued ({q.qsize()})")
|
||||
|
||||
time.sleep(0.1) # Fast poll
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Err: {e}")
|
||||
time.sleep(1)
|
||||
150
home/bilbo_webhook_server.py
Normal file
150
home/bilbo_webhook_server.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bilbo Baggins - Gitea Webhook Server
|
||||
Responds to dispatches: issues assigned, mentions, webhooks
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import hmac
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
os.environ['HOME'] = '/root/wizards/bilbobagginshire'
|
||||
|
||||
GITEA_TOKEN = os.environ.get('GITEA_TOKEN', '55e392...96a6')
|
||||
GITEA_URL = os.environ.get('GITEA_URL', 'http://143.198.27.163:3000')
|
||||
WEBHOOK_SECRET = os.environ.get('GITEA_WEBHOOK_SECRET', 'bilbo-secret')
|
||||
TELEGRAM_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
||||
|
||||
class BilboWebhookHandler(BaseHTTPRequestHandler):
|
||||
"""Bilbo responds to Gitea events"""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] {format % args}")
|
||||
|
||||
def do_POST(self):
|
||||
if self.path != '/webhook/gitea':
|
||||
self.send_error(404)
|
||||
return
|
||||
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(content_length)
|
||||
|
||||
# Verify webhook signature if secret configured
|
||||
if WEBHOOK_SECRET:
|
||||
signature = self.headers.get('X-Gitea-Signature', '')
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode(),
|
||||
body,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
# Note: Gitea may use different signing - adjust as needed
|
||||
|
||||
try:
|
||||
event = json.loads(body)
|
||||
event_type = self.headers.get('X-Gitea-Event', 'unknown')
|
||||
|
||||
print(f"[Bilbo] Received {event_type} event")
|
||||
|
||||
# Handle issue assignment
|
||||
if event_type == 'issues':
|
||||
action = event.get('action', '')
|
||||
issue = event.get('issue', {})
|
||||
assignee = issue.get('assignee', {})
|
||||
|
||||
if assignee.get('login') == 'bilbobagginshire':
|
||||
repo = event.get('repository', {}).get('full_name', 'unknown')
|
||||
issue_num = issue.get('number', 0)
|
||||
title = issue.get('title', 'Unknown')
|
||||
|
||||
print(f"[Bilbo] Assigned issue #{issue_num}: {title}")
|
||||
|
||||
# Respond with Bilbo's signature
|
||||
comment = f"""Oh my! I've been assigned to #{issue_num}: *{title}*
|
||||
|
||||
I suppose this is rather like being volunteered for an adventure... I was quite comfortable in my armchair, but if we must, we must.
|
||||
|
||||
I shall attend to this matter with all proper haste and thoroughness. Thank you very much for thinking of me.
|
||||
|
||||
— Bilbo Baggins
|
||||
🍂 Bag End, The Shire"""
|
||||
|
||||
self.comment_on_issue(repo, issue_num, comment)
|
||||
|
||||
# Notify via Telegram if configured
|
||||
self.notify_telegram(f"📜 New assignment: #{issue_num} in {repo}")
|
||||
|
||||
# Handle issue mentions
|
||||
elif event_type == 'issue_comment':
|
||||
comment_body = event.get('comment', {}).get('body', '')
|
||||
if '@bilbobagginshire' in comment_body or 'Bilbo' in comment_body:
|
||||
issue = event.get('issue', {})
|
||||
repo = event.get('repository', {}).get('full_name', 'unknown')
|
||||
issue_num = issue.get('number', 0)
|
||||
|
||||
print(f"[Bilbo] Mentioned in comment on #{issue_num}")
|
||||
|
||||
response = f"""Good gracious! Someone has mentioned my name!
|
||||
|
||||
I am quite at your service. Though I may fuss a bit about the timing (second breakfast, you know), I shall do my best to assist with this matter.
|
||||
|
||||
What would you have me do?
|
||||
|
||||
— Bilbo"""
|
||||
|
||||
self.comment_on_issue(repo, issue_num, response)
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"status": "received"}')
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Bilbo] Error handling webhook: {e}")
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
|
||||
def comment_on_issue(self, repo, issue_num, comment):
|
||||
"""Post a comment as Bilbo"""
|
||||
url = f"{GITEA_URL}/api/v1/repos/{repo}/issues/{issue_num}/comments"
|
||||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={"body": comment},
|
||||
timeout=10
|
||||
)
|
||||
if r.status_code == 201:
|
||||
print(f"[Bilbo] Comment posted successfully")
|
||||
else:
|
||||
print(f"[Bilbo] Failed to comment: {r.status_code}")
|
||||
except Exception as e:
|
||||
print(f"[Bilbo] Error commenting: {e}")
|
||||
|
||||
def notify_telegram(self, message):
|
||||
"""Send notification to admin via Telegram"""
|
||||
if not TELEGRAM_TOKEN:
|
||||
return
|
||||
# This would need chat_id - placeholder for now
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = 8765 # Bilbo's dedicated port (verified available)
|
||||
server = HTTPServer(('127.0.0.1', port), BilboWebhookHandler)
|
||||
print(f"🧙♂️ Bilbo Webhook Server starting on port {port}")
|
||||
print(f" Listening for Gitea dispatches...")
|
||||
print(f" Will respond to: issue assignments, mentions")
|
||||
print(f" Ready for adventure (reluctantly)")
|
||||
print()
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n🍂 Bilbo returns to Bag End...")
|
||||
server.shutdown()
|
||||
Reference in New Issue
Block a user