# views/forum.py
from snek.system.view import BaseView
from aiohttp import web
import json
from aiohttp import web
from snek.system.view import BaseView
class ForumIndexView(BaseView):
login_required = True
async def get(self):
if self.login_required and not self.session.get("logged_in"):
return web.HTTPFound("/")
channel = await self.services.channel.get(
uid=self.request.match_info.get("channel")
)
if not channel:
user = await self.services.user.get(
uid=self.request.match_info.get("channel")
)
if user:
channel = await self.services.channel.get_dm(
self.session.get("uid"), user["uid"]
)
if channel:
return web.HTTPFound("/channel/{}.html".format(channel["uid"]))
if not channel:
return web.HTTPNotFound()
channel_member = await self.app.services.channel_member.get(
user_uid=self.session.get("uid"), channel_uid=channel["uid"]
)
if not channel_member:
if not channel["is_private"]:
channel_member = await self.app.services.channel_member.create(
channel_uid=channel["uid"],
user_uid=self.session.get("uid"),
is_moderator=False,
is_read_only=False,
is_muted=False,
is_banned=False,
)
return web.HTTPNotFound()
channel_member["new_count"] = 0
await self.app.services.channel_member.save(channel_member)
user = await self.services.user.get(uid=self.session.get("uid"))
messages = [
await self.app.services.channel_message.to_extended_dict(message)
for message in await self.app.services.channel_message.offset(
channel["uid"]
)
]
for message in messages:
await self.app.services.notification.mark_as_read(
self.session.get("uid"), message["uid"]
)
name = await channel_member.get_name()
return await self.render_template(
"forum.html",
{"name": name, "channel": channel, "user": user, "messages": messages},
)
class ForumView(BaseView):
"""REST API endpoints for forum"""
login_required = True
async def get_forums(self):
request = self
self = request.app
"""GET /forum/api/forums - Get all active forums"""
forums = []
async for forum in self.services.forum.get_active_forums():
forums.append({
"uid": forum["uid"],
"name": forum["name"],
"description": forum["description"],
"slug": forum["slug"],
"icon": forum["icon"],
"thread_count": forum["thread_count"],
"post_count": forum["post_count"],
"last_post_at": forum["last_post_at"],
"last_thread_uid": forum["last_thread_uid"]
})
return web.json_response({"forums": forums})
async def get_forum(self):
request = self
self = request.app
setattr(self, "request", request)
"""GET /forum/api/forums/:slug - Get forum by slug"""
slug = self.request.match_info["slug"]
forum = await self.services.forum.get(slug=slug, is_active=True)
if not forum:
return web.json_response({"error": "Forum not found"}, status=404)
# Get threads
threads = []
page = int(self.request.query.get("page", 1))
limit = 50
offset = (page - 1) * limit
async for thread in forum.get_threads(limit=limit, offset=offset):
# Get author info
author = await self.services.user.get(uid=thread["created_by_uid"])
last_post_author = None
if thread["last_post_by_uid"]:
last_post_author = await self.services.user.get(uid=thread["last_post_by_uid"])
threads.append({
"uid": thread["uid"],
"title": thread["title"],
"slug": thread["slug"],
"is_pinned": thread["is_pinned"],
"is_locked": thread["is_locked"],
"view_count": thread["view_count"],
"post_count": thread["post_count"],
"created_at": thread["created_at"],
"last_post_at": thread["last_post_at"],
"author": {
"uid": author["uid"],
"username": author["username"],
"nick": author["nick"],
"color": author["color"]
},
"last_post_author": {
"uid": last_post_author["uid"],
"username": last_post_author["username"],
"nick": last_post_author["nick"],
"color": last_post_author["color"]
} if last_post_author else None
})
return web.json_response({
"forum": {
"uid": forum["uid"],
"name": forum["name"],
"description": forum["description"],
"slug": forum["slug"],
"icon": forum["icon"],
"thread_count": forum["thread_count"],
"post_count": forum["post_count"]
},
"threads": threads,
"page": page,
"hasMore": len(threads) == limit
})
async def create_thread(self):
request = self
self = request.app
setattr(self, "request", request)
"""POST /forum/api/forums/:slug/threads - Create new thread"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
slug = self.request.match_info["slug"]
forum = await self.services.forum.get(slug=slug, is_active=True)
if not forum:
return web.json_response({"error": "Forum not found"}, status=404)
data = await self.request.json()
title = data.get("title", "").strip()
content = data.get("content", "").strip()
if not title or not content:
return web.json_response({"error": "Title and content required"}, status=400)
try:
thread, post = await self.services.thread.create_thread(
forum_uid=forum["uid"],
title=title,
content=content,
created_by_uid=self.request.session["uid"]
)
return web.json_response({
"thread": {
"uid": thread["uid"],
"slug": thread["slug"],
"forum_slug": forum["slug"]
}
})
except Exception as e:
return web.json_response({"error": str(e)}, status=400)
async def get_thread(self):
request = self
self = request.app
setattr(self, "request", request)
"""GET /forum/api/threads/:thread_slug - Get thread with posts"""
thread_slug = self.request.match_info["thread_slug"]
thread = await self.services.thread.get(slug=thread_slug)
if not thread:
return web.json_response({"error": "Thread not found"}, status=404)
# Increment view count
await thread.increment_view_count()
# Get forum
forum = await self.services.forum.get(uid=thread["forum_uid"])
# Get posts
posts = []
page = int(self.request.query.get("page", 1))
limit = 50
offset = (page - 1) * limit
current_user_uid = self.request.session.get("uid")
async for post in thread.get_posts(limit=limit, offset=offset):
author = await post.get_author()
is_liked = False
if current_user_uid:
is_liked = await post.is_liked_by(current_user_uid)
posts.append({
"uid": post["uid"],
"content": post["content"],
"created_at": post["created_at"],
"edited_at": post["edited_at"],
"is_first_post": post["is_first_post"],
"like_count": post["like_count"],
"is_liked": is_liked,
"author": {
"uid": author["uid"],
"username": author["username"],
"nick": author["nick"],
"color": author["color"]
}
})
# Get thread author
thread_author = await self.services.user.get(uid=thread["created_by_uid"])
return web.json_response({
"thread": {
"uid": thread["uid"],
"title": thread["title"],
"slug": thread["slug"],
"is_pinned": thread["is_pinned"],
"is_locked": thread["is_locked"],
"view_count": thread["view_count"],
"post_count": thread["post_count"],
"created_at": thread["created_at"],
"author": {
"uid": thread_author["uid"],
"username": thread_author["username"],
"nick": thread_author["nick"],
"color": thread_author["color"]
}
},
"forum": {
"uid": forum["uid"],
"name": forum["name"],
"slug": forum["slug"]
},
"posts": posts,
"page": page,
"hasMore": len(posts) == limit
})
async def create_post(self):
request = self
self = request.app
setattr(self, "request", request)
"""POST /forum/api/threads/:thread_uid/posts - Create new post"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
thread_uid = self.request.match_info["thread_uid"]
thread = await self.services.thread.get(uid=thread_uid)
if not thread:
return web.json_response({"error": "Thread not found"}, status=404)
data = await self.request.json()
content = data.get("content", "").strip()
if not content:
return web.json_response({"error": "Content required"}, status=400)
try:
post = await self.services.post.create_post(
thread_uid=thread["uid"],
forum_uid=thread["forum_uid"],
content=content,
created_by_uid=self.request.session["uid"]
)
author = await post.get_author()
return web.json_response({
"post": {
"uid": post["uid"],
"content": post["content"],
"created_at": post["created_at"],
"like_count": post["like_count"],
"is_liked": False,
"author": {
"uid": author["uid"],
"username": author["username"],
"nick": author["nick"],
"color": author["color"]
}
}
})
except Exception as e:
return web.json_response({"error": str(e)}, status=400)
async def edit_post(self):
request = self
self = request.app
setattr(self, "request", request)
"""PUT /forum/api/posts/:post_uid - Edit post"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
post_uid = self.request.match_info["post_uid"]
data = await self.request.json()
content = data.get("content", "").strip()
if not content:
return web.json_response({"error": "Content required"}, status=400)
post = await self.services.post.edit_post(
post_uid=post_uid,
content=content,
user_uid=self.request.session["uid"]
)
if not post:
return web.json_response({"error": "Cannot edit post"}, status=403)
return web.json_response({
"post": {
"uid": post["uid"],
"content": post["content"],
"edited_at": post["edited_at"]
}
})
async def delete_post(self):
request = self
self = request.app
setattr(self, "request", request)
"""DELETE /forum/api/posts/:post_uid - Delete post"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
post_uid = self.request.match_info["post_uid"]
success = await self.services.post.delete_post(
post_uid=post_uid,
user_uid=self.request.session["uid"]
)
if not success:
return web.json_response({"error": "Cannot delete post"}, status=403)
return web.json_response({"success": True})
async def toggle_like(self):
request = self
self = request.app
setattr(self, "request", request)
"""POST /forum/api/posts/:post_uid/like - Toggle post like"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
post_uid = self.request.match_info["post_uid"]
is_liked = await self.services.post_like.toggle_like(
post_uid=post_uid,
user_uid=self.request.session["uid"]
)
if is_liked is None:
return web.json_response({"error": "Failed to toggle like"}, status=400)
# Get updated post
post = await self.services.post.get(uid=post_uid)
return web.json_response({
"is_liked": is_liked,
"like_count": post["like_count"]
})
async def toggle_pin(self):
request = self
self = request.app
setattr(self, "request", request)
"""POST /forum/api/threads/:thread_uid/pin - Toggle thread pin"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
thread_uid = self.request.match_info["thread_uid"]
thread = await self.services.thread.toggle_pin(
thread_uid=thread_uid,
user_uid=self.request.session["uid"]
)
if not thread:
return web.json_response({"error": "Cannot toggle pin"}, status=403)
return web.json_response({"is_pinned": thread["is_pinned"]})
async def toggle_lock(self):
request = self
self = request.app
setattr(self, "request", request)
"""POST /forum/api/threads/:thread_uid/lock - Toggle thread lock"""
if not self.request.session.get("logged_in"):
return web.json_response({"error": "Unauthorized"}, status=401)
thread_uid = self.request.match_info["thread_uid"]
thread = await self.services.thread.toggle_lock(
thread_uid=thread_uid,
user_uid=self.request.session["uid"]
)
if not thread:
return web.json_response({"error": "Cannot toggle lock"}, status=403)
return web.json_response({"is_locked": thread["is_locked"]})
# views/forum_websocket.py
class ForumWebSocketView(BaseView):
"""WebSocket view for real-time forum updates"""
async def get(self):
ws = web.WebSocketResponse()
await ws.prepare(self.request)
# Store WebSocket connection
ws_id = self.services.forum.generate_uid()
if not hasattr(self.app, 'forum_websockets'):
self.app.forum_websockets = {}
user_uid = self.request.session.get("uid")
self.app.forum_websockets[ws_id] = {
"ws": ws,
"user_uid": user_uid,
"subscriptions": set()
}
try:
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
data = json.loads(msg.data)
await self.handle_ws_message(ws_id, data)
elif msg.type == web.WSMsgType.ERROR:
break
finally:
# Clean up
if ws_id in self.app.forum_websockets:
del self.app.forum_websockets[ws_id]
return ws
async def handle_ws_message(self, ws_id, data):
"""Handle incoming WebSocket messages"""
action = data.get("action")
if action == "subscribe":
# Subscribe to forum/thread updates
target_type = data.get("type") # "forum" or "thread"
target_id = data.get("id")
if target_type and target_id:
subscription = f"{target_type}:{target_id}"
self.app.forum_websockets[ws_id]["subscriptions"].add(subscription)
# Send confirmation
ws = self.app.forum_websockets[ws_id]["ws"]
await ws.send_str(json.dumps({
"type": "subscribed",
"subscription": subscription
}))
elif action == "unsubscribe":
target_type = data.get("type")
target_id = data.get("id")
if target_type and target_id:
subscription = f"{target_type}:{target_id}"
self.app.forum_websockets[ws_id]["subscriptions"].discard(subscription)
# Send confirmation
ws = self.app.forum_websockets[ws_id]["ws"]
await ws.send_str(json.dumps({
"type": "unsubscribed",
"subscription": subscription
}))
@staticmethod
async def broadcast_update(app, event_type, data):
"""Broadcast updates to subscribed WebSocket clients"""
if not hasattr(app, 'forum_websockets'):
return
# Determine subscription targets based on event
targets = set()
if event_type in ["thread_created", "post_created", "post_edited", "post_deleted"]:
if "forum_uid" in data:
targets.add(f"forum:{data['forum_uid']}")
if event_type in ["post_created", "post_edited", "post_deleted", "post_liked", "post_unliked"]:
if "thread_uid" in data:
targets.add(f"thread:{data['thread_uid']}")
# Send to subscribed clients
for ws_id, ws_data in app.forum_websockets.items():
if ws_data["subscriptions"] & targets:
try:
await ws_data["ws"].send_str(json.dumps({
"type": event_type,
"data": data
}))
except Exception:
pass