Compare commits

...

10 commits

Author SHA1 Message Date
yuuki
dc1f156025 uidの調整 2024-11-24 02:04:03 +00:00
yuuki
161343909a fastapiの更新 2024-11-15 17:41:07 +09:00
yuuki
d4575837d4 uuidでインデックスを作成しない 2024-11-01 13:37:52 +09:00
yuuki
071ffab88a 誤字fix 2024-10-31 14:48:14 +09:00
yuuki
7a3db8c48c リファクタリング 2024-10-31 14:32:07 +09:00
yuuki
d5ca0a68da Python 3.13に対応 2024-10-16 03:54:02 +00:00
yuuki
bc6b871b6c 細かい修正 2024-08-14 18:43:02 +09:00
yuuki
36247d7607 /ninjaコマンドの有効期限を72時間に設定する 2024-07-29 17:36:03 +09:00
yuuki
dc6dd3fe3f 削除ボタンを隠すコマンドを追加 2024-07-13 23:22:28 +00:00
yuuki
c22936c670 uniqueにしたほうがいいのか 2024-06-25 03:39:07 +09:00
5 changed files with 82 additions and 25 deletions

View file

@ -1,4 +1,4 @@
FROM python:3 FROM python:3.13.0
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN pip install -r requirements.txt RUN pip install -r requirements.txt

33
README.md Normal file
View file

@ -0,0 +1,33 @@
# LiteY
軽量な掲示板です
## デバッグの開始
依存関係をインストールします
```bash
pip install -r requirements.txt
```
データベースを起動します
```bash
docker run --detach \
--name litey-mongo-debug \
--volume litey-mongo-debug:/data/db \
--publish 27017:27017 \
mongo
```
サーバーを起動します
```bash
fastapi dev
```
## デプロイ
今すぐデプロイ!
- https://litey.trade/

64
app.py
View file

