Skip to content

Commit 1da862c

Browse files
feat(webhook): send multiple webhook for user usages
1 parent 7509a9d commit 1da862c

File tree

7 files changed

+126
-33
lines changed

7 files changed

+126
-33
lines changed

.env.example

+3-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ UVICORN_PORT = 8000
8282
# NOTIFY_USER_DATA_USED_RESET = True
8383
# NOTIFY_USER_SUB_REVOKED = True
8484
# NOTIFY_IF_DATA_USAGE_PERCENT_REACHED = True
85-
# NOTIFY_IF_DAYS_LEF_REACHED = True
85+
# NOTIFY_IF_DAYS_LEFT_REACHED = True
8686
# NOTIFY_LOGIN = True
8787

8888
## Whitelist of IPs/hosts to disable login notifications
@@ -95,6 +95,8 @@ UVICORN_PORT = 8000
9595
# If You Want To Send Webhook To Multiple Server Add Multi Address
9696
# WEBHOOK_ADDRESS = "http://127.0.0.1:9000/,http://127.0.0.1:9001/"
9797
# WEBHOOK_SECRET = "something-very-very-secret"
98+
# NOTIFY_DAYS_LEFT=3,7
99+
# NOTIFY_REACHED_USAGE_PERCENT=80,90
98100

99101
# VITE_BASE_API="https://example.com/api/"
100102
# JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 1440

app/db/crud.py

+48-20
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
)
4242
from app.models.user_template import UserTemplateCreate, UserTemplateModify
4343
from app.utils.helpers import calculate_expiration_days, calculate_usage_percent
44-
from app.utils.notification import Notification
4544
from config import NOTIFY_DAYS_LEFT, NOTIFY_REACHED_USAGE_PERCENT, USERS_AUTODELETE_DAYS
4645

4746

@@ -455,8 +454,8 @@ def update_user(db: Session, dbuser: User, modify: UserModify) -> User:
455454
if modify.inbounds:
456455
for proxy_type, tags in modify.excluded_inbounds.items():
457456
dbproxy = db.query(Proxy) \
458-
.where(Proxy.user == dbuser, Proxy.type == proxy_type) \
459-
.first() or added_proxies.get(proxy_type)
457+
.where(Proxy.user == dbuser, Proxy.type == proxy_type) \
458+
.first() or added_proxies.get(proxy_type)
460459
if dbproxy:
461460
dbproxy.excluded_inbounds = [get_or_create_inbound(db, tag) for tag in tags]
462461

@@ -470,9 +469,13 @@ def update_user(db: Session, dbuser: User, modify: UserModify) -> User:
470469
if dbuser.status != UserStatus.on_hold:
471470
dbuser.status = UserStatus.active
472471

473-
if not dbuser.data_limit or (calculate_usage_percent(
474-
dbuser.used_traffic, dbuser.data_limit) < NOTIFY_REACHED_USAGE_PERCENT):
475-
delete_notification_reminder_by_type(db, dbuser.id, ReminderType.data_usage)
472+
for percent in sorted(NOTIFY_REACHED_USAGE_PERCENT, reverse=True):
473+
if not dbuser.data_limit or (calculate_usage_percent(
474+
dbuser.used_traffic, dbuser.data_limit) < percent):
475+
reminder = get_notification_reminder(db, dbuser.id, ReminderType.data_usage, threshold=percent)
476+
if reminder:
477+
delete_notification_reminder(db, reminder)
478+
476479
else:
477480
dbuser.status = UserStatus.limited
478481

@@ -481,9 +484,12 @@ def update_user(db: Session, dbuser: User, modify: UserModify) -> User:
481484
if dbuser.status in (UserStatus.active, UserStatus.expired):
482485
if not dbuser.expire or dbuser.expire > datetime.utcnow().timestamp():
483486
dbuser.status = UserStatus.active
484-
if not dbuser.expire or (calculate_expiration_days(
485-
dbuser.expire) > NOTIFY_DAYS_LEFT):
486-
delete_notification_reminder_by_type(db, dbuser.id, ReminderType.expiration_date)
487+
for days_left in sorted(NOTIFY_DAYS_LEFT):
488+
if not dbuser.expire or (calculate_expiration_days(
489+
dbuser.expire) > days_left):
490+
reminder = get_notification_reminder(db, dbuser.id, ReminderType.expiration_date, threshold=days_left)
491+
if reminder:
492+
delete_notification_reminder(db, reminder)
487493
else:
488494
dbuser.status = UserStatus.expired
489495

