Skip to content

Commit d7d5322

Browse files
authored
Request limit (#19)
* feat: implement rate limiting to prevent abuse This commit introduces rate limiting to the linebot to prevent abuse and ensure fair usage. - Added a new `redis_client.py` to manage the Redis connection. - Added a `RateLimiter` class in `rate_limiter.py` to handle rate limiting logic using Redis. - Modified `text_handler.py` to integrate the `RateLimiter`. - Implemented a check to determine if a user has exceeded their request limit. - If the limit is exceeded, a message is sent to the user informing them of the rate limit and the time remaining until they can make another request. - Added `flex_message.py` to convert flex message to json. - Added `__init__.py` to export the utils. - Added `redis` to `pyproject.toml`. - Added `*.rdb` to `.gitignore`. * feat(linebot): add redis support for rate limiting This commit introduces Redis integration for rate limiting in the Line Bot application. It includes the following changes: - Added Redis configuration parameters (REDIS_HOST, REDIS_PORT, REDIS_DB) to the .env.example file. - Updated the README.md file to include instructions for installing sqlite3 and redis. - Modified the redis_client.py file to read Redis configuration from environment variables, allowing for flexible deployment. The addition of Redis enables the implementation of robust rate limiting mechanisms, preventing abuse and ensuring the stability of the Line Bot service.
1 parent 61dd984 commit d7d5322

10 files changed

Lines changed: 129 additions & 9 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ __pycache__
66

77
# db
88
sql_app.db
9+
*.rdb
910

1011
# logs
11-
logs/
12+
logs/

linebot/.env.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
DIFY_API_KEY=
22
LINE_CHANNEL_ACCESS_TOKEN=
33
LINE_CHANNEL_SECRET=
4-
<<<<<<< HEAD
5-
=======
64
OPENAI_API_KEY=
7-
>>>>>>> d61705a (feat: add OPENAI_API_KEY and GEMINI_API_KEY to .env.example)
8-
GEMINI_API_KEY=
5+
GEMINI_API_KEY=
6+
REDIS_HOST=
7+
REDIS_PORT=
8+
REDIS_DB=
9+

linebot/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Usage
44

5+
Make sure you have sqlite3 and redis installed on your system.
6+
57
1. Copy `.env.example` to `.env` and replace the values with your own
68
```cli
79
cp .env.example .env
@@ -14,4 +16,4 @@
1416
3. Run the service
1517
```bash
1618
poetry run uvicorn app.main:app --reload
17-
```
19+
```

linebot/app/db/redis_client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
3+
from redis import Redis
4+
from dotenv import load_dotenv
5+
6+
load_dotenv()
7+
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
8+
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
9+
REDIS_DB = int(os.getenv("REDIS_DB", 0))
10+
11+
redis_client: Redis = Redis(
12+
host=REDIS_HOST,
13+
port=REDIS_PORT,
14+
db=REDIS_DB,
15+
decode_responses=True,
16+
retry_on_timeout=True,
17+
socket_connect_timeout=5,
18+
socket_timeout=5,
19+
)

linebot/app/services/handlers/text_handler.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
"""處理文字訊息的模組"""
22

33
import json
4+
import random
5+
46
from linebot.models import TextSendMessage, FlexSendMessage
7+
58
from .common import create_quick_reply, COMMANDS
69
from ...api.dify import inference
10+
from ...config.line_config import line_bot_api
711
from ...config.logger import get_logger
12+
from ...services.utils.rate_limiter import RateLimiter
813

9-
# 取得模組的日誌記錄器
1014
logger = get_logger(__name__)
15+
rate_limiter = RateLimiter(max_requests=50, window_seconds=3600)
1116

1217

1318
def handle_text_message(event):
1419
"""處理文字訊息"""
1520
try:
1621
user_input = event.message.text
1722
user_id = event.source.user_id
23+
user_profile = line_bot_api.get_profile(user_id)
24+
user_display_name = user_profile.display_name
1825

1926
# 產生回應訊息
2027
quick_reply = None
@@ -23,6 +30,31 @@ def handle_text_message(event):
2330
quick_reply = create_quick_reply()
2431
return [TextSendMessage(text=response_text, quick_reply=quick_reply)]
2532

33+
# 檢查使用次數,決定要不要 inference
34+
if not rate_limiter.is_allowed(user_id):
35+
remaining_time = rate_limiter.time_to_reset(user_id)
36+
minutes, seconds = divmod(remaining_time, 60)
37+
response_text_list = [
38+
f"我知道你很喜歡跟我聊天,不過我已經有點累了,請稍等 {minutes}{seconds} 秒再來找我吧!",
39+
f"親愛ㄉ {user_display_name},我知道你很喜歡跟我聊天,不過我已經有點累了,請稍等 {minutes}{seconds} 秒再來找我吧!",
40+
f"嘿嘿,{user_display_name},我知道你很喜歡跟我聊天,不過我已經有點累了,請稍等 {minutes}{seconds} 秒再來找我吧!",
41+
f"{user_display_name}~你太常敲我了啦,我要先去喘口氣,{minutes}{seconds} 秒後我就回來哦!",
42+
f"休息是為了走更長遠的路,{user_display_name} 我們等一下再聊~還差 {minutes}{seconds} 秒喔!",
43+
f"等等等等,太激烈了!我需要 {minutes}{seconds} 秒冷靜一下 🧘‍♂️",
44+
f"{user_display_name},你是我見過最愛聊天的使用者了 ❤️ 但我真的需要休息一下,等 {minutes}{seconds} 秒後再見吧!",
45+
f"訊息太多我快招架不住了!休息一下吧,還要等 {minutes}{seconds} 秒喔!",
46+
f"{user_display_name},我剛剛問了一下伺服器,它說:我真的需要靜一靜,請等 {minutes}{seconds} 秒。",
47+
f"啊~系統快燒起來了!我先去喝口水,{minutes}{seconds} 秒後我們繼續~",
48+
]
49+
response_text = (
50+
random.choice(response_text_list)
51+
if user_display_name
52+
else response_text_list[0]
53+
)
54+
return [
55+
TextSendMessage(text=response_text, quick_reply=create_quick_reply())
56+
]
57+
2658
# 處理一般查詢
2759
response_text = inference(user_input, user_id)
2860
quick_reply = create_quick_reply()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .flex_message import flex_message_convert_to_json
2+
from .rate_limiter import RateLimiter
3+
4+
__all__ = [
5+
"flex_message_convert_to_json",
6+
"RateLimiter",
7+
]
File renamed without changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import redis
2+
from typing import cast
3+
4+
from app.db.redis_client import redis_client
5+
6+
7+
class RateLimiter:
8+
def __init__(self, max_requests: int = 100, window_seconds: int = 3600):
9+
self.redis: redis.Redis = redis_client
10+
self.max_requests = max_requests
11+
self.window_seconds = window_seconds
12+
13+
def is_allowed(self, user_id: str) -> bool:
14+
key = f"rate_limit:{user_id}"
15+
raw = self.redis.incr(key)
16+
count = cast(int, raw) # 防止 linter 哇哇叫
17+
if count == 1:
18+
self.redis.expire(key, self.window_seconds)
19+
return count <= self.max_requests
20+
21+
def time_to_reset(self, user_id: str) -> int:
22+
raw = self.redis.ttl(f"rate_limit:{user_id}")
23+
ttl = cast(int, raw)
24+
return max(ttl, 0)
25+
26+
27+
if __name__ == "__main__":
28+
# Example
29+
rate_limiter = RateLimiter(max_requests=5, window_seconds=60)
30+
user_id = "user123"
31+
32+
for i in range(7):
33+
if rate_limiter.is_allowed(user_id):
34+
print(f"Request {i + 1} allowed for user {user_id}.")
35+
else:
36+
print(f"Request {i + 1} denied for user {user_id}.")
37+
print(f"Time to reset: {rate_limiter.time_to_reset(user_id)} seconds.")

linebot/poetry.lock

Lines changed: 22 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

linebot/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ uvicorn = "^0.32.0"
1414
python-dotenv = "^1.0.1"
1515
sqlalchemy = "^2.0.36"
1616
openai = "^1.70.0"
17+
redis = "^6.2.0"
1718

1819

1920
[build-system]

0 commit comments

Comments
 (0)