diff --git a/API/app/contollers/rss_router.py b/API/app/contollers/rss_router.py
new file mode 100644
index 0000000..95ef45e
--- /dev/null
+++ b/API/app/contollers/rss_router.py
@@ -0,0 +1,17 @@
+from fastapi import APIRouter
+
+from app.domain.entities.activity_item import ActivityItem
+from app.infrastructure.rss_service import RSSService
+
+router = APIRouter()
+
+
+@router.get(
+ "/{username}",
+ response_model=list[ActivityItem],
+ summary="Get the user's RSS activity",
+ description="Returns an array with the date and number of activities in the last 30 days"
+)
+def get_user_rss(username: str):
+ rss_service = RSSService()
+ return rss_service.get_rss_by_username(username)
diff --git a/API/app/domain/entities/activity_item.py b/API/app/domain/entities/activity_item.py
new file mode 100644
index 0000000..72cf910
--- /dev/null
+++ b/API/app/domain/entities/activity_item.py
@@ -0,0 +1,6 @@
+from pydantic import BaseModel
+
+
+class ActivityItem(BaseModel):
+ date: str
+ count: int
diff --git a/API/app/infrastructure/rss_service.py b/API/app/infrastructure/rss_service.py
new file mode 100644
index 0000000..724d745
--- /dev/null
+++ b/API/app/infrastructure/rss_service.py
@@ -0,0 +1,39 @@
+from fastapi import APIRouter, HTTPException
+import requests
+import xml.etree.ElementTree as ET
+from datetime import datetime, timedelta
+
+
+class RSSService:
+ @staticmethod
+ def get_rss_by_username(username: str):
+ url = f"https://git.numerum.team/{username}.rss"
+ response = requests.get(url)
+
+ if response.status_code != 200:
+ raise HTTPException(status_code=400, detail="Не удалось загрузить RSS")
+
+ root = ET.fromstring(response.text)
+ items = root.findall(".//item")
+
+ activity_map = {}
+
+ for item in items:
+ pub_date_elem = item.find("pubDate")
+ if pub_date_elem is not None:
+ pub_date_str = pub_date_elem.text.strip()
+ pub_date = datetime.strptime(pub_date_str, "%a, %d %b %Y %H:%M:%S %z")
+ date_str = pub_date.strftime("%Y-%m-%d")
+ activity_map[date_str] = activity_map.get(date_str, 0) + 1
+
+ today = datetime.now(pub_date.tzinfo)
+ activity_days = 365
+ result = []
+
+ for i in range(activity_days - 1, -1, -1):
+ day = today - timedelta(days=i)
+ date_str = day.strftime("%Y-%m-%d")
+ count = activity_map.get(date_str, 0)
+ result.append({"date": date_str, "count": count})
+
+ return result
diff --git a/API/app/main.py b/API/app/main.py
index 38a2a14..4235d4d 100644
--- a/API/app/main.py
+++ b/API/app/main.py
@@ -8,6 +8,7 @@ from app.contollers.project_files_router import router as project_files_router
from app.contollers.project_members_router import router as project_members_router
from app.contollers.projects_router import router as projects_router
from app.contollers.register_router import router as register_router
+from app.contollers.rss_router import router as rss_router
from app.contollers.teams_router import router as team_router
from app.contollers.users_router import router as users_router
from app.settings import settings
@@ -31,6 +32,7 @@ def start_app():
api_app.include_router(project_members_router, prefix=f'{settings.PREFIX}/project_members', tags=['project_members'])
api_app.include_router(projects_router, prefix=f'{settings.PREFIX}/projects', tags=['projects'])
api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register'])
+ api_app.include_router(rss_router, prefix=f'{settings.PREFIX}/rss', tags=['rss_router'])
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
diff --git a/WEB/package-lock.json b/WEB/package-lock.json
index f08e674..9dc3887 100644
--- a/WEB/package-lock.json
+++ b/WEB/package-lock.json
@@ -12,7 +12,8 @@
"axios": "^1.9.0",
"quasar": "^2.18.1",
"vue": "^3.5.13",
- "vue-router": "^4.5.1"
+ "vue-router": "^4.5.1",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
@@ -1849,6 +1850,12 @@
"node": ">=14.0.0"
}
},
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "license": "ISC"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2038,6 +2045,28 @@
"peerDependencies": {
"vue": "^3.2.0"
}
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
}
}
}
diff --git a/WEB/package.json b/WEB/package.json
index b7530aa..40f81b9 100644
--- a/WEB/package.json
+++ b/WEB/package.json
@@ -13,7 +13,8 @@
"axios": "^1.9.0",
"quasar": "^2.18.1",
"vue": "^3.5.13",
- "vue-router": "^4.5.1"
+ "vue-router": "^4.5.1",
+ "xml2js": "^0.6.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.2",
diff --git a/WEB/src/pages/HomePage.vue b/WEB/src/pages/HomePage.vue
index 0251249..c5cdbc3 100644
--- a/WEB/src/pages/HomePage.vue
+++ b/WEB/src/pages/HomePage.vue
@@ -60,43 +60,60 @@