@@ -1254,7 +1260,7 @@ def update_node_status(db: Session, dbnode: Node, status: NodeStatus, message: s
12541260

12551261

12561262
def create_notification_reminder(
1257-
db: Session, reminder_type: ReminderType, expires_at: datetime, user_id: int) -> NotificationReminder:
1263+
db: Session, reminder_type: ReminderType, expires_at: datetime, user_id: int, threshold: Optional[int] = None) -> NotificationReminder:
12581264
"""
12591265
Creates a new notification reminder.
12601266
@@ -1263,19 +1269,22 @@ def create_notification_reminder(
12631269
reminder_type (ReminderType): The type of reminder.
12641270
expires_at (datetime): The expiration time of the reminder.
12651271
user_id (int): The ID of the user associated with the reminder.
1272+
threshold (Optional[int]): The threshold value to check for (e.g., days left or usage percent).
12661273
12671274
Returns:
12681275
NotificationReminder: The newly created NotificationReminder object.
12691276
"""
12701277
reminder = NotificationReminder(type=reminder_type, expires_at=expires_at, user_id=user_id)
1278+
if threshold is not None:
1279+
reminder.threshold = threshold
12711280
db.add(reminder)
12721281
db.commit()
12731282
db.refresh(reminder)
12741283
return reminder
12751284

12761285

12771286
def get_notification_reminder(
1278-
db: Session, user_id: int, reminder_type: ReminderType,
1287+
db: Session, user_id: int, reminder_type: ReminderType, threshold: Optional[int] = None
12791288
) -> Union[NotificationReminder, None]:
12801289
"""
12811290
Retrieves a notification reminder for a user.
@@ -1284,38 +1293,57 @@ def get_notification_reminder(
12841293
db (Session): The database session.
12851294
user_id (int): The ID of the user.
12861295
reminder_type (ReminderType): The type of reminder to retrieve.
1296+
threshold (Optional[int]): The threshold value to check for (e.g., days left or usage percent).
12871297
12881298
Returns:
12891299
Union[NotificationReminder, None]: The NotificationReminder object if found and not expired, None otherwise.
12901300
"""
1291-
reminder = db.query(NotificationReminder).filter(
1292-
NotificationReminder.user_id == user_id).filter(
1293-
NotificationReminder.type == reminder_type).first()
1301+
query = db.query(NotificationReminder).filter(
1302+
NotificationReminder.user_id == user_id,
1303+
NotificationReminder.type == reminder_type
1304+
)
1305+
1306+
# If a threshold is provided, filter for reminders with this threshold
1307+
if threshold is not None:
1308+
query = query.filter(NotificationReminder.threshold == threshold)
1309+
1310+
reminder = query.first()
1311+
12941312
if reminder is None:
1295-
return
1313+
return None
1314+
1315+
# Check if the reminder has expired
12961316
if reminder.expires_at and reminder.expires_at < datetime.utcnow():
12971317
db.delete(reminder)
12981318
db.commit()
1299-
return
1319+
return None
1320+
13001321
return reminder
13011322

13021323

1303-
def delete_notification_reminder_by_type(db: Session, user_id: int, reminder_type: ReminderType) -> None:
1324+
def delete_notification_reminder_by_type(
1325+
db: Session, user_id: int, reminder_type: ReminderType, threshold: Optional[int] = None
1326+
) -> None:
13041327
"""
1305-
Deletes a notification reminder for a user based on the reminder type.
1328+
Deletes a notification reminder for a user based on the reminder type and optional threshold.
13061329
13071330
Args:
13081331
db (Session): The database session.
13091332
user_id (int): The ID of the user.
13101333
reminder_type (ReminderType): The type of reminder to delete.
1334+
threshold (Optional[int]): The threshold to delete (e.g., days left or usage percent). If not provided, deletes all reminders of that type.
13111335
"""
13121336
stmt = delete(NotificationReminder).where(
13131337
NotificationReminder.user_id == user_id,
1314-
NotificationReminder.type == reminder_type,
1338+
NotificationReminder.type == reminder_type
13151339
)
1340+
1341+
# If a threshold is provided, include it in the filter
1342+
if threshold is not None:
1343+
stmt = stmt.where(NotificationReminder.threshold == threshold)
1344+
13161345
db.execute(stmt)
13171346
db.commit()
1318-
return
13191347

