|
# 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
|