Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccec4cdf27 | |||
| 4d4fce186c | |||
| aeed5ea45c | |||
| 10310b9416 | |||
| f9277ad570 | |||
| 3a25f4fdd4 | |||
| a5c512c7d4 | |||
| 18b13fdb57 | |||
| d5ed160746 | |||
| f9cfa1648e | |||
| 8ba9b7816e |
@ -1,17 +1,9 @@
|
|||||||
# Используем официальный образ Python 3
|
|
||||||
FROM python:3.13-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
# Устанавливаем рабочую директорию внутри контейнера
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем requirements.txt в контейнер для установки зависимостей
|
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
|
|
||||||
# Устанавливаем зависимости
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Копируем остальные файлы проекта в контейнер
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Определяем команду запуска приложения
|
|
||||||
CMD ["python", "src/__main__.py"]
|
CMD ["python", "src/__main__.py"]
|
||||||
|
|||||||
7
notes.md
Normal file
7
notes.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
### Сомнения по API:
|
||||||
|
1) `feeds` API delete feed
|
||||||
|
2) Нужно ли API картинок? Вроде вообще всё будет делать scraper. (хотя наверное получение инфы о картинке по id - нужная штука)
|
||||||
|
3) Возможно стоит добавить возможность динамической смены длины invite кода
|
||||||
|
|
||||||
|
### TODO:
|
||||||
|
1) Реализовать лайк картинки на платформе, если её лайкнуло больше n/2 (предварительно) участников группы. Так же что-то придумать с АФК челами, которые будут не давать ленте меняться, своим присутствием и не-голосованием. Возможно сделать систему очков, типа +1 за лайк, -1 за дизлайк и сохранять только перед генерацией новой ленты? (звучит как упор в rate limit и вообще..)
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
100
src/api/accounts.py
Normal file
100
src/api/accounts.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.accounts as db
|
||||||
|
import settings.settings as settings
|
||||||
|
from api.models import Account, User
|
||||||
|
from api.string_tools import encode_str
|
||||||
|
from api.utils import get_current_user
|
||||||
|
from db.groups import check_group_author
|
||||||
|
from db.internal import get_db_connection
|
||||||
|
|
||||||
|
accounts_router = APIRouter(prefix="/api/accounts", tags=["accounts"])
|
||||||
|
|
||||||
|
|
||||||
|
@accounts_router.post("/group")
|
||||||
|
async def read_accounts_by_group(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if not check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
return db.get_accounts_by_group(conn, groupname)
|
||||||
|
|
||||||
|
|
||||||
|
@accounts_router.post("/group/platform")
|
||||||
|
async def read_accounts_by_group_platform(
|
||||||
|
groupname: str,
|
||||||
|
platform: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
account_data = db.get_accounts_by_group_platform(conn, groupname, platform.lower())
|
||||||
|
if account_data is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such account",
|
||||||
|
)
|
||||||
|
return Account().fill(account_data)
|
||||||
|
|
||||||
|
|
||||||
|
@accounts_router.post("/add")
|
||||||
|
async def add_account(
|
||||||
|
groupname: str,
|
||||||
|
platform: str,
|
||||||
|
login: str,
|
||||||
|
password: str,
|
||||||
|
metadata: dict,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if not check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
if db.check_account_existence(conn, groupname, platform.lower()):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Account already exists",
|
||||||
|
)
|
||||||
|
hashed_password = encode_str(password)
|
||||||
|
return db.create_account(conn, groupname, current_user.username, platform.lower(), login, hashed_password, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
@accounts_router.post("/update")
|
||||||
|
async def update_account(
|
||||||
|
groupname: str,
|
||||||
|
platform: str,
|
||||||
|
author: str,
|
||||||
|
login: str,
|
||||||
|
password: str,
|
||||||
|
metadata: dict,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
account_data = db.get_accounts_by_group_platform(conn, groupname, platform.lower())
|
||||||
|
if account_data is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such account",
|
||||||
|
)
|
||||||
|
account = Account().fill(account_data)
|
||||||
|
|
||||||
|
if current_user.username != account.author and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
return db.update_account(conn, groupname, author, platform.lower(), login, password, metadata)
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.feeds as db
|
||||||
|
import settings.settings as settings
|
||||||
|
from api.models import Feed, User
|
||||||
|
from api.utils import get_current_user
|
||||||
|
from db.accounts import get_accounts_by_group
|
||||||
|
from db.feeds import get_groupname_by_feed_id
|
||||||
|
from db.groups import check_group_author
|
||||||
|
from db.internal import get_db_connection
|
||||||
|
from db.memberships import check_membership_exists
|
||||||
|
from scraper.utils import generate_feed
|
||||||
|
|
||||||
|
feeds_router = APIRouter(prefix="/api/feeds", tags=["feeds"])
|
||||||
|
|
||||||
|
|
||||||
|
@feeds_router.post("/feed")
|
||||||
|
async def read_feed(
|
||||||
|
feed_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
feed_data = db.get_feed(conn, feed_id)
|
||||||
|
if feed_data is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed",
|
||||||
|
)
|
||||||
|
feed = Feed().fill(feed_data)
|
||||||
|
|
||||||
|
groupname = get_groupname_by_feed_id(conn, feed_id)
|
||||||
|
if groupname is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
return feed
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: review exception
|
||||||
|
@feeds_router.post("/new")
|
||||||
|
async def new_feed(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if not check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
accounts = get_accounts_by_group(conn, groupname)
|
||||||
|
feed = generate_feed(conn, accounts)
|
||||||
|
if feed:
|
||||||
|
return db.create_feed(conn, groupname, feed)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
||||||
|
detail="Failed to generate feed",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# maybe to delete
|
||||||
|
@feeds_router.post("/delete")
|
||||||
|
async def delete_feed(
|
||||||
|
feed_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.delete_feed(conn, feed_id)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|||||||
@ -5,13 +5,23 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|||||||
import settings.settings as settings
|
import settings.settings as settings
|
||||||
from api.models import User
|
from api.models import User
|
||||||
from api.utils import get_current_user
|
from api.utils import get_current_user
|
||||||
|
from settings.consts import API_EDITABLE_SETTINGS_LIST
|
||||||
from settings.settings import load_settings, reset_settings, save_settings
|
from settings.settings import load_settings, reset_settings, save_settings
|
||||||
|
|
||||||
general_router = APIRouter(prefix="/api", tags=["general"])
|
general_router = APIRouter(prefix="/api", tags=["general"])
|
||||||
|
|
||||||
|
|
||||||
@general_router.get("/ping")
|
@general_router.get("/ping/user")
|
||||||
async def ping():
|
async def ping_user(current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
|
return {"ok"}
|
||||||
|
|
||||||
|
@general_router.get("/ping/admin")
|
||||||
|
async def ping_admin(current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
return {"ok"}
|
return {"ok"}
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +35,7 @@ async def get_settings(current_user: Annotated[User, Depends(get_current_user)])
|
|||||||
return settings.settings
|
return settings.settings
|
||||||
|
|
||||||
|
|
||||||
@general_router.post("/settings/update")
|
@general_router.post("/settings/update", description=API_EDITABLE_SETTINGS_LIST)
|
||||||
async def update_settings(data: dict, current_user: Annotated[User, Depends(get_current_user)]):
|
async def update_settings(data: dict, current_user: Annotated[User, Depends(get_current_user)]):
|
||||||
if current_user.role not in settings.settings.admin_roles:
|
if current_user.role not in settings.settings.admin_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import db.groups as db
|
|||||||
import settings.settings as settings
|
import settings.settings as settings
|
||||||
import settings.startup_settings as startup_settings
|
import settings.startup_settings as startup_settings
|
||||||
from api.models import Group, User
|
from api.models import Group, User
|
||||||
from api.utils import get_current_user, get_group_by_name
|
from api.utils import get_current_user
|
||||||
from db.internal import get_db_connection
|
from db.internal import get_db_connection
|
||||||
from db.memberships import check_membership_exists
|
from db.memberships import check_membership_exists
|
||||||
from settings.consts import JOIN_CODE_SYMBOLS
|
from settings.consts import JOIN_CODE_SYMBOLS
|
||||||
@ -27,15 +27,13 @@ async def read_any_group(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
group = Group()
|
|
||||||
group_data = db.get_group(conn, groupname)
|
group_data = db.get_group(conn, groupname)
|
||||||
if group_data is None:
|
if group_data is None:
|
||||||
return HTTPException(
|
return HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No such group",
|
detail="No such group",
|
||||||
)
|
)
|
||||||
group.fill(group_data)
|
return Group().fill(group_data)
|
||||||
return group
|
|
||||||
|
|
||||||
@groups_router.post("/invite_code")
|
@groups_router.post("/invite_code")
|
||||||
async def read_group_invite_code(
|
async def read_group_invite_code(
|
||||||
@ -56,6 +54,19 @@ async def read_group_invite_code(
|
|||||||
)
|
)
|
||||||
return invite_code
|
return invite_code
|
||||||
|
|
||||||
|
@groups_router.post("/last_feed")
|
||||||
|
async def read_group_last_feed_id(
|
||||||
|
groupname: str,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
return db.get_group_last_feed_id(conn, groupname)
|
||||||
|
|
||||||
|
|
||||||
@groups_router.post("/add")
|
@groups_router.post("/add")
|
||||||
async def add_group(
|
async def add_group(
|
||||||
@ -91,10 +102,9 @@ async def delete_group(
|
|||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
group = get_group_by_name(conn, groupname)
|
|
||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.delete_group(conn, groupname)
|
return db.delete_group(conn, groupname)
|
||||||
if current_user.username == group.author:
|
if db.check_group_author(conn, groupname, current_user.username):
|
||||||
return db.delete_group(conn, groupname)
|
return db.delete_group(conn, groupname)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -115,11 +125,10 @@ async def update_groupname(
|
|||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
detail="Groupname is already taken",
|
detail="Groupname is already taken",
|
||||||
)
|
)
|
||||||
group = get_group_by_name(conn, groupname)
|
|
||||||
|
|
||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.update_group_groupname(conn, groupname, new_groupname)
|
return db.update_group_groupname(conn, groupname, new_groupname)
|
||||||
if current_user.username == group.author:
|
if db.check_group_author(conn, groupname, current_user.username):
|
||||||
return db.update_group_groupname(conn, groupname, new_groupname)
|
return db.update_group_groupname(conn, groupname, new_groupname)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -134,10 +143,9 @@ async def update_author(
|
|||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
group = get_group_by_name(conn, groupname)
|
|
||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.update_group_author(conn, groupname, new_author)
|
return db.update_group_author(conn, groupname, new_author)
|
||||||
if current_user.username == group.author:
|
if db.check_group_author(conn, groupname, current_user.username):
|
||||||
return db.update_group_author(conn, groupname, new_author)
|
return db.update_group_author(conn, groupname, new_author)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -151,8 +159,6 @@ async def update_invite_code(
|
|||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
group = get_group_by_name(conn, groupname)
|
|
||||||
|
|
||||||
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
||||||
while db.check_invite_code(conn, invite_code):
|
while db.check_invite_code(conn, invite_code):
|
||||||
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
|
||||||
@ -162,7 +168,7 @@ async def update_invite_code(
|
|||||||
"result": db.update_group_invite_code(conn, groupname, invite_code),
|
"result": db.update_group_invite_code(conn, groupname, invite_code),
|
||||||
"invite code": invite_code
|
"invite code": invite_code
|
||||||
}
|
}
|
||||||
if current_user.username == group.author:
|
if db.check_group_author(conn, groupname, current_user.username):
|
||||||
return {
|
return {
|
||||||
"result": db.update_group_invite_code(conn, groupname, invite_code),
|
"result": db.update_group_invite_code(conn, groupname, invite_code),
|
||||||
"invite code": invite_code
|
"invite code": invite_code
|
||||||
@ -181,10 +187,9 @@ async def update_allow_skips(
|
|||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
group = get_group_by_name(conn, groupname)
|
|
||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.update_group_allow_skips(conn, groupname, allow_skips)
|
return db.update_group_allow_skips(conn, groupname, allow_skips)
|
||||||
if current_user.username == group.author:
|
if db.check_group_author(conn, groupname, current_user.username):
|
||||||
return db.update_group_allow_skips(conn, groupname, allow_skips)
|
return db.update_group_allow_skips(conn, groupname, allow_skips)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -200,10 +205,9 @@ async def update_feed_interval(
|
|||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
group = get_group_by_name(conn, groupname)
|
|
||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.update_group_feed_interval(conn, groupname, feed_interval)
|
return db.update_group_feed_interval(conn, groupname, feed_interval)
|
||||||
if current_user.username == group.author:
|
if db.check_group_author(conn, groupname, current_user.username):
|
||||||
return db.update_group_feed_interval(conn, groupname, feed_interval)
|
return db.update_group_feed_interval(conn, groupname, feed_interval)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@ -6,8 +6,12 @@ from psycopg2._psycopg import connection
|
|||||||
import db.memberships as db
|
import db.memberships as db
|
||||||
import settings.settings as settings
|
import settings.settings as settings
|
||||||
from api.models import User
|
from api.models import User
|
||||||
from api.utils import get_current_user, get_group_by_name
|
from api.utils import get_current_user
|
||||||
from db.groups import check_group_existence, get_groupname_by_invite_code
|
from db.groups import (
|
||||||
|
check_group_author,
|
||||||
|
check_group_existence,
|
||||||
|
get_groupname_by_invite_code,
|
||||||
|
)
|
||||||
from db.internal import get_db_connection
|
from db.internal import get_db_connection
|
||||||
from db.users import check_user_existence
|
from db.users import check_user_existence
|
||||||
|
|
||||||
@ -107,8 +111,7 @@ async def delete_membership(
|
|||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.delete_membership(conn, username, groupname)
|
return db.delete_membership(conn, username, groupname)
|
||||||
|
|
||||||
group = get_group_by_name(conn, groupname)
|
if check_group_author(conn, groupname, current_user.username):
|
||||||
if current_user.username == group.author:
|
|
||||||
return db.delete_membership(conn, username, groupname)
|
return db.delete_membership(conn, username, groupname)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@ -2,6 +2,8 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from api.string_tools import decode_str
|
||||||
|
|
||||||
|
|
||||||
class Token(BaseModel):
|
class Token(BaseModel):
|
||||||
access_token: str
|
access_token: str
|
||||||
@ -18,14 +20,13 @@ class User(BaseModel):
|
|||||||
self.password = params["password"]
|
self.password = params["password"]
|
||||||
self.role = params["role"]
|
self.role = params["role"]
|
||||||
self.disabled = params["disabled"]
|
self.disabled = params["disabled"]
|
||||||
self.groups_ids = params["groups_ids"]
|
|
||||||
self.last_seen_at = params["last_seen_at"]
|
self.last_seen_at = params["last_seen_at"]
|
||||||
self.created_at = params["created_at"]
|
self.created_at = params["created_at"]
|
||||||
|
return self
|
||||||
username: str = ""
|
username: str = ""
|
||||||
password: str = ""
|
password: str = ""
|
||||||
role: str = "user"
|
role: str = "user"
|
||||||
disabled: bool = False
|
disabled: bool = False
|
||||||
groups_ids: list[str] | None = None
|
|
||||||
last_seen_at: datetime | None = None
|
last_seen_at: datetime | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ class Group(BaseModel):
|
|||||||
self.feed_interval_minutes = params["feed_interval_minutes"]
|
self.feed_interval_minutes = params["feed_interval_minutes"]
|
||||||
self.last_feed_id = params["last_feed_id"]
|
self.last_feed_id = params["last_feed_id"]
|
||||||
self.created_at = params["created_at"]
|
self.created_at = params["created_at"]
|
||||||
|
return self
|
||||||
groupname: str = ""
|
groupname: str = ""
|
||||||
author: str = ""
|
author: str = ""
|
||||||
invite_code: str = ""
|
invite_code: str = ""
|
||||||
@ -53,6 +55,7 @@ class Membership(BaseModel):
|
|||||||
self.groupname = params["groupname"]
|
self.groupname = params["groupname"]
|
||||||
self.username = params["username"]
|
self.username = params["username"]
|
||||||
self.joined_at = params["joined_at"]
|
self.joined_at = params["joined_at"]
|
||||||
|
return self
|
||||||
groupname: str = ""
|
groupname: str = ""
|
||||||
username: str = ""
|
username: str = ""
|
||||||
joined_at: datetime | None = None
|
joined_at: datetime | None = None
|
||||||
@ -66,9 +69,59 @@ class Picture(BaseModel):
|
|||||||
self.url = params["url"]
|
self.url = params["url"]
|
||||||
self.metadata = params["metadata"]
|
self.metadata = params["metadata"]
|
||||||
self.created_at = params["created_at"]
|
self.created_at = params["created_at"]
|
||||||
|
return self
|
||||||
id: int = -1
|
id: int = -1
|
||||||
source: str = ""
|
source: str = ""
|
||||||
external_id: str = ""
|
external_id: str = ""
|
||||||
url: str = ""
|
url: str = ""
|
||||||
metadata: dict | None = None
|
metadata: dict = {}
|
||||||
|
created_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Swipe(BaseModel):
|
||||||
|
def fill(self, params):
|
||||||
|
self.username = params["username"]
|
||||||
|
self.feed_id = params["feed_id"]
|
||||||
|
self.picture_id = params["picture_id"]
|
||||||
|
self.value = params["value"]
|
||||||
|
self.created_at = params["created_at"]
|
||||||
|
return self
|
||||||
|
username: str = ""
|
||||||
|
feed_id: int = -1
|
||||||
|
picture_id: int = -1
|
||||||
|
value: int = 0
|
||||||
|
created_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Feed(BaseModel):
|
||||||
|
def fill(self, params):
|
||||||
|
self.id = params["id"]
|
||||||
|
self.groupname = params["groupname"]
|
||||||
|
self.image_ids = params["image_ids"]
|
||||||
|
self.created_at = params["created_at"]
|
||||||
|
return self
|
||||||
|
id: int = -1
|
||||||
|
groupname: str = ""
|
||||||
|
image_ids: list[int] = []
|
||||||
|
created_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Account(BaseModel):
|
||||||
|
def fill(self, params):
|
||||||
|
self.id = params["id"]
|
||||||
|
self.groupname = params["groupname"]
|
||||||
|
self.author = params["author"]
|
||||||
|
self.platform = params["platform"]
|
||||||
|
self.login = params["login"]
|
||||||
|
self.password = decode_str(params["password"])
|
||||||
|
self.metadata = params["metadata"]
|
||||||
|
self.created_at = params["created_at"]
|
||||||
|
return self
|
||||||
|
id: int = -1
|
||||||
|
groupname: str = ""
|
||||||
|
author: str = ""
|
||||||
|
platform: str = ""
|
||||||
|
login: str = ""
|
||||||
|
password: str = ""
|
||||||
|
metadata: dict = {}
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
|||||||
@ -12,39 +12,19 @@ from db.internal import get_db_connection
|
|||||||
pictures_router = APIRouter(prefix="/api/pictures", tags=["pictures"])
|
pictures_router = APIRouter(prefix="/api/pictures", tags=["pictures"])
|
||||||
|
|
||||||
|
|
||||||
# maybe to delete
|
@pictures_router.post("/picture")
|
||||||
@pictures_router.post("/picture/url")
|
async def read_picture(
|
||||||
async def read_picture_by_url(
|
id: int,
|
||||||
groupname: str,
|
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
picture = Picture()
|
picture_data = db.get_picture(conn, id)
|
||||||
picture_data = db.get_picture_by_url(conn, groupname)
|
|
||||||
if picture_data is None:
|
if picture_data is None:
|
||||||
return HTTPException(
|
return HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No such picture",
|
detail="No such picture",
|
||||||
)
|
)
|
||||||
picture.fill(picture_data)
|
return Picture().fill(picture_data)
|
||||||
return picture
|
|
||||||
|
|
||||||
|
|
||||||
@pictures_router.post("/picture/id")
|
|
||||||
async def read_picture_by_id(
|
|
||||||
groupname: str,
|
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
|
||||||
):
|
|
||||||
picture = Picture()
|
|
||||||
picture_data = db.get_picture_by_id(conn, groupname)
|
|
||||||
if picture_data is None:
|
|
||||||
return HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="No such picture",
|
|
||||||
)
|
|
||||||
picture.fill(picture_data)
|
|
||||||
return picture
|
|
||||||
|
|
||||||
|
|
||||||
@pictures_router.post("/add")
|
@pictures_router.post("/add")
|
||||||
@ -62,40 +42,19 @@ async def add_picture(
|
|||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
|
|
||||||
# if db.check_picture_existence(conn, groupname):
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_409_CONFLICT,
|
|
||||||
# detail="Picture already exists",
|
|
||||||
# )
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": db.create_picture(conn, source, external_id, url, metadata)
|
"id": db.create_picture(conn, source, external_id, url, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# maybe to delete
|
@pictures_router.post("/delete")
|
||||||
@pictures_router.post("/delete/url")
|
async def delete_picture(
|
||||||
async def delete_picture_by_url(
|
|
||||||
picture_url: str,
|
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
|
||||||
):
|
|
||||||
if current_user.role in settings.settings.admin_roles:
|
|
||||||
return db.delete_picture_by_url(conn, picture_url)
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="Not allowed",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pictures_router.post("/delete/id")
|
|
||||||
async def delete_picture_by_id(
|
|
||||||
picture_id: int,
|
picture_id: int,
|
||||||
conn: Annotated[connection, Depends(get_db_connection)],
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
current_user: Annotated[User, Depends(get_current_user)]
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
if current_user.role in settings.settings.admin_roles:
|
if current_user.role in settings.settings.admin_roles:
|
||||||
return db.delete_picture_by_id(conn, picture_id)
|
return db.delete_picture(conn, picture_id)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
|||||||
22
src/api/string_tools.py
Normal file
22
src/api/string_tools.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import settings.startup_settings as startup_settings
|
||||||
|
|
||||||
|
|
||||||
|
def encode_str(string) -> str:
|
||||||
|
key = startup_settings.secret_key
|
||||||
|
encoded_chars = []
|
||||||
|
for i in range(len(string)):
|
||||||
|
key_c = key[i % len(key)]
|
||||||
|
encoded_c = chr(ord(string[i]) + ord(key_c) % 256)
|
||||||
|
encoded_chars.append(encoded_c)
|
||||||
|
encoded_string = ''.join(encoded_chars)
|
||||||
|
return encoded_string
|
||||||
|
|
||||||
|
def decode_str(encoded_string) -> str:
|
||||||
|
key = startup_settings.secret_key
|
||||||
|
encoded_chars = []
|
||||||
|
for i in range(len(encoded_string)):
|
||||||
|
key_c = key[i % len(key)]
|
||||||
|
encoded_c = chr((ord(encoded_string[i]) - ord(key_c) + 256) % 256)
|
||||||
|
encoded_chars.append(encoded_c)
|
||||||
|
decoded_string = ''.join(encoded_chars)
|
||||||
|
return decoded_string
|
||||||
144
src/api/swipes.py
Normal file
144
src/api/swipes.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.swipes as db
|
||||||
|
import settings.settings as settings
|
||||||
|
from api.models import Group, Swipe, User
|
||||||
|
from api.utils import get_current_user
|
||||||
|
from db.feeds import get_groupname_by_feed_id
|
||||||
|
from db.groups import get_group
|
||||||
|
from db.internal import get_db_connection
|
||||||
|
from db.memberships import check_membership_exists
|
||||||
|
|
||||||
|
swipes_router = APIRouter(prefix="/api/swipes", tags=["swipes"])
|
||||||
|
|
||||||
|
|
||||||
|
# Maybe endpoints should be remade
|
||||||
|
# to return id and then work with swipe id
|
||||||
|
|
||||||
|
@swipes_router.post("/swipe")
|
||||||
|
async def read_swipe(
|
||||||
|
username: str,
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
groupname = get_groupname_by_feed_id(conn, feed_id)
|
||||||
|
if groupname is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed or feed is not linked to group",
|
||||||
|
)
|
||||||
|
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
|
||||||
|
swipe_data = db.get_swipe(conn, username, feed_id, picture_id)
|
||||||
|
if swipe_data is None:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such swipe",
|
||||||
|
)
|
||||||
|
return Swipe().fill(swipe_data)
|
||||||
|
|
||||||
|
|
||||||
|
@swipes_router.post("/swipe/picture_id")
|
||||||
|
async def read_swipes_by_picture_id(
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
groupname = get_groupname_by_feed_id(conn, feed_id)
|
||||||
|
if groupname is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed or feed is not linked to group",
|
||||||
|
)
|
||||||
|
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
return db.get_swipes_by_picture_id(conn, picture_id, feed_id)
|
||||||
|
|
||||||
|
|
||||||
|
@swipes_router.post("/swipe/user")
|
||||||
|
async def read_user_swipes(
|
||||||
|
username: str,
|
||||||
|
feed_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
groupname = get_groupname_by_feed_id(conn, feed_id)
|
||||||
|
if groupname is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed or feed is not linked to group",
|
||||||
|
)
|
||||||
|
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
|
return db.get_swipes_by_user(conn, username, feed_id)
|
||||||
|
|
||||||
|
|
||||||
|
@swipes_router.post("/add")
|
||||||
|
async def add_swipe(
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int,
|
||||||
|
value: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
groupname = get_groupname_by_feed_id(conn, feed_id)
|
||||||
|
if groupname is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed or feed is not linked to group",
|
||||||
|
)
|
||||||
|
|
||||||
|
group_data = get_group(conn, groupname)
|
||||||
|
if group_data is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="No such feed or feed is not linked to group",
|
||||||
|
)
|
||||||
|
group = Group().fill(group_data)
|
||||||
|
|
||||||
|
# Check for trying to skip in
|
||||||
|
# a group with skips disabled
|
||||||
|
if value == 0 and group.allow_skips is False:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Skips are disallowed in the group",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = db.create_swipe(conn, current_user.username, feed_id, picture_id, value)
|
||||||
|
if result:
|
||||||
|
# TODO: call function to like picture on the platform
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@swipes_router.post("/delete")
|
||||||
|
async def delete_swipe(
|
||||||
|
username: str,
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int,
|
||||||
|
conn: Annotated[connection, Depends(get_db_connection)],
|
||||||
|
current_user: Annotated[User, Depends(get_current_user)]
|
||||||
|
):
|
||||||
|
if current_user.role in settings.settings.admin_roles:
|
||||||
|
return db.delete_swipe(conn, username, feed_id, picture_id)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not allowed",
|
||||||
|
)
|
||||||
@ -27,14 +27,13 @@ async def read_users_any(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Not allowed",
|
detail="Not allowed",
|
||||||
)
|
)
|
||||||
user = User()
|
|
||||||
user_data = db.get_user(conn, username)
|
user_data = db.get_user(conn, username)
|
||||||
if user_data is None:
|
if user_data is None:
|
||||||
return HTTPException(
|
return HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No such user",
|
detail="No such user",
|
||||||
)
|
)
|
||||||
user.fill(user_data)
|
user = User().fill(user_data)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import jwt
|
|||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from jwt.exceptions import InvalidTokenError
|
from jwt.exceptions import InvalidTokenError
|
||||||
|
|
||||||
# from passlib.context import CryptContext
|
|
||||||
from psycopg2._psycopg import connection
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
import db.groups
|
import db.groups
|
||||||
@ -16,8 +14,6 @@ import settings.startup_settings as startup_settings
|
|||||||
from api.models import Group, TokenData, User
|
from api.models import Group, TokenData, User
|
||||||
from db.internal import get_db_connection
|
from db.internal import get_db_connection
|
||||||
|
|
||||||
# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
|
|
||||||
|
|
||||||
@ -81,12 +77,11 @@ async def get_current_user(
|
|||||||
except InvalidTokenError:
|
except InvalidTokenError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
user = User()
|
|
||||||
user_data = db.users.get_user(conn, token_data.username)
|
user_data = db.users.get_user(conn, token_data.username)
|
||||||
if user_data is None:
|
if user_data is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
user.fill(user_data)
|
user = User().fill(user_data)
|
||||||
|
|
||||||
if user.disabled:
|
if user.disabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -96,6 +91,7 @@ async def get_current_user(
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_group_by_name(
|
def get_group_by_name(
|
||||||
conn: connection,
|
conn: connection,
|
||||||
groupname: str
|
groupname: str
|
||||||
@ -104,10 +100,8 @@ def get_group_by_name(
|
|||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail="No such group"
|
detail="No such group"
|
||||||
)
|
)
|
||||||
group = Group()
|
|
||||||
group_data = db.groups.get_group(conn, groupname)
|
group_data = db.groups.get_group(conn, groupname)
|
||||||
if group_data is None:
|
if group_data is None:
|
||||||
raise group_exception
|
raise group_exception
|
||||||
|
|
||||||
group.fill(group_data)
|
return Group().fill(group_data)
|
||||||
return group
|
|
||||||
|
|||||||
@ -3,16 +3,20 @@ import sys
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from api.accounts import accounts_router
|
||||||
from api.anon import anon_router
|
from api.anon import anon_router
|
||||||
from api.auth import auth_router
|
from api.auth import auth_router
|
||||||
|
from api.feeds import feeds_router
|
||||||
from api.general import general_router
|
from api.general import general_router
|
||||||
from api.groups import groups_router
|
from api.groups import groups_router
|
||||||
from api.memberships import memberships_router
|
from api.memberships import memberships_router
|
||||||
from api.pictures import pictures_router
|
from api.pictures import pictures_router
|
||||||
|
from api.swipes import swipes_router
|
||||||
from api.users import users_router
|
from api.users import users_router
|
||||||
from db.internal import connect_db, disconnect_db
|
from db.internal import connect_db, disconnect_db
|
||||||
from settings import startup_settings
|
from settings import startup_settings
|
||||||
from settings.settings import settings_down, settings_up
|
from settings.settings import settings_down, settings_up
|
||||||
|
from settings.startup_settings import setup_settings
|
||||||
|
|
||||||
docs_url = None
|
docs_url = None
|
||||||
if startup_settings.swagger_enabled:
|
if startup_settings.swagger_enabled:
|
||||||
@ -30,11 +34,12 @@ def create_app():
|
|||||||
{
|
{
|
||||||
"sink": sys.stdout,
|
"sink": sys.stdout,
|
||||||
"level": startup_settings.log_level,
|
"level": startup_settings.log_level,
|
||||||
"format": "<level>{level}: {message}</level>",
|
"format": "<level>{level}</level>: {message}",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_event_handler("startup", setup_settings)
|
||||||
app.add_event_handler("startup", connect_db)
|
app.add_event_handler("startup", connect_db)
|
||||||
app.add_event_handler("startup", settings_up)
|
app.add_event_handler("startup", settings_up)
|
||||||
|
|
||||||
@ -44,6 +49,9 @@ def create_app():
|
|||||||
app.include_router(users_router)
|
app.include_router(users_router)
|
||||||
app.include_router(groups_router)
|
app.include_router(groups_router)
|
||||||
app.include_router(memberships_router)
|
app.include_router(memberships_router)
|
||||||
|
app.include_router(accounts_router)
|
||||||
|
app.include_router(feeds_router)
|
||||||
|
app.include_router(swipes_router)
|
||||||
app.include_router(pictures_router)
|
app.include_router(pictures_router)
|
||||||
|
|
||||||
app.add_event_handler("shutdown", disconnect_db)
|
app.add_event_handler("shutdown", disconnect_db)
|
||||||
|
|||||||
154
src/db/accounts.py
Normal file
154
src/db/accounts.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
from api.models import Account
|
||||||
|
|
||||||
|
# account create and delete
|
||||||
|
|
||||||
|
def create_account(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
author: str,
|
||||||
|
platform: str,
|
||||||
|
login: str,
|
||||||
|
password: str,
|
||||||
|
metadata: dict
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
insert into picrinth.accounts
|
||||||
|
(groupname, author, platform, login,
|
||||||
|
password, metadata, created_at)
|
||||||
|
values (%s, %s, %s, %s, now())
|
||||||
|
returning id
|
||||||
|
""",
|
||||||
|
(groupname, author, platform, login, password, json.dumps(metadata)),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_account(
|
||||||
|
conn: connection,
|
||||||
|
account_id: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.accounts
|
||||||
|
where account_id = %s
|
||||||
|
""",
|
||||||
|
(account_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# account checks
|
||||||
|
|
||||||
|
def check_account_existence(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
platform: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select exists(
|
||||||
|
select 1
|
||||||
|
from picrinth.accounts
|
||||||
|
where groupname = %s and platform = %s
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
(groupname, platform),
|
||||||
|
)
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# account update
|
||||||
|
|
||||||
|
def update_account(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
author: str,
|
||||||
|
platform: str,
|
||||||
|
login: str,
|
||||||
|
password: str,
|
||||||
|
metadata: dict
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.accounts
|
||||||
|
SET author = %s,
|
||||||
|
login = %s,
|
||||||
|
password = %s,
|
||||||
|
metadata = %s
|
||||||
|
where groupname = %s and platform = %s
|
||||||
|
""",
|
||||||
|
(author, login, password, json.dumps(metadata), groupname, platform),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
def update_account_metadata(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
platform: str,
|
||||||
|
metadata: dict
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
update picrinth.accounts
|
||||||
|
SET metadata = %s
|
||||||
|
where groupname = %s and platform = %s
|
||||||
|
""",
|
||||||
|
(json.dumps(metadata), groupname, platform),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# account receiving
|
||||||
|
|
||||||
|
def get_accounts_by_group(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
) -> list[Account]:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select *
|
||||||
|
from picrinth.accounts
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
return [Account().fill(account_data) for (account_data,) in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_accounts_by_group_platform(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
platform: str
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select groupname, author,
|
||||||
|
platform, login, password,
|
||||||
|
metadata, created_at
|
||||||
|
from picrinth.accounts
|
||||||
|
where groupname = %s and platform = %s
|
||||||
|
""",
|
||||||
|
(groupname, platform),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
83
src/db/feeds.py
Normal file
83
src/db/feeds.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
# feed create and delete
|
||||||
|
|
||||||
|
def create_feed(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
image_ids: list[int]
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
insert into picrinth.feeds
|
||||||
|
(groupname, image_ids, created_at)
|
||||||
|
values (%s, %s, now())
|
||||||
|
returning id
|
||||||
|
""",
|
||||||
|
(groupname, image_ids),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_feed(
|
||||||
|
conn: connection,
|
||||||
|
feed_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.feeds
|
||||||
|
where id = %s
|
||||||
|
""",
|
||||||
|
(feed_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# feed receiving
|
||||||
|
|
||||||
|
def get_feed(
|
||||||
|
conn: connection,
|
||||||
|
feed_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select groupname
|
||||||
|
from picrinth.feeds
|
||||||
|
where id = %s
|
||||||
|
""",
|
||||||
|
(feed_id,),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# additional
|
||||||
|
|
||||||
|
def get_groupname_by_feed_id(
|
||||||
|
conn: connection,
|
||||||
|
feed_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select groupname
|
||||||
|
from picrinth.feeds
|
||||||
|
where id = %s
|
||||||
|
""",
|
||||||
|
(feed_id,),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
@ -79,6 +79,25 @@ def check_invite_code(
|
|||||||
return cur.fetchone()[0] # type: ignore
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def check_group_author(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str,
|
||||||
|
author: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select exists(
|
||||||
|
select 1
|
||||||
|
from picrinth.groups
|
||||||
|
where groupname = %s and author = %s
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
(groupname, author),
|
||||||
|
)
|
||||||
|
return cur.fetchone()[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# group updates
|
# group updates
|
||||||
|
|
||||||
def update_group_groupname(
|
def update_group_groupname(
|
||||||
@ -243,6 +262,7 @@ def get_groupname_by_invite_code(
|
|||||||
return None
|
return None
|
||||||
return result[0] # type: ignore
|
return result[0] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def get_group_invite_code(
|
def get_group_invite_code(
|
||||||
conn: connection,
|
conn: connection,
|
||||||
groupname: str
|
groupname: str
|
||||||
@ -260,3 +280,21 @@ def get_group_invite_code(
|
|||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return result[0] # type: ignore
|
return result[0] # type: ignore
|
||||||
|
|
||||||
|
def get_group_last_feed_id(
|
||||||
|
conn: connection,
|
||||||
|
groupname: str
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select last_feed_id
|
||||||
|
from picrinth.groups
|
||||||
|
where groupname = %s
|
||||||
|
""",
|
||||||
|
(groupname,),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0] # type: ignore
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import psycopg2.extras
|
|
||||||
from psycopg2._psycopg import connection
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
# membership create and delete
|
# membership create and delete
|
||||||
@ -65,7 +64,7 @@ def get_memberships_by_username(
|
|||||||
conn: connection,
|
conn: connection,
|
||||||
username: str
|
username: str
|
||||||
):
|
):
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
select *
|
select *
|
||||||
@ -81,7 +80,7 @@ def get_memberships_by_groupname(
|
|||||||
conn: connection,
|
conn: connection,
|
||||||
groupname: str
|
groupname: str
|
||||||
):
|
):
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
select *
|
select *
|
||||||
|
|||||||
@ -29,22 +29,7 @@ def create_picture(
|
|||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
def delete_picture_by_url(
|
def delete_picture(
|
||||||
conn: connection,
|
|
||||||
url: str
|
|
||||||
):
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
delete from picrinth.pictures
|
|
||||||
where url = %s
|
|
||||||
""",
|
|
||||||
(url,),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
return cur.rowcount > 0
|
|
||||||
|
|
||||||
def delete_picture_by_id(
|
|
||||||
conn: connection,
|
conn: connection,
|
||||||
id: int
|
id: int
|
||||||
):
|
):
|
||||||
@ -62,26 +47,9 @@ def delete_picture_by_id(
|
|||||||
|
|
||||||
# picture receiving
|
# picture receiving
|
||||||
|
|
||||||
def get_picture_by_url(
|
def get_picture(
|
||||||
conn: connection,
|
conn: connection,
|
||||||
url: str
|
id: int
|
||||||
):
|
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
select id, source,
|
|
||||||
external_id, url,
|
|
||||||
metadata, created_at
|
|
||||||
from picrinth.pictures
|
|
||||||
where url = %s
|
|
||||||
""",
|
|
||||||
(url,),
|
|
||||||
)
|
|
||||||
return cur.fetchone()
|
|
||||||
|
|
||||||
def get_picture_by_id(
|
|
||||||
conn: connection,
|
|
||||||
id: str
|
|
||||||
):
|
):
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@ -95,3 +63,16 @@ def get_picture_by_id(
|
|||||||
(id,),
|
(id,),
|
||||||
)
|
)
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_pictures_ids(
|
||||||
|
conn: connection,
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select id
|
||||||
|
from picrinth.pictures
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return [element for (element,) in cur.fetchall()]
|
||||||
|
|||||||
100
src/db/swipes.py
Normal file
100
src/db/swipes.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import psycopg2.extras
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
# swipe create and delete
|
||||||
|
|
||||||
|
def create_swipe(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int,
|
||||||
|
value: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
insert into picrinth.swipes
|
||||||
|
(username, feed_id, picture_id, value, created_at)
|
||||||
|
values (%s, %s, %s, %s, now())
|
||||||
|
returning id
|
||||||
|
""",
|
||||||
|
(username, feed_id, picture_id, value),
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
|
def delete_swipe(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
delete from picrinth.swipes
|
||||||
|
where username = %s and picture_id = %s and feed_id = %s
|
||||||
|
""",
|
||||||
|
(username, picture_id, feed_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
# swipe receiving
|
||||||
|
|
||||||
|
def get_swipe(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select username,
|
||||||
|
feed_id, picture_id,
|
||||||
|
value, created_at
|
||||||
|
from picrinth.swipes
|
||||||
|
where username = %s and feed_id = %s and picture_id = %s
|
||||||
|
""",
|
||||||
|
(username, feed_id, picture_id),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def get_swipes_by_picture_id(
|
||||||
|
conn: connection,
|
||||||
|
feed_id: int,
|
||||||
|
picture_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select *
|
||||||
|
from picrinth.swipes
|
||||||
|
where feed_id = %s and picture_id = %s
|
||||||
|
""",
|
||||||
|
(feed_id, picture_id),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
|
|
||||||
|
def get_swipes_by_user(
|
||||||
|
conn: connection,
|
||||||
|
username: str,
|
||||||
|
feed_id: int
|
||||||
|
):
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
select *
|
||||||
|
from picrinth.swipes
|
||||||
|
where username = %s and feed_id = %s
|
||||||
|
""",
|
||||||
|
(username, feed_id),
|
||||||
|
)
|
||||||
|
return cur.fetchone()
|
||||||
@ -176,8 +176,7 @@ def get_user(
|
|||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
select username, password, role,
|
select username, password, role,
|
||||||
disabled, groups_ids,
|
disabled, last_seen_at, created_at
|
||||||
last_seen_at, created_at
|
|
||||||
from picrinth.users
|
from picrinth.users
|
||||||
where username = %s
|
where username = %s
|
||||||
""",
|
""",
|
||||||
|
|||||||
10
src/scraper/gelbooru.py
Normal file
10
src/scraper/gelbooru.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.pictures as db
|
||||||
|
from api.models import Account, Picture
|
||||||
|
|
||||||
|
|
||||||
|
def gelbooru(conn: connection, account: Account):
|
||||||
|
picture = Picture(external_id = "3", url = "", metadata = {})
|
||||||
|
picture_id = db.create_picture(conn, "gelbooru", picture.external_id, picture.url, picture.metadata)
|
||||||
|
return picture_id
|
||||||
10
src/scraper/pinterest.py
Normal file
10
src/scraper/pinterest.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.pictures as db
|
||||||
|
from api.models import Account, Picture
|
||||||
|
|
||||||
|
|
||||||
|
def pinterest(conn: connection, account: Account):
|
||||||
|
picture = Picture(external_id = "1", url = "", metadata = {})
|
||||||
|
picture_id = db.create_picture(conn, "pinterest", picture.external_id, picture.url, picture.metadata)
|
||||||
|
return picture_id
|
||||||
132
src/scraper/pixiv.py
Normal file
132
src/scraper/pixiv.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
from fastapi import HTTPException, status
|
||||||
|
from gppt import GetPixivToken
|
||||||
|
from loguru import logger
|
||||||
|
from pixivpy3 import AppPixivAPI
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.pictures as db
|
||||||
|
from api.models import Account, Picture
|
||||||
|
from db.accounts import update_account_metadata
|
||||||
|
|
||||||
|
# Wrapper functions
|
||||||
|
|
||||||
|
def pixiv(conn: connection, account: Account) -> list[int]:
|
||||||
|
# Getting refresh token
|
||||||
|
refresh_token = account.metadata.get('refresh_token', '')
|
||||||
|
if not refresh_token:
|
||||||
|
try:
|
||||||
|
refresh_token = get_refresh_token(account.login, account.password)
|
||||||
|
account.metadata['refresh_token'] = refresh_token
|
||||||
|
update_account_metadata(conn, account.groupname, account.platform, account.metadata)
|
||||||
|
except Exception as e:
|
||||||
|
# TODO: review ruff "do not use bare `except`"
|
||||||
|
logger.debug(f"Pixiv refresh token missing and creation failed: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED,
|
||||||
|
detail="Pixiv refresh token missing and creation failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Logging into pixiv
|
||||||
|
try:
|
||||||
|
api = auth(refresh_token)
|
||||||
|
except Exception as e:
|
||||||
|
# TODO: review ruff "do not use bare `except`"
|
||||||
|
logger.debug(f"Pixiv refresh token invalid and recreation failed: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED,
|
||||||
|
detail="Pixiv refresh token invalid and recreation failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Getting pixiv account recommendations
|
||||||
|
pictures = []
|
||||||
|
try:
|
||||||
|
pictures = get_recommended(api, 20)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get recommendations from pixiv: {e}")
|
||||||
|
if not pictures:
|
||||||
|
logger.error("Failed to generate feed from pixiv")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Saving recommendations as Pictures to DB
|
||||||
|
pictures_ids = []
|
||||||
|
for picture in pictures:
|
||||||
|
pictures_ids.append(db.create_picture(conn, "pixiv", picture.external_id, picture.url, picture.metadata))
|
||||||
|
return pictures_ids
|
||||||
|
|
||||||
|
|
||||||
|
def like_picture(conn: connection, account: Account, picture_id: int):
|
||||||
|
# Getting refresh token
|
||||||
|
refresh_token = account.metadata.get('refresh_token', '')
|
||||||
|
if not refresh_token:
|
||||||
|
try:
|
||||||
|
refresh_token = get_refresh_token(account.login, account.password)
|
||||||
|
account.metadata['refresh_token'] = refresh_token
|
||||||
|
update_account_metadata(conn, account.groupname, account.platform, account.metadata)
|
||||||
|
except Exception as e:
|
||||||
|
# TODO: review ruff "do not use bare `except`"
|
||||||
|
logger.debug(f"Pixiv refresh token missing and creation failed: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED,
|
||||||
|
detail="Pixiv refresh token missing and creation failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Logging into pixiv
|
||||||
|
try:
|
||||||
|
api = auth(refresh_token)
|
||||||
|
except Exception as e:
|
||||||
|
# TODO: review ruff "do not use bare `except`"
|
||||||
|
logger.debug(f"Pixiv refresh token invalid and recreation failed: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED,
|
||||||
|
detail="Pixiv refresh token invalid and recreation failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Liking
|
||||||
|
return like(api, picture_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
|
def get_refresh_token(login: str, password: str) -> str:
|
||||||
|
g = GetPixivToken(headless=True)
|
||||||
|
login_response = g.login(username=login, password=password)
|
||||||
|
refresh_token = login_response.get("refresh_token", '')
|
||||||
|
if not refresh_token:
|
||||||
|
raise Exception
|
||||||
|
return refresh_token
|
||||||
|
|
||||||
|
def auth(refresh_token: str) -> AppPixivAPI:
|
||||||
|
api = AppPixivAPI()
|
||||||
|
api.auth(refresh_token)
|
||||||
|
print("Login successful") # TODO: delete
|
||||||
|
return api
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_refresh_token(refresh_token: str) -> str:
|
||||||
|
g = GetPixivToken(headless=True)
|
||||||
|
# TODO: save new refresh_token to DB + account + add auto updating as coroutine
|
||||||
|
response = g.refresh(refresh_token)
|
||||||
|
print(response) # TODO: delete
|
||||||
|
return response.get("refresh_token", '')
|
||||||
|
|
||||||
|
|
||||||
|
# Main functions
|
||||||
|
|
||||||
|
def get_recommended(api: AppPixivAPI, recommendations_number: int = 20) -> list[Picture]:
|
||||||
|
result = api.illust_recommended()
|
||||||
|
|
||||||
|
# TODO: make recommendations number useful + add as var to settings/group settings
|
||||||
|
# illusts = result.illusts[:recommendations_number]
|
||||||
|
illusts = result.illusts
|
||||||
|
|
||||||
|
pictures = []
|
||||||
|
for illust in illusts:
|
||||||
|
if illust.type == "illust":
|
||||||
|
picture = Picture(external_id=illust.id, source="pixiv", url=illust.image_urls.large, metadata={})
|
||||||
|
pictures.append(picture)
|
||||||
|
return pictures
|
||||||
|
|
||||||
|
|
||||||
|
def like(api: AppPixivAPI, illust_id: int):
|
||||||
|
api.illust_bookmark_add(illust_id)
|
||||||
|
print(f"Picture {illust_id} liked!") # TODO: delete
|
||||||
53
src/scraper/utils.py
Normal file
53
src/scraper/utils.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from loguru import logger
|
||||||
|
from psycopg2._psycopg import connection
|
||||||
|
|
||||||
|
import db.pictures as db
|
||||||
|
from api.models import Account, Picture
|
||||||
|
from scraper.gelbooru import gelbooru
|
||||||
|
from scraper.pinterest import pinterest
|
||||||
|
from scraper.pixiv import pixiv
|
||||||
|
from settings import startup_settings
|
||||||
|
|
||||||
|
|
||||||
|
def generate_feed(
|
||||||
|
conn: connection,
|
||||||
|
accounts: list[Account]
|
||||||
|
) -> list:
|
||||||
|
feed = []
|
||||||
|
for account in accounts:
|
||||||
|
if account.platform not in startup_settings.platforms_enabled:
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
match account.platform:
|
||||||
|
case "pinterest":
|
||||||
|
temp_feed = pinterest(conn, account)
|
||||||
|
feed.append(temp_feed)
|
||||||
|
|
||||||
|
case "pixiv":
|
||||||
|
temp_feed = pixiv(conn, account)
|
||||||
|
feed.append(temp_feed)
|
||||||
|
|
||||||
|
case "gelbooru":
|
||||||
|
temp_feed = gelbooru(conn, account)
|
||||||
|
feed.append(temp_feed)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
logger.warning(f"Platform for feed generation is not supported: {account.platform}")
|
||||||
|
|
||||||
|
# TODO: remove mock results
|
||||||
|
feed = db.get_all_pictures_ids(conn)
|
||||||
|
return feed
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Implement process_swipe
|
||||||
|
def process_swipe():
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_picture(
|
||||||
|
conn: connection,
|
||||||
|
picture_id: int
|
||||||
|
) -> int:
|
||||||
|
picture_data = db.get_picture(conn, picture_id)
|
||||||
|
if picture_id is None:
|
||||||
|
return -1
|
||||||
|
return Picture().fill(picture_data).id
|
||||||
@ -1 +1,20 @@
|
|||||||
JOIN_CODE_SYMBOLS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No O + 0, I + 1
|
# No O + 0, I + 1. All in upper case
|
||||||
|
JOIN_CODE_SYMBOLS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
|
||||||
|
# All platforms should be written in lower case
|
||||||
|
SUPPORTED_PLATFORMS = ["pinterest", "pixiv", "gelbooru"]
|
||||||
|
|
||||||
|
# Info for settings update endpoint
|
||||||
|
API_EDITABLE_SETTINGS_LIST = """
|
||||||
|
admin_roles ["admin"]
|
||||||
|
|
||||||
|
allow_create_admins_by_admins True
|
||||||
|
|
||||||
|
allow_create_admins True
|
||||||
|
|
||||||
|
allow_create_users True
|
||||||
|
|
||||||
|
allow_create_groups True
|
||||||
|
|
||||||
|
allow_create_pictures True
|
||||||
|
"""
|
||||||
|
|||||||
@ -12,7 +12,7 @@ class Settings(BaseModel):
|
|||||||
self.allow_create_admins = params["allow_create_admins"]
|
self.allow_create_admins = params["allow_create_admins"]
|
||||||
self.allow_create_users = params["allow_create_users"]
|
self.allow_create_users = params["allow_create_users"]
|
||||||
self.allow_create_groups = params["allow_create_groups"]
|
self.allow_create_groups = params["allow_create_groups"]
|
||||||
self.allow_create_pictures = params["allow_create_groups"]
|
self.allow_create_pictures = params["allow_create_pictures"]
|
||||||
admin_roles: list[str] = ["admin"]
|
admin_roles: list[str] = ["admin"]
|
||||||
allow_create_admins_by_admins: bool = True
|
allow_create_admins_by_admins: bool = True
|
||||||
allow_create_admins: bool = True
|
allow_create_admins: bool = True
|
||||||
@ -61,9 +61,7 @@ def settings_down():
|
|||||||
def reset_settings():
|
def reset_settings():
|
||||||
global settings, json_path, json_settings_name
|
global settings, json_path, json_settings_name
|
||||||
logger.info("Resetting settings")
|
logger.info("Resetting settings")
|
||||||
print(settings)
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
print(settings)
|
|
||||||
with open(json_path + json_settings_name, "w") as f:
|
with open(json_path + json_settings_name, "w") as f:
|
||||||
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
|
||||||
logger.info("Wrote settings to the JSON")
|
logger.info("Wrote settings to the JSON")
|
||||||
|
|||||||
@ -1,24 +1,36 @@
|
|||||||
from decouple import config
|
from decouple import Csv, config
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .consts import SUPPORTED_PLATFORMS
|
||||||
def str_to_bool(string: str) -> bool:
|
|
||||||
if string.lower() == "true":
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# database
|
# database
|
||||||
db_host = str(config("db_host", default="127.0.0.1"))
|
db_host: str = config("db_host", default="127.0.0.1", cast=str) # type: ignore
|
||||||
db_port = int(config("db_port", default=5432))
|
db_port: int = config("db_port", default=5432, cast=int) # type: ignore
|
||||||
db_name = str(config("db_name", default="postgres"))
|
db_name: str = config("db_name", default="postgres", cast=str) # type: ignore
|
||||||
db_user = str(config("db_user", default="postgres"))
|
db_user: str = config("db_user", default="postgres", cast=str) # type: ignore
|
||||||
db_password = str(config("db_password", default="postgres"))
|
db_password: str = config("db_password", default="postgres", cast=str) # type: ignore
|
||||||
|
|
||||||
# auth
|
# auth
|
||||||
secret_key = str(config("secret_key"))
|
secret_key: str = config("secret_key", cast=str) # type: ignore
|
||||||
algorithm = str(config("algorithm", "HS256"))
|
algorithm: str = config("algorithm", "HS256", cast=str) # type: ignore
|
||||||
access_token_expiration_time = int(config("access_token_expiration_time", default=10080))
|
access_token_expiration_time: int = config("access_token_expiration_time", default=10080, cast=int) # type: ignore
|
||||||
|
|
||||||
# other settings
|
# other settings
|
||||||
swagger_enabled = str_to_bool(str(config("swagger_enabled", "false")))
|
join_code_length: int = config("join_code_length", default=8, cast=int) # type: ignore
|
||||||
log_level = str(config("log_level", default="INFO"))
|
platforms_enabled: list[str] = config("platforms_enabled", default=SUPPORTED_PLATFORMS, cast=Csv(str)) # type: ignore
|
||||||
join_code_length = int(config("join_code_length", default=8))
|
|
||||||
|
# dev
|
||||||
|
swagger_enabled: bool = config("swagger_enabled", "false", cast=bool) # type: ignore
|
||||||
|
log_level: str = config("log_level", default="INFO", cast=str) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def setup_settings():
|
||||||
|
global platforms_enabled
|
||||||
|
platforms_supported_and_enabled = []
|
||||||
|
for platform in platforms_enabled:
|
||||||
|
if platform.lower() in SUPPORTED_PLATFORMS:
|
||||||
|
platforms_supported_and_enabled.append(platform.lower())
|
||||||
|
else:
|
||||||
|
logger.warning(f"Platform {platform} is not supported by design. It will not be used.")
|
||||||
|
logger.info(f"Supported platforms list: {platforms_supported_and_enabled}")
|
||||||
|
platforms_enabled = platforms_supported_and_enabled
|
||||||
|
|||||||
19
tables.sql
19
tables.sql
@ -37,7 +37,7 @@ create table picrinth.pictures (
|
|||||||
id serial not null,
|
id serial not null,
|
||||||
source text not null,
|
source text not null,
|
||||||
external_id text not null,
|
external_id text not null,
|
||||||
url text not null,
|
"url" text not null,
|
||||||
metadata jsonb null,
|
metadata jsonb null,
|
||||||
created_at timestamp with time zone default now(),
|
created_at timestamp with time zone default now(),
|
||||||
constraint pictures_pkey primary key (id),
|
constraint pictures_pkey primary key (id),
|
||||||
@ -59,11 +59,24 @@ create table picrinth.swipes (
|
|||||||
username text not null,
|
username text not null,
|
||||||
feed_id integer not null,
|
feed_id integer not null,
|
||||||
picture_id integer not null,
|
picture_id integer not null,
|
||||||
swipe_value smallint not null,
|
"value" smallint not null,
|
||||||
swiped_at timestamp with time zone default now(),
|
created_at timestamp with time zone default now(),
|
||||||
primary key (id),
|
primary key (id),
|
||||||
foreign key (username) references picrinth.users (username) on delete cascade on update cascade,
|
foreign key (username) references picrinth.users (username) on delete cascade on update cascade,
|
||||||
foreign key (feed_id) references picrinth.feeds (id) on delete cascade on update cascade,
|
foreign key (feed_id) references picrinth.feeds (id) on delete cascade on update cascade,
|
||||||
foreign key (picture_id) references picrinth.pictures (id) on delete cascade on update cascade,
|
foreign key (picture_id) references picrinth.pictures (id) on delete cascade on update cascade,
|
||||||
constraint swipes_unique unique (username, feed_id, picture_id)
|
constraint swipes_unique unique (username, feed_id, picture_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create table picrinth.accounts (
|
||||||
|
groupname text not null,
|
||||||
|
author text null,
|
||||||
|
platform text not null,
|
||||||
|
"login" text not null,
|
||||||
|
"password" text not null,
|
||||||
|
metadata jsonb null,
|
||||||
|
created_at timestamp with time zone default now(),
|
||||||
|
foreign key (groupname) references picrinth.groups (groupname) on delete cascade on update cascade,
|
||||||
|
foreign key (author) references picrinth.groups (author) on delete cascade on update cascade,
|
||||||
|
constraint unique_account_for_group_per_platform unique (groupname, platform)
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user