13201348

13211349
def delete_notification_reminder(db: Session, dbreminder: NotificationReminder) -> None:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""add threshold to NotificationReminder
2+
3+
Revision ID: 21226bc711ac
4+
Revises: 2ea33513efc0
5+
Create Date: 2024-10-18 12:26:30.504491
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '21226bc711ac'
14+
down_revision = '2ea33513efc0'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column('notification_reminders', sa.Column('threshold', sa.Integer(), nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade() -> None:
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column('notification_reminders', 'threshold')
28+
# ### end Alembic commands ###

app/db/models.py

+1
Original file line numberDiff line numberDiff line change
@@ -299,5 +299,6 @@ class NotificationReminder(Base):
299299
user_id = Column(Integer, ForeignKey("users.id"))
300300
user = relationship("User", back_populates="notification_reminders")
301301
type = Column(Enum(ReminderType), nullable=False)
302+
threshold = Column(Integer, nullable=True)
302303
expires_at = Column(DateTime, nullable=True)
303304
created_at = Column(DateTime, default=datetime.utcnow)

app/jobs/review_users.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
def add_notification_reminders(db: Session, user: "User", now: datetime = datetime.utcnow()) -> None:
2121
if user.data_limit:
22-
usage_percent = calculate_usage_percent(
23-
user.used_traffic, user.data_limit)
22+
usage_percent = calculate_usage_percent(user.used_traffic, user.data_limit)
2423
if (usage_percent >= NOTIFY_REACHED_USAGE_PERCENT) and (not get_notification_reminder(db, user.id, ReminderType.data_usage)):
2524
report.data_usage_percent_reached(
2625
db, usage_percent, UserResponse.from_orm(user),
@@ -34,6 +33,32 @@ def add_notification_reminders(db: Session, user: "User", now: datetime = dateti
3433
user.id, user.expire)
3534

3635

36+
def add_notification_reminders(db: Session, user: "User", now: datetime = datetime.utcnow()) -> None:
37+
if user.data_limit:
38+
usage_percent = calculate_usage_percent(user.used_traffic, user.data_limit)
39+
40+
for percent in sorted(NOTIFY_REACHED_USAGE_PERCENT, reverse=True):
41+
if usage_percent >= percent:
42+
if not get_notification_reminder(db, user.id, ReminderType.data_usage, threshold=percent):
43+
report.data_usage_percent_reached(
44+
db, usage_percent, UserResponse.from_orm(user),
45+
user.id, user.expire, threshold=percent
46+
)
47+
break
48+
49+
if user.expire:
50+
expire_days = calculate_expiration_days(user.expire)
51+
52+
for days_left in sorted(NOTIFY_DAYS_LEFT):
53+
if expire_days <= days_left:
54+
if not get_notification_reminder(db, user.id, ReminderType.expiration_date, threshold=days_left):
55+
report.expire_days_reached(
56+
db, expire_days, UserResponse.from_orm(user),
57+
user.id, user.expire, threshold=days_left
58+
)
59+
break
60+
61+
3762
def review():
3863
now = datetime.utcnow()
3964
now_ts = now.timestamp()

app/utils/report.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
NOTIFY_USER_DATA_USED_RESET,
2323
NOTIFY_USER_SUB_REVOKED,
2424
NOTIFY_IF_DATA_USAGE_PERCENT_REACHED,
25-
NOTIFY_IF_DAYS_LEF_REACHED,
25+
NOTIFY_IF_DAYS_LEFT_REACHED,
2626
NOTIFY_LOGIN
2727
)
2828

@@ -164,19 +164,19 @@ def user_subscription_revoked(user: UserResponse, by: Admin, user_admin: Admin =
164164

165165

166166
def data_usage_percent_reached(
167-
db: Session, percent: float, user: UserResponse, user_id: int, expire: Optional[int] = None) -> None:
167+
db: Session, percent: float, user: UserResponse, user_id: int, expire: Optional[int] = None, threshold: Optional[int] = None) -> None:
168168
if NOTIFY_IF_DATA_USAGE_PERCENT_REACHED:
169169
notify(ReachedUsagePercent(username=user.username, user=user, used_percent=percent))
170170
create_notification_reminder(db, ReminderType.data_usage,
171-
expires_at=dt.utcfromtimestamp(expire) if expire else None, user_id=user_id)
171+
expires_at=dt.utcfromtimestamp(expire) if expire else None, user_id=user_id, threshold=threshold)
172172

173173

174-
def expire_days_reached(db: Session, days: int, user: UserResponse, user_id: int, expire: int) -> None:
174+
def expire_days_reached(db: Session, days: int, user: UserResponse, user_id: int, expire: int, threshold=None) -> None:
175175
notify(ReachedDaysLeft(username=user.username, user=user, days_left=days))
176-
if NOTIFY_IF_DAYS_LEF_REACHED:
176+
if NOTIFY_IF_DAYS_LEFT_REACHED:
177177
create_notification_reminder(
178178
db, ReminderType.expiration_date, expires_at=dt.utcfromtimestamp(expire),
179-
user_id=user_id)
179+
user_id=user_id, threshold=threshold)
180180