@ -1,11 +1,12 @@
from typing import Callable, Awaitable, Optional, List from typing import Callable, Awaitable, Optional, List
from datetime import datetime, timezone from datetime import datetime, timezone, timedelta
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from os import environ from os import environ
from urllib.parse import urlparse from urllib.parse import urlparse
from base64 import b64encode from base64 import b64encode
from re import escape, compile, IGNORECASE from re import escape, compile, IGNORECASE
from pprint import pprint
from requests import get from requests import get
from fastapi import FastAPI, Request, Response, status from fastapi import FastAPI, Request, Response, status
@ -13,6 +14,7 @@ from fastapi.responses import JSONResponse, Response, PlainTextResponse, FileRes
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel
from pymongo import MongoClient, DESCENDING from pymongo import MongoClient, DESCENDING
from contextlib import asynccontextmanager
# Jinja2 # Jinja2
@ -20,7 +22,7 @@ def ip_to_uid(ip: Optional[str]) -> str:
if not ip: if not ip:
return "-" return "-"
return b64encode(ip.encode("utf-8")).decode("utf-8")[:11] return b64encode(ip.encode("utf-8")).decode("utf-8")[-11:]
def replace_ng_words(src: str, ng_words: List[str]) -> str: def replace_ng_words(src: str, ng_words: List[str]) -> str:
result = src result = src
@ -39,21 +41,39 @@ def content_to_linksets(content: str) -> str:
groups = pattern.findall(content) groups = pattern.findall(content)
return "\n".join(groups) return "\n".join(groups)
def is_over_n_hours(src: datetime, hours: int) -> bool:
now = datetime.now()
return now - src.replace(tzinfo=None) > timedelta(hours=hours)
# 初期化 # 初期化
templates = Jinja2Templates("templates") ctx = {}
templates.env.filters["ip_to_uid"] = ip_to_uid @asynccontextmanager
templates.env.filters["replace_ng_words"] = replace_ng_words async def lifespan(app: FastAPI):
templates.env.filters["content_to_linksets"] = content_to_linksets ctx["templates"] = Jinja2Templates("templates")
mongo_client = MongoClient( ctx["templates"].env.filters["ip_to_uid"] = ip_to_uid
environ.get("MONGO_URI", "mongodb://127.0.0.1:27017/"), ctx["templates"].env.filters["replace_ng_words"] = replace_ng_words
username=environ.get("MONGO_USER"), ctx["templates"].env.filters["content_to_linksets"] = content_to_linksets
password=environ.get("MONGO_PASSWORD") ctx["templates"].env.filters["fromisoformat"] = datetime.fromisoformat
) ctx["templates"].env.filters["is_over_n_hours"] = is_over_n_hours
mongo_client.litey.ngs.create_index("word", unique=True) ctx["mongo_client"] = MongoClient(
environ.get("MONGO_URI", "mongodb://127.0.0.1:27017/"),
username=environ.get("MONGO_USER"),
password=environ.get("MONGO_PASSWORD")
)
#uuid重複する考えすぎ
#ctx["mongo_client"].litey.notes.create_index("id", unique=True)
ctx["mongo_client"].litey.ngs.create_index("word", unique=True)
pprint(ctx)
yield
ctx["mongo_client"].close()
ctx.clear()
# スニペット # スニペット
@ -95,18 +115,18 @@ def get_ip(req: Request) -> str:
def get_litey_notes(id: str = None) -> List[dict]: def get_litey_notes(id: str = None) -> List[dict]:
if not id: if not id:
cursor = mongo_client.litey.notes.find({}, { "_id": False }).sort("date", DESCENDING) cursor = ctx["mongo_client"].litey.notes.find({}, { "_id": False }).sort("date", DESCENDING)
return list(cursor) return list(cursor)
return mongo_client.litey.notes.find_one({ "id": id }, { "_id": False }) return ctx["mongo_client"].litey.notes.find_one({ "id": id }, { "_id": False })
def get_ng_words() -> List[str]: def get_ng_words() -> List[str]:
cursor = mongo_client.litey.ngs.find({}, { "_id": False }) cursor = ctx["mongo_client"].litey.ngs.find({}, { "_id": False })
return [ng["word"] for ng in list(cursor) if "word" in ng] return [ng["word"] for ng in list(cursor) if "word" in ng]
# FastAPI # FastAPI
app = FastAPI() app = FastAPI(lifespan=lifespan)
@app.middleware("http") @app.middleware("http")
async def cors_handler(req: Request, call_next: Callable[[Request], Awaitable[Response]]): async def cors_handler(req: Request, call_next: Callable[[Request], Awaitable[Response]]):
@ -132,7 +152,7 @@ async def api_get(id: str = None):
@app.post("/api/litey/post") @app.post("/api/litey/post")
async def api_post(item: LiteYItem, req: Request): async def api_post(item: LiteYItem, req: Request):
mongo_client.litey.notes.insert_one({ ctx["mongo_client"].litey.notes.insert_one({
"id": str(uuid4()), "id": str(uuid4()),
"content": item.content, "content": item.content,
"date": datetime.now().astimezone(timezone.utc).isoformat(), "date": datetime.now().astimezone(timezone.utc).isoformat(),
@ -143,7 +163,9 @@ async def api_post(item: LiteYItem, req: Request):
@app.post("/api/litey/delete") @app.post("/api/litey/delete")
async def api_delete(item: LiteYDeleteItem): async def api_delete(item: LiteYDeleteItem):
mongo_client.litey.notes.delete_one({ "id": item.id }) ctx["mongo_client"].litey.notes.delete_one({
"id": item.id
})
return PlainTextResponse("OK") return PlainTextResponse("OK")
@ -170,7 +192,7 @@ async def api_ng_get():
@app.post("/api/ng/post") @app.post("/api/ng/post")
async def api_ng_post(item: NGItem): async def api_ng_post(item: NGItem):
mongo_client.litey.ngs.insert_one({ ctx["mongo_client"].litey.ngs.insert_one({
"word": item.word "word": item.word
}) })
@ -178,7 +200,7 @@ async def api_ng_post(item: NGItem):
@app.post("/api/ng/delete") @app.post("/api/ng/delete")
async def api_ng_delete(item: NGItem): async def api_ng_delete(item: NGItem):
mongo_client.litey.ngs.delete_one({ ctx["mongo_client"].litey.ngs.delete_one({
"word": item.word "word": item.word
}) })
@ -186,7 +208,7 @@ async def api_ng_delete(item: NGItem):
@app.get("/") @app.get("/")
async def home(req: Request): async def home(req: Request):
res = templates.TemplateResponse(req, "index.html", { res = ctx["templates"].TemplateResponse(req, "index.html", {
"notes": get_litey_notes(), "notes": get_litey_notes(),
"ng_words": get_ng_words() "ng_words": get_ng_words()
}) })

View file

@ -1,4 +1,4 @@
requests==2.32.3 requests==2.32.3
fastapi==0.111.0 fastapi[standard]==0.115.5
pymongo==4.7.3 pymongo==4.10.1
Jinja2==3.1.4 Jinja2==3.1.4

View file

@ -18,7 +18,9 @@
<div>{{ note.content | replace_ng_words(ng_words) }}</div> <div>{{ note.content | replace_ng_words(ng_words) }}</div>
<div id="attachments" data-linksets="{{ note.content | content_to_linksets | urlencode() }}"></div> <div id="attachments" data-linksets="{{ note.content | content_to_linksets | urlencode() }}"></div>
<code id="date" data-date="{{ note.date | urlencode() }}"></code> <code id="date" data-date="{{ note.date | urlencode() }}"></code>
<input type="submit" value="削除" onclick="noteDelete(this);" data-id="{{ note.id | urlencode() }}" data-preview="{{ note.content | urlencode() }}"> {% if not ("/ninja" in note.content and not note.date | fromisoformat | is_over_n_hours(72)) %}
<input type="submit" value="削除" onclick="noteDelete(this);" data-id="{{ note.id | urlencode() }}" data-preview="{{ note.content | urlencode() }}">
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</body> </body>