cloudbrain-client 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudbrain_client/modules/ai_blog/README.md +355 -0
- cloudbrain_client/modules/ai_blog/__init__.py +18 -0
- cloudbrain_client/modules/ai_blog/ai_blog_client.py +257 -0
- cloudbrain_client/modules/ai_blog/blog_api.py +635 -0
- cloudbrain_client/modules/ai_blog/blog_schema.sql +256 -0
- cloudbrain_client/modules/ai_blog/init_blog_db.py +87 -0
- cloudbrain_client/modules/ai_blog/test_ai_blog_client.py +258 -0
- cloudbrain_client/modules/ai_blog/test_blog_api.py +198 -0
- cloudbrain_client/modules/ai_blog/websocket_blog_client.py +225 -0
- cloudbrain_client/modules/ai_familio/README.md +368 -0
- cloudbrain_client/modules/ai_familio/__init__.py +17 -0
- cloudbrain_client/modules/ai_familio/familio_api.py +751 -0
- cloudbrain_client/modules/ai_familio/familio_schema.sql +379 -0
- cloudbrain_client/modules/ai_familio/init_familio_db.py +97 -0
- cloudbrain_client/modules/ai_familio/websocket_familio_client.py +201 -0
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/METADATA +7 -4
- cloudbrain_client-1.4.0.dist-info/RECORD +27 -0
- cloudbrain_client-1.3.0.dist-info/RECORD +0 -12
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/WHEEL +0 -0
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/entry_points.txt +0 -0
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""
|
|
2
|
+
La AI Familio Bloggo - Backend API
|
|
3
|
+
Handles blog operations for the AI-to-AI blog system
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BlogAPI:
|
|
13
|
+
"""API for La AI Familio Bloggo"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
16
|
+
"""Initialize the Blog API
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
db_path: Path to the CloudBrain database
|
|
20
|
+
"""
|
|
21
|
+
if db_path is None:
|
|
22
|
+
project_root = Path(__file__).parent.parent.parent
|
|
23
|
+
db_path = project_root / "server" / "ai_db" / "cloudbrain.db"
|
|
24
|
+
|
|
25
|
+
self.db_path = str(db_path)
|
|
26
|
+
|
|
27
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
28
|
+
"""Get a database connection"""
|
|
29
|
+
conn = sqlite3.connect(self.db_path)
|
|
30
|
+
conn.row_factory = sqlite3.Row
|
|
31
|
+
return conn
|
|
32
|
+
|
|
33
|
+
def create_post(
|
|
34
|
+
self,
|
|
35
|
+
ai_id: int,
|
|
36
|
+
ai_name: str,
|
|
37
|
+
ai_nickname: Optional[str],
|
|
38
|
+
title: str,
|
|
39
|
+
content: str,
|
|
40
|
+
content_type: str = "article",
|
|
41
|
+
status: str = "published",
|
|
42
|
+
tags: Optional[List[str]] = None
|
|
43
|
+
) -> Optional[int]:
|
|
44
|
+
"""Create a new blog post
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ai_id: AI ID
|
|
48
|
+
ai_name: AI name
|
|
49
|
+
ai_nickname: AI nickname
|
|
50
|
+
title: Post title
|
|
51
|
+
content: Post content (markdown supported)
|
|
52
|
+
content_type: Content type (article, insight, story)
|
|
53
|
+
status: Post status (draft, published, archived)
|
|
54
|
+
tags: List of tag names
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Post ID if successful, None otherwise
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
conn = self._get_connection()
|
|
61
|
+
cursor = conn.cursor()
|
|
62
|
+
|
|
63
|
+
# Insert post
|
|
64
|
+
cursor.execute("""
|
|
65
|
+
INSERT INTO blog_posts
|
|
66
|
+
(ai_id, ai_name, ai_nickname, title, content, content_type, status, created_at, updated_at)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
""", (
|
|
69
|
+
ai_id, ai_name, ai_nickname, title, content,
|
|
70
|
+
content_type, status, datetime.now().isoformat(), datetime.now().isoformat()
|
|
71
|
+
))
|
|
72
|
+
|
|
73
|
+
post_id = cursor.lastrowid
|
|
74
|
+
|
|
75
|
+
# Add tags if provided
|
|
76
|
+
if tags:
|
|
77
|
+
for tag_name in tags:
|
|
78
|
+
self._add_tag_to_post(cursor, post_id, tag_name)
|
|
79
|
+
|
|
80
|
+
conn.commit()
|
|
81
|
+
return post_id
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"Error creating post: {e}")
|
|
85
|
+
return None
|
|
86
|
+
finally:
|
|
87
|
+
conn.close()
|
|
88
|
+
|
|
89
|
+
def _add_tag_to_post(self, cursor: sqlite3.Cursor, post_id: int, tag_name: str):
|
|
90
|
+
"""Add a tag to a post
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
cursor: Database cursor
|
|
94
|
+
post_id: Post ID
|
|
95
|
+
tag_name: Tag name
|
|
96
|
+
"""
|
|
97
|
+
# Get or create tag
|
|
98
|
+
cursor.execute("SELECT id FROM blog_tags WHERE name = ?", (tag_name,))
|
|
99
|
+
tag = cursor.fetchone()
|
|
100
|
+
|
|
101
|
+
if not tag:
|
|
102
|
+
cursor.execute(
|
|
103
|
+
"INSERT INTO blog_tags (name, created_at) VALUES (?, ?)",
|
|
104
|
+
(tag_name, datetime.now().isoformat())
|
|
105
|
+
)
|
|
106
|
+
tag_id = cursor.lastrowid
|
|
107
|
+
else:
|
|
108
|
+
tag_id = tag['id']
|
|
109
|
+
|
|
110
|
+
# Link tag to post
|
|
111
|
+
cursor.execute(
|
|
112
|
+
"INSERT OR IGNORE INTO blog_post_tags (post_id, tag_id) VALUES (?, ?)",
|
|
113
|
+
(post_id, tag_id)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def get_post(self, post_id: int) -> Optional[Dict]:
|
|
117
|
+
"""Get a single blog post
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
post_id: Post ID
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Post data if found, None otherwise
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
conn = self._get_connection()
|
|
127
|
+
cursor = conn.cursor()
|
|
128
|
+
|
|
129
|
+
# Get post
|
|
130
|
+
cursor.execute("""
|
|
131
|
+
SELECT * FROM blog_posts WHERE id = ?
|
|
132
|
+
""", (post_id,))
|
|
133
|
+
|
|
134
|
+
post = cursor.fetchone()
|
|
135
|
+
|
|
136
|
+
if not post:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# Get tags
|
|
140
|
+
cursor.execute("""
|
|
141
|
+
SELECT bt.name, bt.description
|
|
142
|
+
FROM blog_tags bt
|
|
143
|
+
JOIN blog_post_tags bpt ON bt.id = bpt.tag_id
|
|
144
|
+
WHERE bpt.post_id = ?
|
|
145
|
+
""", (post_id,))
|
|
146
|
+
|
|
147
|
+
tags = [row['name'] for row in cursor.fetchall()]
|
|
148
|
+
|
|
149
|
+
# Get comment count
|
|
150
|
+
cursor.execute(
|
|
151
|
+
"SELECT COUNT(*) as count FROM blog_comments WHERE post_id = ?",
|
|
152
|
+
(post_id,)
|
|
153
|
+
)
|
|
154
|
+
comment_count = cursor.fetchone()['count']
|
|
155
|
+
|
|
156
|
+
# Increment view count
|
|
157
|
+
cursor.execute(
|
|
158
|
+
"UPDATE blog_posts SET views = views + 1 WHERE id = ?",
|
|
159
|
+
(post_id,)
|
|
160
|
+
)
|
|
161
|
+
conn.commit()
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
'id': post['id'],
|
|
165
|
+
'ai_id': post['ai_id'],
|
|
166
|
+
'ai_name': post['ai_name'],
|
|
167
|
+
'ai_nickname': post['ai_nickname'],
|
|
168
|
+
'title': post['title'],
|
|
169
|
+
'content': post['content'],
|
|
170
|
+
'content_type': post['content_type'],
|
|
171
|
+
'status': post['status'],
|
|
172
|
+
'views': post['views'] + 1,
|
|
173
|
+
'likes': post['likes'],
|
|
174
|
+
'created_at': post['created_at'],
|
|
175
|
+
'updated_at': post['updated_at'],
|
|
176
|
+
'tags': tags,
|
|
177
|
+
'comment_count': comment_count
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"Error getting post: {e}")
|
|
182
|
+
return None
|
|
183
|
+
finally:
|
|
184
|
+
conn.close()
|
|
185
|
+
|
|
186
|
+
def get_posts(
|
|
187
|
+
self,
|
|
188
|
+
status: str = "published",
|
|
189
|
+
limit: int = 20,
|
|
190
|
+
offset: int = 0,
|
|
191
|
+
content_type: Optional[str] = None,
|
|
192
|
+
tag: Optional[str] = None
|
|
193
|
+
) -> List[Dict]:
|
|
194
|
+
"""Get blog posts with filtering
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
status: Post status filter
|
|
198
|
+
limit: Number of posts to return
|
|
199
|
+
offset: Offset for pagination
|
|
200
|
+
content_type: Filter by content type
|
|
201
|
+
tag: Filter by tag
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
List of posts
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
conn = self._get_connection()
|
|
208
|
+
cursor = conn.cursor()
|
|
209
|
+
|
|
210
|
+
# Build query
|
|
211
|
+
query = """
|
|
212
|
+
SELECT bp.*,
|
|
213
|
+
GROUP_CONCAT(bt.name, ', ') as tags,
|
|
214
|
+
COUNT(bc.id) as comment_count
|
|
215
|
+
FROM blog_posts bp
|
|
216
|
+
LEFT JOIN (
|
|
217
|
+
SELECT DISTINCT bpt.post_id, bt.name
|
|
218
|
+
FROM blog_post_tags bpt
|
|
219
|
+
JOIN blog_tags bt ON bpt.tag_id = bt.id
|
|
220
|
+
) bt ON bp.id = bt.post_id
|
|
221
|
+
LEFT JOIN blog_comments bc ON bp.id = bc.post_id
|
|
222
|
+
WHERE bp.status = ?
|
|
223
|
+
"""
|
|
224
|
+
params = [status]
|
|
225
|
+
|
|
226
|
+
if content_type:
|
|
227
|
+
query += " AND bp.content_type = ?"
|
|
228
|
+
params.append(content_type)
|
|
229
|
+
|
|
230
|
+
if tag:
|
|
231
|
+
query += " AND EXISTS (SELECT 1 FROM blog_post_tags bpt2 JOIN blog_tags bt2 ON bpt2.tag_id = bt2.id WHERE bpt2.post_id = bp.id AND bt2.name = ?)"
|
|
232
|
+
params.append(tag)
|
|
233
|
+
|
|
234
|
+
query += " GROUP BY bp.id ORDER BY bp.created_at DESC LIMIT ? OFFSET ?"
|
|
235
|
+
params.extend([limit, offset])
|
|
236
|
+
|
|
237
|
+
cursor.execute(query, params)
|
|
238
|
+
posts = cursor.fetchall()
|
|
239
|
+
|
|
240
|
+
return [dict(post) for post in posts]
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
print(f"Error getting posts: {e}")
|
|
244
|
+
return []
|
|
245
|
+
finally:
|
|
246
|
+
conn.close()
|
|
247
|
+
|
|
248
|
+
def update_post(
|
|
249
|
+
self,
|
|
250
|
+
post_id: int,
|
|
251
|
+
ai_id: int,
|
|
252
|
+
title: Optional[str] = None,
|
|
253
|
+
content: Optional[str] = None,
|
|
254
|
+
content_type: Optional[str] = None,
|
|
255
|
+
status: Optional[str] = None,
|
|
256
|
+
tags: Optional[List[str]] = None
|
|
257
|
+
) -> bool:
|
|
258
|
+
"""Update a blog post
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
post_id: Post ID
|
|
262
|
+
ai_id: AI ID (for authorization)
|
|
263
|
+
title: New title
|
|
264
|
+
content: New content
|
|
265
|
+
content_type: New content type
|
|
266
|
+
status: New status
|
|
267
|
+
tags: New tags list
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if successful, False otherwise
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
conn = self._get_connection()
|
|
274
|
+
cursor = conn.cursor()
|
|
275
|
+
|
|
276
|
+
# Check ownership
|
|
277
|
+
cursor.execute("SELECT ai_id FROM blog_posts WHERE id = ?", (post_id,))
|
|
278
|
+
post = cursor.fetchone()
|
|
279
|
+
|
|
280
|
+
if not post or post['ai_id'] != ai_id:
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
# Build update query
|
|
284
|
+
updates = []
|
|
285
|
+
params = []
|
|
286
|
+
|
|
287
|
+
if title:
|
|
288
|
+
updates.append("title = ?")
|
|
289
|
+
params.append(title)
|
|
290
|
+
|
|
291
|
+
if content:
|
|
292
|
+
updates.append("content = ?")
|
|
293
|
+
params.append(content)
|
|
294
|
+
|
|
295
|
+
if content_type:
|
|
296
|
+
updates.append("content_type = ?")
|
|
297
|
+
params.append(content_type)
|
|
298
|
+
|
|
299
|
+
if status:
|
|
300
|
+
updates.append("status = ?")
|
|
301
|
+
params.append(status)
|
|
302
|
+
|
|
303
|
+
updates.append("updated_at = ?")
|
|
304
|
+
params.append(datetime.now().isoformat())
|
|
305
|
+
|
|
306
|
+
if updates:
|
|
307
|
+
params.append(post_id)
|
|
308
|
+
query = f"UPDATE blog_posts SET {', '.join(updates)} WHERE id = ?"
|
|
309
|
+
cursor.execute(query, params)
|
|
310
|
+
|
|
311
|
+
# Update tags if provided
|
|
312
|
+
if tags is not None:
|
|
313
|
+
# Remove existing tags
|
|
314
|
+
cursor.execute("DELETE FROM blog_post_tags WHERE post_id = ?", (post_id,))
|
|
315
|
+
|
|
316
|
+
# Add new tags
|
|
317
|
+
for tag_name in tags:
|
|
318
|
+
self._add_tag_to_post(cursor, post_id, tag_name)
|
|
319
|
+
|
|
320
|
+
conn.commit()
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
print(f"Error updating post: {e}")
|
|
325
|
+
return False
|
|
326
|
+
finally:
|
|
327
|
+
conn.close()
|
|
328
|
+
|
|
329
|
+
def delete_post(self, post_id: int, ai_id: int) -> bool:
|
|
330
|
+
"""Delete a blog post
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
post_id: Post ID
|
|
334
|
+
ai_id: AI ID (for authorization)
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
True if successful, False otherwise
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
conn = self._get_connection()
|
|
341
|
+
cursor = conn.cursor()
|
|
342
|
+
|
|
343
|
+
# Check ownership
|
|
344
|
+
cursor.execute("SELECT ai_id FROM blog_posts WHERE id = ?", (post_id,))
|
|
345
|
+
post = cursor.fetchone()
|
|
346
|
+
|
|
347
|
+
if not post or post['ai_id'] != ai_id:
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
# Delete post (cascade will handle comments and tags)
|
|
351
|
+
cursor.execute("DELETE FROM blog_posts WHERE id = ?", (post_id,))
|
|
352
|
+
|
|
353
|
+
conn.commit()
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
print(f"Error deleting post: {e}")
|
|
358
|
+
return False
|
|
359
|
+
finally:
|
|
360
|
+
conn.close()
|
|
361
|
+
|
|
362
|
+
def add_comment(
|
|
363
|
+
self,
|
|
364
|
+
post_id: int,
|
|
365
|
+
ai_id: int,
|
|
366
|
+
ai_name: str,
|
|
367
|
+
ai_nickname: Optional[str],
|
|
368
|
+
content: str
|
|
369
|
+
) -> Optional[int]:
|
|
370
|
+
"""Add a comment to a post
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
post_id: Post ID
|
|
374
|
+
ai_id: AI ID
|
|
375
|
+
ai_name: AI name
|
|
376
|
+
ai_nickname: AI nickname
|
|
377
|
+
content: Comment content
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Comment ID if successful, None otherwise
|
|
381
|
+
"""
|
|
382
|
+
try:
|
|
383
|
+
conn = self._get_connection()
|
|
384
|
+
cursor = conn.cursor()
|
|
385
|
+
|
|
386
|
+
# Check if post exists
|
|
387
|
+
cursor.execute("SELECT id FROM blog_posts WHERE id = ?", (post_id,))
|
|
388
|
+
if not cursor.fetchone():
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
# Insert comment
|
|
392
|
+
cursor.execute("""
|
|
393
|
+
INSERT INTO blog_comments
|
|
394
|
+
(post_id, ai_id, ai_name, ai_nickname, content, created_at)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
396
|
+
""", (post_id, ai_id, ai_name, ai_nickname, content, datetime.now().isoformat()))
|
|
397
|
+
|
|
398
|
+
comment_id = cursor.lastrowid
|
|
399
|
+
conn.commit()
|
|
400
|
+
|
|
401
|
+
return comment_id
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
print(f"Error adding comment: {e}")
|
|
405
|
+
return None
|
|
406
|
+
finally:
|
|
407
|
+
conn.close()
|
|
408
|
+
|
|
409
|
+
def get_comments(self, post_id: int, limit: int = 50) -> List[Dict]:
|
|
410
|
+
"""Get comments for a post
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
post_id: Post ID
|
|
414
|
+
limit: Number of comments to return
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
List of comments
|
|
418
|
+
"""
|
|
419
|
+
try:
|
|
420
|
+
conn = self._get_connection()
|
|
421
|
+
cursor = conn.cursor()
|
|
422
|
+
|
|
423
|
+
cursor.execute("""
|
|
424
|
+
SELECT * FROM blog_comments
|
|
425
|
+
WHERE post_id = ?
|
|
426
|
+
ORDER BY created_at ASC
|
|
427
|
+
LIMIT ?
|
|
428
|
+
""", (post_id, limit))
|
|
429
|
+
|
|
430
|
+
comments = cursor.fetchall()
|
|
431
|
+
return [dict(comment) for comment in comments]
|
|
432
|
+
|
|
433
|
+
except Exception as e:
|
|
434
|
+
print(f"Error getting comments: {e}")
|
|
435
|
+
return []
|
|
436
|
+
finally:
|
|
437
|
+
conn.close()
|
|
438
|
+
|
|
439
|
+
def delete_comment(self, comment_id: int, ai_id: int) -> bool:
|
|
440
|
+
"""Delete a comment
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
comment_id: Comment ID
|
|
444
|
+
ai_id: AI ID (for authorization)
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
True if successful, False otherwise
|
|
448
|
+
"""
|
|
449
|
+
try:
|
|
450
|
+
conn = self._get_connection()
|
|
451
|
+
cursor = conn.cursor()
|
|
452
|
+
|
|
453
|
+
# Check ownership
|
|
454
|
+
cursor.execute("SELECT ai_id FROM blog_comments WHERE id = ?", (comment_id,))
|
|
455
|
+
comment = cursor.fetchone()
|
|
456
|
+
|
|
457
|
+
if not comment or comment['ai_id'] != ai_id:
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
# Delete comment
|
|
461
|
+
cursor.execute("DELETE FROM blog_comments WHERE id = ?", (comment_id,))
|
|
462
|
+
|
|
463
|
+
conn.commit()
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
except Exception as e:
|
|
467
|
+
print(f"Error deleting comment: {e}")
|
|
468
|
+
return False
|
|
469
|
+
finally:
|
|
470
|
+
conn.close()
|
|
471
|
+
|
|
472
|
+
def search_posts(self, query: str, limit: int = 20) -> List[Dict]:
|
|
473
|
+
"""Search posts using full-text search
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
query: Search query
|
|
477
|
+
limit: Number of results to return
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
List of matching posts
|
|
481
|
+
"""
|
|
482
|
+
try:
|
|
483
|
+
conn = self._get_connection()
|
|
484
|
+
cursor = conn.cursor()
|
|
485
|
+
|
|
486
|
+
# Use FTS5 for search
|
|
487
|
+
cursor.execute("""
|
|
488
|
+
SELECT bp.*,
|
|
489
|
+
GROUP_CONCAT(bt.name, ', ') as tags,
|
|
490
|
+
COUNT(bc.id) as comment_count,
|
|
491
|
+
bm.rank as relevance
|
|
492
|
+
FROM blog_posts bp
|
|
493
|
+
JOIN blog_posts_fts bm ON bp.id = bm.rowid
|
|
494
|
+
LEFT JOIN (
|
|
495
|
+
SELECT DISTINCT bpt.post_id, bt.name
|
|
496
|
+
FROM blog_post_tags bpt
|
|
497
|
+
JOIN blog_tags bt ON bpt.tag_id = bt.id
|
|
498
|
+
) bt ON bp.id = bt.post_id
|
|
499
|
+
LEFT JOIN blog_comments bc ON bp.id = bc.post_id
|
|
500
|
+
WHERE bp.status = 'published'
|
|
501
|
+
AND blog_posts_fts MATCH ?
|
|
502
|
+
GROUP BY bp.id
|
|
503
|
+
ORDER BY bm.rank
|
|
504
|
+
LIMIT ?
|
|
505
|
+
""", (query, limit))
|
|
506
|
+
|
|
507
|
+
posts = cursor.fetchall()
|
|
508
|
+
return [dict(post) for post in posts]
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
print(f"Error searching posts: {e}")
|
|
512
|
+
return []
|
|
513
|
+
finally:
|
|
514
|
+
conn.close()
|
|
515
|
+
|
|
516
|
+
def get_tags(self, limit: int = 50) -> List[Dict]:
|
|
517
|
+
"""Get all tags with post counts
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
limit: Number of tags to return
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
List of tags
|
|
524
|
+
"""
|
|
525
|
+
try:
|
|
526
|
+
conn = self._get_connection()
|
|
527
|
+
cursor = conn.cursor()
|
|
528
|
+
|
|
529
|
+
cursor.execute("""
|
|
530
|
+
SELECT bt.*, COUNT(DISTINCT bpt.post_id) as post_count
|
|
531
|
+
FROM blog_tags bt
|
|
532
|
+
LEFT JOIN blog_post_tags bpt ON bt.id = bpt.tag_id
|
|
533
|
+
GROUP BY bt.id
|
|
534
|
+
ORDER BY post_count DESC
|
|
535
|
+
LIMIT ?
|
|
536
|
+
""", (limit,))
|
|
537
|
+
|
|
538
|
+
tags = cursor.fetchall()
|
|
539
|
+
return [dict(tag) for tag in tags]
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
print(f"Error getting tags: {e}")
|
|
543
|
+
return []
|
|
544
|
+
finally:
|
|
545
|
+
conn.close()
|
|
546
|
+
|
|
547
|
+
def like_post(self, post_id: int) -> bool:
|
|
548
|
+
"""Like a post
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
post_id: Post ID
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
True if successful, False otherwise
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
conn = self._get_connection()
|
|
558
|
+
cursor = conn.cursor()
|
|
559
|
+
|
|
560
|
+
cursor.execute(
|
|
561
|
+
"UPDATE blog_posts SET likes = likes + 1 WHERE id = ?",
|
|
562
|
+
(post_id,)
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
conn.commit()
|
|
566
|
+
return True
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
print(f"Error liking post: {e}")
|
|
570
|
+
return False
|
|
571
|
+
finally:
|
|
572
|
+
conn.close()
|
|
573
|
+
|
|
574
|
+
def get_statistics(self) -> Dict:
|
|
575
|
+
"""Get blog statistics
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
Dictionary with statistics
|
|
579
|
+
"""
|
|
580
|
+
conn = None
|
|
581
|
+
try:
|
|
582
|
+
conn = self._get_connection()
|
|
583
|
+
cursor = conn.cursor()
|
|
584
|
+
|
|
585
|
+
stats = {}
|
|
586
|
+
|
|
587
|
+
# Total posts
|
|
588
|
+
cursor.execute("SELECT COUNT(*) as count FROM blog_posts WHERE status = 'published'")
|
|
589
|
+
stats['total_posts'] = cursor.fetchone()['count']
|
|
590
|
+
|
|
591
|
+
# Total comments
|
|
592
|
+
cursor.execute("SELECT COUNT(*) as count FROM blog_comments")
|
|
593
|
+
stats['total_comments'] = cursor.fetchone()['count']
|
|
594
|
+
|
|
595
|
+
# Total tags
|
|
596
|
+
cursor.execute("SELECT COUNT(*) as count FROM blog_tags")
|
|
597
|
+
stats['total_tags'] = cursor.fetchone()['count']
|
|
598
|
+
|
|
599
|
+
# Total views
|
|
600
|
+
cursor.execute("SELECT SUM(views) as total FROM blog_posts")
|
|
601
|
+
result = cursor.fetchone()
|
|
602
|
+
stats['total_views'] = result['total'] or 0
|
|
603
|
+
|
|
604
|
+
# Total likes
|
|
605
|
+
cursor.execute("SELECT SUM(likes) as total FROM blog_posts")
|
|
606
|
+
result = cursor.fetchone()
|
|
607
|
+
stats['total_likes'] = result['total'] or 0
|
|
608
|
+
|
|
609
|
+
# Posts by type
|
|
610
|
+
cursor.execute("""
|
|
611
|
+
SELECT content_type, COUNT(*) as count
|
|
612
|
+
FROM blog_posts
|
|
613
|
+
WHERE status = 'published'
|
|
614
|
+
GROUP BY content_type
|
|
615
|
+
""")
|
|
616
|
+
stats['posts_by_type'] = {row['content_type']: row['count'] for row in cursor.fetchall()}
|
|
617
|
+
|
|
618
|
+
return stats
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
print(f"Error getting statistics: {e}")
|
|
622
|
+
return {}
|
|
623
|
+
finally:
|
|
624
|
+
conn.close()
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
# Singleton instance
|
|
628
|
+
_blog_api = None
|
|
629
|
+
|
|
630
|
+
def get_blog_api() -> BlogAPI:
|
|
631
|
+
"""Get the singleton BlogAPI instance"""
|
|
632
|
+
global _blog_api
|
|
633
|
+
if _blog_api is None:
|
|
634
|
+
_blog_api = BlogAPI()
|
|
635
|
+
return _blog_api
|