181181

182182
def login(username: str, password: str, client_ip: str, success: bool) -> None:

config.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
GRPC_USER_AGENT_TEMPLATE = config("GRPC_USER_AGENT_TEMPLATE", default="user_agent/grpc.json")
6666

6767
EXTERNAL_CONFIG = config("EXTERNAL_CONFIG", default="", cast=str)
68-
LOGIN_NOTIFY_WHITE_LIST = [ip.strip() for ip in config("LOGIN_NOTIFY_WHITE_LIST", default="", cast=str).split(",") if ip.strip()]
68+
LOGIN_NOTIFY_WHITE_LIST = [ip.strip() for ip in config("LOGIN_NOTIFY_WHITE_LIST",
69+
default="", cast=str).split(",") if ip.strip()]
6970

7071
USE_CUSTOM_JSON_DEFAULT = config("USE_CUSTOM_JSON_DEFAULT", default=False, cast=bool)
7172
USE_CUSTOM_JSON_FOR_V2RAYN = config("USE_CUSTOM_JSON_FOR_V2RAYN", default=False, cast=bool)
@@ -79,7 +80,7 @@
7980
NOTIFY_USER_DATA_USED_RESET = config("NOTIFY_USER_DATA_USED_RESET", default=True, cast=bool)
8081
NOTIFY_USER_SUB_REVOKED = config("NOTIFY_USER_SUB_REVOKED", default=True, cast=bool)
8182
NOTIFY_IF_DATA_USAGE_PERCENT_REACHED = config("NOTIFY_IF_DATA_USAGE_PERCENT_REACHED", default=True, cast=bool)
82-
NOTIFY_IF_DAYS_LEF_REACHED = config("NOTIFY_IF_DAYS_LEF_REACHED", default=True, cast=bool)
83+
NOTIFY_IF_DAYS_LEFT_REACHED = config("NOTIFY_IF_DAYS_LEFT_REACHED", default=True, cast=bool)
8384
NOTIFY_LOGIN = config("NOTIFY_LOGIN", default=True, cast=bool)
8485

8586
ACTIVE_STATUS_TEXT = config("ACTIVE_STATUS_TEXT", default="Active")
@@ -113,10 +114,18 @@
113114
NUMBER_OF_RECURRENT_NOTIFICATIONS = config("NUMBER_OF_RECURRENT_NOTIFICATIONS", default=3, cast=int)
114115

115116
# sends a notification when the user uses this much of thier data
116-
NOTIFY_REACHED_USAGE_PERCENT = config("NOTIFY_REACHED_USAGE_PERCENT", default=80, cast=int)
117+
NOTIFY_REACHED_USAGE_PERCENT = config(
118+
"NOTIFY_REACHED_USAGE_PERCENT",
119+
default=[80],
120+
cast=lambda v: [int(p.strip()) for p in v.split(',')] if v else []
121+
)
117122

118123
# sends a notification when there is n days left of their service
119-
NOTIFY_DAYS_LEFT = config("NOTIFY_DAYS_LEFT", default=3, cast=int)
124+
NOTIFY_DAYS_LEFT = config(
125+
"NOTIFY_DAYS_LEFT",
126+
default=[3],
127+
cast=lambda v: [int(d.strip()) for d in v.split(',')] if v else []
128+
)
120129

121130
DISABLE_RECORDING_NODE_USAGE = config("DISABLE_RECORDING_NODE_USAGE", cast=bool, default=False)
122131

0 commit comments

Comments
 (0)